#!/usr/bin/env node
// verify_scan.mjs — extracting-fable §E support. Re-measures the verification-marker density over the corpus.
//
// What it is: the DRIFT/AUDIT tool behind `~/.claude/fable/verification/profile.md`. profile.md is a static
// authored artifact (like strategy/inferred.md); this script lets you re-measure its embedded counts on a fresh
// corpus and detect drift. It does NOT write profile.md (synthesis + epistemic tagging is authored, not generated).
//
// CRITICAL framing (matches profile.md): this skill's engine is MODEL-minus-BASELINE. These are ABSOLUTE counts in
// Fable's corpus with NO baseline corpus to contrast against — so a high count is NOT evidence of distinctiveness
// (acceptance/gates/grep-evidence are standard engineering). Counts here gate the honesty notes (B6, the §E
// absence-wall), they do not by themselves justify a §E rule. Distinctiveness needs a matched-pair contrast.
//
// Usage: node pipeline/verify_scan.mjs [--root ~/.claude/fable]   (scans <root>/corpus/**/*.md)

import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';

const expand = p => p.replace(/^~/, os.homedir());
const arg = (n, d) => { const i = process.argv.indexOf(`--${n}`); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
const ROOT = expand(arg('root', '~/.claude/fable'));
const CORPUS = path.join(ROOT, 'corpus');

if (!fs.existsSync(CORPUS)) { console.error(`no corpus at ${CORPUS} — run extract.mjs (step 3) first`); process.exit(1); }

// Walk corpus/**/*.md
function walk(dir) {
  const out = [];
  for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
    const p = path.join(dir, e.name);
    if (e.isDirectory()) out.push(...walk(p));
    else if (e.name.endsWith('.md')) out.push(p);
  }
  return out;
}
const files = walk(CORPUS);

// Present markers — measured prevalence. `distinctive` flags the ones §E actually keeps (the rest are context
// for the "generic, not distinctive" caveat). Patterns are JS-regex equivalents of the corrected ERE scan.
const PRESENT = [
  { label: 'acceptance|gate',            re: /acceptance|gate/gi,                 note: 'DOMINANT but GENERIC (no baseline → not distinctive)' },
  { label: 'evidence cmds grep|curl|ls', re: /grep |curl |ls -|cat /g,            note: 'verify-with-real-commands; generic good practice' },
  { label: 'verify',                     re: /verify/gi,                          note: 'generic' },
  { label: 'acceptance checkbox "- [ ]"',re: /- \[ \]/g,                          note: 'artifact-side of B6' },
  { label: 'repro|regression',           re: /repro|regression/gi,                note: 'regression-vs-baseline kin idiom' },
  { label: 'deletion test|delete this',  re: /deletion test|delete this/gi,       note: 'falsification idiom, arch-decision scoped' },
  { label: 'measure|measured',           re: /measure|measured/gi,                note: 'generic' },
  { label: 'assert(label, ok) idiom',    re: /assert\(/g,                         note: 'B6 idiom — runnable acceptance (.test.mjs); GROUNDED, not absent' },
  { label: 'probe',                      re: /probe/gi,                           note: 'DISTINCTIVE (E2) — probe>inspection', distinctive: true },
  { label: 'ground truth',               re: /ground truth/gi,                    note: 'DISTINCTIVE (E2) — "probe = ground truth"', distinctive: true },
  { label: 'convergence|rescan',         re: /convergence|rescan/gi,              note: 'DISTINCTIVE (E3) — fresh-agent rescan; partly context', distinctive: true },
];

// Absence wall — these MUST stay ~0 in Fable's corpus. >0 = DRIFT: the §E matched-pair wall / C10's [UNCERTAIN]
// honesty notes may need revisiting. Informational, not a hard fail. NOTE: `assert(` is deliberately NOT here —
// an earlier stale scan wrongly reported it 0; the live corpus has ~200 (B6's runnable-acceptance idiom), so it
// sits in PRESENT. [UNCERTAIN] would surface in narration/, NOT here — this scan is corpus-only and cannot adjudicate C.
const ABSENT = [
  { label: 'falsif',                     re: /falsif/gi,                          why: 'not Fable\'s authored word' },
  { label: '[UNCERTAIN',                 re: /\[UNCERTAIN/gi,                      why: 'C10 idiom — but corpus-only scan cannot adjudicate C (would be in narration/)' },
  { label: 'matched-pair/RED→GREEN',     re: /matched.pair|RED\b.{0,8}GREEN|n\s*≥\s*3/gi, why: '§Validate is the SKILL\'s own method, NOT Fable\'s — keep walled off' },
];

function tally(re) {
  let docs = 0, occ = 0;
  for (const f of files) {
    const c = fs.readFileSync(f, 'utf8');
    const m = c.match(re);
    if (m && m.length) { docs++; occ += m.length; }
  }
  return { docs, occ };
}

console.log(`# verify_scan — ${files.length} corpus docs under ${CORPUS}`);
console.log(`# ABSOLUTE counts, NO baseline → high count ≠ Fable-distinctive. Gates honesty notes only.\n`);
console.log(`PRESENT marker                       docs   occ   note`);
for (const p of PRESENT) {
  const { docs, occ } = tally(p.re);
  const tag = p.distinctive ? '★' : ' ';
  console.log(`${tag} ${p.label.padEnd(34)} ${String(docs).padStart(4)}  ${String(occ).padStart(4)}   ${p.note}`);
}
console.log(`\nABSENCE WALL (expect 0 — >0 = DRIFT, revisit the honesty note)`);
let drift = false;
for (const a of ABSENT) {
  const { occ } = tally(a.re);
  const status = occ === 0 ? 'OK  ' : 'DRIFT';
  if (occ > 0) drift = true;
  console.log(`  [${status}] ${a.label.padEnd(28)} occ=${String(occ).padStart(3)}  — ${a.why}`);
}
console.log(`\n★ = the only markers §E keeps as distinctive (E2/E3). Everything else is generic context.`);
if (drift) console.log(`\nWARN: absence wall breached — re-read profile.md honesty notes against the new corpus before regenerating.`);
