#!/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 };
}
