#!/usr/bin/env node /** * Missed-activation matcher (Pravila §16.4 + §17, Phase 2 Task 11). * Pure deterministic — read-only, no exec, no fs. * * Two episode schemas supported: * * SCHEMA v4 (LLM-first router, §17): * 1. schema_version === 4 * 2. NOT observer_error * 3. classifier_output.task_type ∉ {conversation, micro, manual_override} (§17 exempt set) * 4. classifier_output.no_skill_found !== true (classifier honestly admits no match → not a miss) * 5. classifier_output.recommended_node is set * 6. dormancy[recommended_node] !== true (still callable) * 7. execution_trace.actual_node_invoked_first === 'direct' (no real node fired first) * → byNode[recommended_node]++, byClassification[task_type]++ * * SCHEMA v2/v3 (legacy, §16.4 conditional rule): * 1. schema_version >= 2 && < 4 * 2. NOT observer_error * 3. primary_rationale.task_classification ∈ classificationMap AND map[c].length > 0 * 4. primary_rationale.node_chosen === 'direct' * 5. AT LEAST ONE recommended node is non-dormant * * Threshold: single episode (per Pravila §16.4 v1.36). * classificationMap/dormancy positional args remain (back-compat with brain-retro- * analyzer + observer-coverage-checker call sites); for v4 episodes the map arg * is ignored — recommended_node is inline in the episode. */ const V4_EXEMPT_TASK_TYPES = new Set(['conversation', 'micro', 'manual_override']); export function detectMissedActivations(episodes, classificationMap = {}, dormancy = {}) { const byNode = {}; const byClassification = {}; let totalMissed = 0; for (const e of episodes) { if (!e || e.observer_error) continue; if (typeof e.schema_version !== 'number') continue; // ── v4 path (§17 LLM-first) ───────────────────────────────────────── if (e.schema_version >= 4) { const co = e.classifier_output || {}; const tr = e.execution_trace || {}; if (!co.task_type || V4_EXEMPT_TASK_TYPES.has(co.task_type)) continue; if (co.no_skill_found) continue; if (!co.recommended_node) continue; if (dormancy[co.recommended_node] === true) continue; const invokedFirst = tr.actual_node_invoked_first; if (invokedFirst && invokedFirst !== 'direct') continue; totalMissed += 1; byClassification[co.task_type] = (byClassification[co.task_type] || 0) + 1; byNode[co.recommended_node] = (byNode[co.recommended_node] || 0) + 1; continue; } // ── v2/v3 legacy path (§16.4) ─────────────────────────────────────── if (e.schema_version < 2) continue; const pr = e.primary_rationale || {}; const cls = pr.task_classification; const chosen = pr.node_chosen; if (!cls || chosen !== 'direct') continue; const recommended = classificationMap[cls]; if (!Array.isArray(recommended) || recommended.length === 0) continue; const live = recommended.filter((id) => dormancy[id] === false); if (live.length === 0) continue; totalMissed += 1; byClassification[cls] = (byClassification[cls] || 0) + 1; for (const id of live) { byNode[id] = (byNode[id] || 0) + 1; } } return { totalMissed, byNode, byClassification }; }