/**
 * Recursive Healing Algorithm (L2).
 *
 * On failure: locate the failing subsystem (bind findings to feature nodes
 * through the ownership index), pick the smallest meaningful repair target
 * (the upstream-most failing node in the dependency tree), apply a bounded
 * fix (write boundary = the node's owned files, enforced deterministically),
 * rerun the affected check, then rerun the parent checks the change could
 * have touched (the node's dependents, read from the supergit forward
 * pointers). A failure surfaced at the parent level becomes its own error
 * node and triggers its own bounded repair — the recursion is bidirectional
 * and moves through the software like a diagnostic tree.
 *
 * The oracle is two-layered:
 *   - the standing taxonomy of mechanical bug classes (deterministic
 *     detectors, run globally each sweep — localization AND generalization)
 *   - the frozen IR (idiosyncratic behavior failures)
 *
 * The artifact is never wholly regenerated: there is no code path here that
 * can write outside one node's ownership set.
 */
import fs from 'node:fs';
import path from 'node:path';
import type {
  EscalationEntry,
  Finding,
  GeneratedFile,
  IRManifest,
  FeatureSpec,
  JobReport,
  LlmClient,
  RepairEpisode,
} from './types';
import { Substrate } from './substrate';
import { runVitest } from './sandbox';
import { sweepTaxonomy } from './taxonomy';
import { boundaryViolations, dependentsOf, depthOf, readFiles, verifyFreeze, writeFiles } from './pipeline';
import type { Emit } from './pipeline';

import { getConfig, swarmTemperatures } from './config';
import { raiseSev1 } from './alerts';
import type { JobState } from './types';
import { EscalationQueue, ownerMap } from './workQueue';
import { partitionFeatureIds } from './roleRouter';

// Ceilings are backstops, not the mechanism: a node is escalated when its
// finding signature stops changing (stall), not when an arbitrary count hits.
// Bounded passes are cheap, so a weak model may spend many as long as each
// one moves. Tune per deployment: a local model on free tokens can afford
// far more passes than metered API calls.

/** Yield to the event loop so queued SSE writes flush between blocking runs. */
const tick = () => new Promise<void>(r => setImmediate(r));

/** Mechanical classes outrank the generic test-failure when choosing focus. */
const CLASS_PRIORITY: Finding['bugClass'][] = [
  'syntax',
  'unresolved-import',
  'undefined-symbol',
  'unwired-export',
  'test-orphan',
  'type-mismatch',
  'env-var-unset',
  'todo-stub',
  'test-failure',
];

export interface Sweep {
  findings: Finding[];
  byFeature: Map<string, Finding[]>;
  totalPassed: number;
  totalFailed: number;
  failureOutput: string;
}

/** One full verification pass: taxonomy detectors + the entire frozen IR. */
export function fullSweep(dir: string, manifest: IRManifest): Sweep {
  const findings: Finding[] = [...sweepTaxonomy(dir, manifest)];
  const run = runVitest(dir);
  const testFileOwner = new Map(manifest.features.map(f => [normalize(f.testFile), f.id]));
  for (const fail of run.failures) {
    findings.push({
      bugClass: 'test-failure',
      file: normalize(fail.file),
      line: null,
      message: fail.message,
      featureId: testFileOwner.get(normalize(fail.file)) ?? null,
      testName: fail.name,
    });
  }
  const byFeature = new Map<string, Finding[]>();
  for (const f of findings) {
    const key = f.featureId ?? '(unbound)';
    if (!byFeature.has(key)) byFeature.set(key, []);
    byFeature.get(key)!.push(f);
  }
  const failureOutput = findings
    .map(f => `[${f.bugClass}] ${f.file}${f.line ? ':' + f.line : ''}${f.testName ? ' (' + f.testName + ')' : ''} ${f.message}`)
    .join('\n');
  return { findings, byFeature, totalPassed: run.passed, totalFailed: run.failed, failureOutput };
}

function normalize(p: string): string {
  return p.replace(/\\/g, '/').replace(/^\.\//, '');
}

function dominantClass(findings: Finding[]): Finding['bugClass'] {
  for (const c of CLASS_PRIORITY) if (findings.some(f => f.bugClass === c)) return c;
  return 'test-failure';
}

export interface ApplyRhaOpts {
  /** Part N: Supervisor for escalation (modes decide when). */
  supervisor?: LlmClient | null;
  /** Combinable Supervisor toggles. */
  modes?: { resolveAllFailures?: boolean; resolveEscalationsOnly?: boolean };
  /** Dual operators for N5 work-stealing (length ≥ 2). */
  operators?: LlmClient[];
}

export async function applyRha(
  dir: string,
  spec: FeatureSpec,
  manifest: IRManifest,
  substrate: Substrate,
  llm: LlmClient,
  emit: Emit,
  job?: JobState,
  /** Part N: optional Supervisor, or full ApplyRhaOpts. */
  supervisorOrOpts?: LlmClient | null | ApplyRhaOpts,
): Promise<JobReport> {
  const opts: ApplyRhaOpts = supervisorOrOpts && typeof supervisorOrOpts === 'object' && 'repair' in (supervisorOrOpts as LlmClient)
    ? { supervisor: supervisorOrOpts as LlmClient }
    : (supervisorOrOpts as ApplyRhaOpts | null | undefined) ?? {};
  const supervisor = opts.supervisor ?? null;
  const modes = opts.modes ?? { resolveAllFailures: false, resolveEscalationsOnly: !!supervisor };
  const operators = opts.operators?.length ? opts.operators : [llm];
  const dual = operators.length >= 2;
  const queue = dual ? new EscalationQueue() : null;
  const owners = dual
    ? ownerMap(...partitionFeatureIds(spec.features))
    : null;
  if (dual && owners) {
    emit('workqueue-partition', {
      a: [...owners.entries()].filter(([, o]) => o === 0).map(([id]) => id),
      b: [...owners.entries()].filter(([, o]) => o === 1).map(([id]) => id),
    });
  }
  /** Features already escalated (human surface) — skip re-targeting. */
  const escalatedIds = new Set<string>();
  const {
    maxEpisodes: MAX_EPISODES,
    maxAttempts: MAX_ATTEMPTS_PER_NODE,
    stallLimit: STALL_LIMIT,
    swarmWidth: defaultSwarmWidth,
  } = getConfig('operator');
  const episodes: RepairEpisode[] = [];
  const filesTouched = new Set<string>();
  const depth = depthOf(spec.features);
  const attemptsByFeature = new Map<string, number>();
  // Progress detection: a node whose finding signature is identical across
  // STALL_LIMIT consecutive attempts is stalled — repairs are changing
  // nothing — and gets escalated regardless of remaining attempt budget.
  const lastSigByFeature = new Map<string, string>();
  const stallByFeature = new Map<string, number>();
  // PROGRESS-based stall (oscillation-proof). Signature-identity is blind to a
  // node whose findings keep CHANGING but never DROP (the 1,2,3,1,2 grind).
  // Track the fewest findings ever seen; no improvement across STALL_LIMIT
  // attempts = stalled, even when every attempt looks "different".
  const bestCountByFeature = new Map<string, number>();
  const noImproveByFeature = new Map<string, number>();
  const findingSig = (list: Finding[]) =>
    list.map(x => `${x.bugClass}:${x.file}:${x.line}:${x.message.slice(0, 80)}`).sort().join('|');

  for (let episode = 1; episode <= MAX_EPISODES; episode++) {
    await tick();
    verifyFreeze(dir, manifest);
    const sweep = fullSweep(dir, manifest);
    emit('rha-sweep', {
      episode,
      findings: sweep.findings.length,
      failed: sweep.totalFailed,
      passed: sweep.totalPassed,
      classes: [...new Set(sweep.findings.map(f => f.bugClass))],
    });
    if (sweep.findings.length === 0) break;

    // Smallest meaningful repair target: the upstream-most failing node —
    // healing it may clear findings inherited by everything downstream.
    // N5: skip already-escalated; prefer own-partition / steal from queue.
    let failingIds = [...sweep.byFeature.keys()]
      .filter(k => k !== '(unbound)' && !escalatedIds.has(k));
    if (failingIds.length === 0) {
      if (sweep.findings.length && escalatedIds.size === 0) {
        substrate.recordNode({
          type: 'error',
          bugClass: 'test-failure',
          status: 'open',
          summary: `unbound findings: ${sweep.findings.length} — manual review required`,
        });
      }
      break;
    }
    failingIds.sort((a, b) => (depth.get(a) ?? 0) - (depth.get(b) ?? 0));
    let targetId = failingIds[0];
    let stolen = false;
    if (dual && owners && queue) {
      // Finish non-queued partition work first; only then drain the steal queue.
      const queued = new Set(queue.peek());
      const ownFirst = failingIds.filter(id => !queued.has(id));
      if (ownFirst.length) {
        targetId = ownFirst[0];
      } else {
        const stolenId = queue.trySteal(failingIds);
        if (stolenId) {
          targetId = stolenId;
          stolen = true;
          emit('workqueue-steal', { featureId: targetId });
        }
      }
    }
    const feature = spec.features.find(f => f.id === targetId)!;
    const findings = sweep.byFeature.get(targetId)!;
    const bugClass = dominantClass(findings);
    const attempt = (attemptsByFeature.get(targetId) ?? 0) + 1;
    attemptsByFeature.set(targetId, attempt);
    const opIdx = owners?.get(targetId) ?? 0;
    // Peer steal uses the other operator (decorrelated temp/seed via different client).
    const activeLlm = stolen && dual
      ? operators[(opIdx + 1) % operators.length]
      : operators[opIdx] ?? llm;

    // Stall check — two signals, whichever trips first:
    //  (a) identical finding signature across consecutive attempts (fast exact-
    //      stuck), and
    //  (b) PROGRESS: the fewest findings seen never drops for STALL_LIMIT
    //      attempts. This catches the oscillating grind (1,2,3,1,2 findings,
    //      always "different" so (a) is blind, but never actually converging).
    const sig = findingSig(findings);
    if (lastSigByFeature.get(targetId) === sig) {
      stallByFeature.set(targetId, (stallByFeature.get(targetId) ?? 0) + 1);
    } else {
      stallByFeature.set(targetId, 0);
    }
    lastSigByFeature.set(targetId, sig);
    const prevBest = bestCountByFeature.get(targetId) ?? Infinity;
    if (findings.length < prevBest) {
      bestCountByFeature.set(targetId, findings.length);
      noImproveByFeature.set(targetId, 0);
    } else {
      noImproveByFeature.set(targetId, (noImproveByFeature.get(targetId) ?? 0) + 1);
    }
    const stalled = (stallByFeature.get(targetId) ?? 0) >= STALL_LIMIT
      || (noImproveByFeature.get(targetId) ?? 0) >= STALL_LIMIT;

    // ENTROPY RESET (anti-anchoring). A repeated identical signature means the
    // repairs are CORRELATED: the model keeps editing its own broken impl
    // (anchored) instead of exploring, so every attempt re-warms the same wrong
    // basin. Before giving up, take FRESH DRAWS from the frozen contract ALONE
    // — no broken impl in context — a swarm of them, and let the verifier keep
    // the best. Supergit branch logic brought to repair: grinding one basin ⇒
    // branch to fresh ones. Bounded to THIS node's files — not artifact-wide
    // regeneration. Different-each-run variance says the answer IS in the
    // model's distribution; this un-anchors so it can land on it.
    if (stalled && attempt <= MAX_ATTEMPTS_PER_NODE) {
      const width = Math.max(1, defaultSwarmWidth);
      const frozenTest = fs.readFileSync(path.join(dir, feature.testFile), 'utf8');
      const depCtx = readFiles(dir, feature.deps.flatMap(d => spec.features.find(x => x.id === d)!.files));
      const snapshot = readFiles(dir, feature.files);
      // Score candidates by the SAME metric that targets/stalls the node:
      // taxonomy findings on this node + its test failures. Judging by tests
      // alone is blind to detector classes (todo-stub etc.) — a baseline of 0
      // test-failures made every draw undiscardable-loser by definition, and
      // threw away draws that actually fixed the finding.
      const nodeScore = () =>
        sweepTaxonomy(dir, manifest).filter(f => f.featureId === targetId).length
        + runVitest(dir, [feature.testFile]).failed;
      const baseline = nodeScore();
      // Decorrelate the fresh instances, or they all sample the same mode:
      //  - tabu: ORACLE facts from the discarded attempts (findings' expected/
      //    got lines) — the graveyard's shape, never the old code.
      //  - temperature ladder: same prompt at one temperature is one basin;
      //    the ladder reaches basins a single setting cannot.
      const tabu = findings
        .map(f => `- ${f.testName ? f.testName + ': ' : ''}${f.message.split('\n')[0].slice(0, 180)}`)
        .join('\n');
      const TEMPS = swarmTemperatures('operator');
      emit('rha-fresh-take', { featureId: targetId, width, baseline });
      const draws = await Promise.all(Array.from({ length: width }, (_, i) =>
        activeLlm.generateImplementation(spec, feature, frozenTest, depCtx, {
          temperature: TEMPS[i % TEMPS.length],
          avoid: tabu,
        })));
      let best: GeneratedFile[] | null = null;
      let bestFail = baseline;
      for (const files of draws) {
        if (boundaryViolations(files, feature).length) continue;
        writeFiles(dir, files);
        const score = nodeScore();
        if (score < bestFail) { bestFail = score; best = files; }
        if (score === 0) break;
      }
      if (best && bestFail < baseline) {
        writeFiles(dir, best);
        for (const f of best) filesTouched.add(normalize(f.path));
        const fresh = substrate.recordNode({
          type: 'repair',
          featureId: targetId,
          parentId: substrate.latest('code', targetId)?.id ?? null,
          status: bestFail === 0 ? 'pass' : 'info',
          summary: `fresh-take (entropy reset, width ${width}): ${baseline}→${bestFail} failing`,
        });
        substrate.commit(fresh, `rha(${targetId}): fresh-take from frozen contract (${baseline}→${bestFail})`);
        stallByFeature.set(targetId, 0);      // new basin — clear the stall
        lastSigByFeature.delete(targetId);    // do not re-trigger on the old signature
        noImproveByFeature.set(targetId, 0);  // fresh basin — progress clock resets
        bestCountByFeature.set(targetId, bestFail);
        episodes.push({
          episode, featureId: targetId, bugClass, attempt,
          outcome: bestFail === 0 ? 'healed' : 'failed',
          parentRecheck: 'n/a',
          summary: `fresh-take reset: ${baseline}→${bestFail} failing`,
        });
        continue; // re-sweep with the fresh impl
      }
      writeFiles(dir, snapshot); // fresh draws did not beat the anchored impl — restore and escalate
    }

    if (stalled || attempt > MAX_ATTEMPTS_PER_NODE) {
      // N5: enqueue-and-continue — never block waiting for the peer.
      if (dual && queue && !stolen) {
        queue.enqueue(targetId, opIdx);
        emit('workqueue-enqueue', { featureId: targetId, by: opIdx, queue: queue.peek() });
        const escQ = substrate.recordNode({
          type: 'decision', featureId: targetId, status: 'info',
          summary: `work-queue: enqueued ${targetId} for peer steal — continuing own partition`,
        });
        substrate.commit(escQ, `workqueue(${targetId}): enqueue`);
        // Reset stall so a later steal gets a fresh attempt budget window.
        stallByFeature.set(targetId, 0);
        noImproveByFeature.set(targetId, 0);
        lastSigByFeature.delete(targetId);
        continue;
      }

      // Part N4: Supervisor on escalations. Same write-boundary.
      if (supervisor && (modes.resolveEscalationsOnly || modes.resolveAllFailures)) {
        const frozenTest = fs.readFileSync(path.join(dir, feature.testFile), 'utf8');
        const causeNode = substrate.latest('repair', targetId) ?? substrate.latest('result', targetId) ?? substrate.latest('code', targetId);
        const lineageNode = causeNode ?? substrate.latest('code', targetId);
        emit('rha-supervisor', { featureId: targetId, attempt, reason: stalled ? 'stall' : 'ceiling' });
        try {
          // Fresh-only: regenerate from frozen contract — never feed broken impl.
          const tabu = findings
            .map(f => `- ${f.testName ? f.testName + ': ' : ''}${f.message.split('\n')[0].slice(0, 180)}`)
            .join('\n');
          const depCtx = readFiles(dir, feature.deps.flatMap(d => spec.features.find(x => x.id === d)!.files));
          const repaired = await supervisor.generateImplementation(
            spec, feature, frozenTest, depCtx, { avoid: tabu },
          );
          if (!boundaryViolations(repaired, feature).length) {
            const preFiles = readFiles(dir, feature.files);
            const preScore = sweepTaxonomy(dir, manifest).filter(f => f.featureId === targetId).length
              + runVitest(dir, [feature.testFile]).failed;
            writeFiles(dir, repaired);
            const postScore = sweepTaxonomy(dir, manifest).filter(f => f.featureId === targetId).length
              + runVitest(dir, [feature.testFile]).failed;
            if (postScore < preScore) {
              for (const f of repaired) filesTouched.add(normalize(f.path));
              const n = substrate.recordNode({
                type: 'repair', featureId: targetId,
                parentId: substrate.latest('code', targetId)?.id ?? null,
                status: postScore === 0 ? 'pass' : 'info',
                summary: `supervisor escalation repair: ${preScore}→${postScore} findings`,
              });
              substrate.commit(n, `rha(${targetId}): supervisor repair (${preScore}→${postScore})`);
              stallByFeature.set(targetId, 0);
              lastSigByFeature.delete(targetId);
              noImproveByFeature.set(targetId, 0);
              bestCountByFeature.set(targetId, postScore);
              queue?.resolve(targetId, opIdx);
              episodes.push({
                episode, featureId: targetId, bugClass, attempt,
                outcome: postScore === 0 ? 'healed' : 'failed',
                parentRecheck: 'n/a',
                summary: `supervisor repair: ${preScore}→${postScore}`,
              });
              continue;
            }
            writeFiles(dir, preFiles); // supervisor did not improve — restore
          }
        } catch (e) {
          emit('rha-supervisor', {
            featureId: targetId,
            error: e instanceof Error ? e.message : String(e),
          });
        }
      }

      // Bounded repair (incl. the fresh-take entropy reset) could not converge.
      // Record the escalation — never fall back to WHOLE-artifact regeneration.
      // Dual mode: continue other partitions; solo: stop (same as today).
      const why = stalled
        ? `stalled: no progress across ${STALL_LIMIT} consecutive attempts + fresh-take (attempt ${attempt})`
        : `attempt ceiling: ${MAX_ATTEMPTS_PER_NODE} bounded attempts spent`;
      const esc = substrate.recordNode({
        type: 'decision',
        featureId: targetId,
        status: 'fail',
        summary: `escalation: ${targetId} not healed — ${why} — surfacing to user`,
      });
      substrate.commit(esc, `rha(${targetId}): escalate (${stalled ? 'stall' : 'ceiling'})`);
      episodes.push({
        episode, featureId: targetId, bugClass, attempt,
        outcome: 'escalated', parentRecheck: 'n/a',
        summary: why,
      });
      escalatedIds.add(targetId);
      queue?.resolve(targetId, opIdx);
      if (dual) continue;
      break;
    }

    // --- error node: localized via ownership, generalized via same-class
    // --- instances elsewhere in the tree
    const causeNode = substrate.latest('repair', targetId) ?? substrate.latest('result', targetId) ?? substrate.latest('code', targetId);
    const errorNode = substrate.recordNode({
      type: 'error',
      featureId: targetId,
      parentId: substrate.latest('code', targetId)?.id ?? null,
      causeId: causeNode?.id ?? null,
      bugClass,
      status: 'open',
      summary: `${findings.length} finding(s), dominant class ${bugClass}: ${findings[0].message.slice(0, 140)}`,
    });
    const parallelInstances = sweep.findings.filter(
      f => f.bugClass === bugClass && f.featureId !== targetId,
    );
    emit('rha-episode', {
      episode, featureId: targetId, bugClass, attempt,
      findings: findings.length, parallel: parallelInstances.length,
    });

    // --- bounded FRESH repair from frozen contract (broken impl withheld)
    const frozenTest = fs.readFileSync(path.join(dir, feature.testFile), 'utf8');
    const depCtx = readFiles(dir, feature.deps.flatMap(d => spec.features.find(x => x.id === d)!.files));
    const tabu = findings
      .map(f => `- ${f.testName ? f.testName + ': ' : ''}${f.message.split('\n')[0].slice(0, 180)}`)
      .join('\n');
    const TEMPS = swarmTemperatures('operator');
    let repaired = await activeLlm.generateImplementation(spec, feature, frozenTest, depCtx, {
      temperature: TEMPS[(attempt - 1) % TEMPS.length],
      avoid: tabu,
    });
    let violations = boundaryViolations(repaired, feature);
    if (violations.length) {
      substrate.recordNode({
        type: 'decision',
        featureId: targetId,
        causeId: errorNode,
        status: 'fail',
        summary: `repair rejected — write-boundary violation: ${violations[0]}`,
      });
      episodes.push({
        episode, featureId: targetId, bugClass, attempt,
        outcome: 'failed', parentRecheck: 'n/a',
        summary: `repair rejected (boundary): ${violations.join('; ')}`,
      });
      continue; // next episode re-sweeps; attempt counter already advanced
    }
    // Ratchet (same doctrine as the cross-suite gate): a repair that leaves the
    // node WORSE than the sweep that targeted it gets REVERTED, not kept. The
    // smoke run walked a 6-passing node down to 3-passing straight through
    // here — regressions must never survive a repair episode.
    const preRepairFiles = readFiles(dir, feature.files);
    const preScore = findings.length;
    writeFiles(dir, repaired);
    const repairNode = substrate.recordNode({
      type: 'repair',
      featureId: targetId,
      parentId: errorNode,
      causeId: errorNode,
      bugClass,
      artifactPath: repaired.map(f => f.path).join(';'),
      status: 'info',
      summary: `bounded repair attempt ${attempt} on ${targetId} (${repaired.length} file(s))`,
    });
    substrate.commit(repairNode, `rha(${targetId}): bounded repair for ${bugClass} (attempt ${attempt})`);

    // --- local re-verification: the affected check only
    const local = runVitest(dir, [feature.testFile]);
    const localTaxonomy = sweepTaxonomy(dir, manifest).filter(f => f.featureId === targetId);
    const localOk = local.failed === 0 && local.passed > 0 && localTaxonomy.length === 0;
    substrate.recordNode({
      type: 'result',
      featureId: targetId,
      parentId: repairNode,
      causeId: repairNode,
      status: localOk ? 'pass' : 'fail',
      summary: `local recheck: tests ${local.passed}/${local.passed + local.failed}, detectors ${localTaxonomy.length} finding(s)`,
    });

    const postScore = local.failed + localTaxonomy.length;
    if (postScore > preScore) {
      writeFiles(dir, preRepairFiles); // ratchet: the regression never lands
      substrate.recordNode({
        type: 'decision',
        featureId: targetId,
        causeId: repairNode,
        status: 'fail',
        summary: `repair reverted (ratchet): node worsened ${preScore}→${postScore} finding(s) — previous state restored`,
      });
      emit('rha-repair', { episode, featureId: targetId, outcome: 'reverted-ratchet', attempt });
      episodes.push({
        episode, featureId: targetId, bugClass, attempt,
        outcome: 'failed', parentRecheck: 'n/a',
        summary: `repair reverted (ratchet): ${preScore}→${postScore} findings`,
      });
      continue;
    }
    for (const f of repaired) filesTouched.add(normalize(f.path));

    if (!localOk) {
      emit('rha-repair', { episode, featureId: targetId, outcome: 'still-failing', attempt });
      episodes.push({
        episode, featureId: targetId, bugClass, attempt,
        outcome: 'failed', parentRecheck: 'n/a',
        summary: `local recheck still failing (${local.failed} test failures, ${localTaxonomy.length} detector findings)`,
      });
      continue;
    }

    // --- parent re-verification: rerun every dependent's suite. A failure
    // --- here surfaces as its own error node in the next sweep — that IS the
    // --- parent-failure-triggered recursion.
    const dependents = dependentsOf(targetId, spec.features);
    let parentRecheck: RepairEpisode['parentRecheck'] = 'n/a';
    if (dependents.length) {
      const parentRun = runVitest(dir, dependents.map(d => d.testFile));
      parentRecheck = parentRun.failed === 0 ? 'pass' : 'fail';
      substrate.recordNode({
        type: 'result',
        featureId: targetId,
        causeId: repairNode,
        status: parentRecheck,
        summary: `parent recheck (${dependents.map(d => d.id).join(', ')}): ${parentRun.passed} passed, ${parentRun.failed} failed`,
      });
      for (const d of dependents) {
        substrate.recordEdge(repairNode, substrate.latest('code', d.id)?.id ?? d.id,
          `repair of ${targetId} is in the blast radius of ${d.id}`);
      }
    }

    attemptsByFeature.set(targetId, 0); // healed — reset the budget
    emit('rha-repair', { episode, featureId: targetId, outcome: 'healed', attempt, parentRecheck });
    episodes.push({
      episode, featureId: targetId, bugClass, attempt,
      outcome: 'healed', parentRecheck,
      summary: `healed ${bugClass} on ${targetId}; parent recheck ${parentRecheck}`,
    });
  }

  // --- final proof: the whole frozen corpus + detectors
  const final = fullSweep(dir, manifest);
  // J2: a frozen suite that executed 0 of its declared cases is a hard failure.
  for (const f of manifest.features) {
    const declared = manifest.cases.filter(c => c.featureId === f.id).length;
    if (declared <= 0) continue;
    const run = runVitest(dir, [f.testFile]);
    if (run.passed + run.failed === 0) {
      const msg = `${f.testFile}: declares ${declared} cases but 0 executed (collection mismatch)`;
      final.findings.push({
        bugClass: 'test-failure',
        file: f.testFile,
        line: null,
        message: msg,
        featureId: f.id,
        testName: '(collection)',
      });
      final.totalFailed = Math.max(final.totalFailed, 1);
      if (job) {
        raiseSev1(job, 'zero-executed', msg, { featureId: f.id, testFile: f.testFile, declared, collected: run.collected }, emit);
      }
    }
  }
  const done = substrate.recordNode({
    type: 'result',
    status: final.totalFailed === 0 && final.totalPassed > 0 ? 'pass' : 'fail',
    summary: `RHA complete: ${final.totalPassed} passed, ${final.totalFailed} failed across the frozen corpus`,
  });
  substrate.commit(done, `rha: complete (${final.totalPassed} passed, ${final.totalFailed} failed)`);

  // --- escalation report: everything still failing, surfaced properly
  const escalations = buildEscalations(spec, final, attemptsByFeature, substrate);
  if (escalations.length) {
    fs.writeFileSync(path.join(dir, '.supergit', 'escalation.md'), renderEscalationMd(spec, escalations));
    const escNode = substrate.recordNode({
      type: 'decision',
      status: 'fail',
      artifactPath: '.supergit/escalation.md',
      summary: `escalation report: ${escalations.length} node(s) surfaced to user`,
    });
    substrate.commit(escNode, `rha: escalation report (${escalations.length} nodes)`);
    emit('escalation', { nodes: escalations.map(e => e.featureId) });
  }

  return {
    totalTests: final.totalPassed + final.totalFailed,
    passed: final.totalPassed,
    failed: final.totalFailed,
    episodes,
    filesTouchedByRha: [...filesTouched],
    wholeArtifactRegenerated: false,
    ledgerRows: substrate.allRows().length,
    commits: substrate.commitCount,
    escalations,
  };
}

// ---------------------------------------------------------------------------
// Escalation report
// ---------------------------------------------------------------------------

export function buildEscalations(
  spec: FeatureSpec,
  final: Sweep,
  attemptsByFeature: Map<string, number>,
  substrate: Substrate,
): EscalationEntry[] {
  const entries: EscalationEntry[] = [];
  const failingSuites = new Set(
    final.findings.filter(f => f.bugClass === 'test-failure').map(f => f.featureId),
  );
  for (const [featureId, findings] of final.byFeature) {
    if (featureId === '(unbound)') continue;
    const feature = spec.features.find(f => f.id === featureId)!;
    // Non-runtime findings (tsc/taxonomy) that point INTO the frozen IR mean
    // the suite itself doesn't hold against the declared interfaces: a
    // contract problem, unhealable by bounded code repair BY DESIGN.
    const frozenFindings = findings.filter(
      f => f.bugClass !== 'test-failure' && f.file.startsWith('tests/'),
    );
    const greenDeps = feature.deps.filter(d => !failingSuites.has(d) &&
      !final.findings.some(f => f.featureId === d));
    const skew = frozenFindings.length && greenDeps.length
      ? {
          withFeatures: greenDeps,
          evidence: frozenFindings[0].message,
        }
      : null;
    const lastError = substrate.latest('error', featureId);
    const attempts = attemptsByFeature.get(featureId) ?? 0;
    entries.push({
      featureId,
      featureName: feature.name,
      bugClass: dominantClass(findings),
      attempts,
      findings,
      frozenContractFindings: frozenFindings.length > 0,
      suspectedContractSkew: skew,
      ledgerTrail: lastError ? substrate.lineage(lastError.id) : [],
      recommendation: skew
        ? `The frozen contract is internally inconsistent: ${featureId}'s suite calls an interface its green dependencies (${skew.withFeatures.join(', ')}) do not declare. Code repair cannot fix a contract, so RHA correctly stopped. Re-run the job — interface stubs and the cross-suite consistency gate now reject this class at gate 1.`
        : frozenFindings.length
          ? `Findings live in the frozen IR itself; RHA may not modify tests, so this requires human review of the contract.`
          : attempts > 0
            ? `Bounded repair did not converge within the attempt budget. Review the node's ledger trail and its frozen suite; the failure is localized to ${feature.files.join(', ')}.`
            : `Node was never repaired (a prior node exhausted the episode budget first). Re-running RHA may heal it.`,
    });
  }
  return entries;
}

export function renderEscalationMd(spec: FeatureSpec, entries: EscalationEntry[]): string {
  const lines: string[] = [
    `# Escalation report — ${spec.title}`,
    ``,
    `RHA stopped with ${entries.length} feature node(s) unhealed. Nothing was regenerated and the frozen IR was not modified; the findings below are surfaced for human judgment.`,
    ``,
  ];
  for (const e of entries) {
    lines.push(
      `## ${e.featureId} — ${e.featureName}`,
      ``,
      `- dominant bug class: \`${e.bugClass}\``,
      `- bounded repair attempts: ${e.attempts}`,
      `- frozen-contract findings: ${e.frozenContractFindings ? 'yes' : 'no'}`,
      e.suspectedContractSkew
        ? `- suspected contract skew with: ${e.suspectedContractSkew.withFeatures.join(', ')} — "${e.suspectedContractSkew.evidence}"`
        : `- suspected contract skew: none detected`,
      ``,
      `**Recommendation:** ${e.recommendation}`,
      ``,
      `### Findings`,
      ...e.findings.map(f => `- [${f.bugClass}] ${f.file}${f.line ? ':' + f.line : ''}${f.testName ? ' (' + f.testName + ')' : ''} — ${f.message.split('\n')[0].slice(0, 200)}`),
      ``,
      `### Ledger trail`,
      ...e.ledgerTrail.map(l => `- ${l}`),
      ``,
    );
  }
  return lines.join('\n');
}
