#!/usr/bin/env node /** * One-shot retrofill: add primary_rationale.chain_ref to existing v2 episodes * in docs/observer/episodes-*.jsonl. Idempotent (skips lines that already have * chain_ref), atomic per file (tmp + rename). Pure fs, no LLM. * * Usage: node tools/observer-retrofill-chain-ref.mjs [--dry-run] */ import { readFileSync, writeFileSync, renameSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { loadChainMap, chainsFor } from './observer-chain-detector.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const OBS_DIR = join(__dirname, '..', 'docs', 'observer'); /** Add chain_ref to a single parsed episode object (pure). Idempotent. */ export function retrofillLine(ep, map) { if (!ep || ep.schema_version !== 2 || !ep.primary_rationale) return ep; if ('chain_ref' in ep.primary_rationale) return ep; // idempotent ep.primary_rationale.chain_ref = chainsFor(ep.primary_rationale.node_chosen, map); return ep; } /** Process one JSONL file atomically (tmp + rename). Returns {changed, total}. */ export function retrofillFile(path, map, { dryRun = false } = {}) { const lines = readFileSync(path, 'utf8').split(/\r?\n/); let changed = 0; let total = 0; const out = lines.map((line) => { if (!line.trim()) return line; total++; const ep = JSON.parse(line); const before = ep.primary_rationale && 'chain_ref' in ep.primary_rationale; const next = retrofillLine(ep, map); const after = next.primary_rationale && 'chain_ref' in next.primary_rationale; if (!before && after) changed++; return JSON.stringify(next); }); if (!dryRun && changed > 0) { const tmp = `${path}.tmp`; writeFileSync(tmp, out.join('\n'), 'utf8'); renameSync(tmp, path); } return { changed, total }; } function main() { const dryRun = process.argv.includes('--dry-run'); const map = loadChainMap(); const files = readdirSync(OBS_DIR).filter((f) => /^episodes-\d{4}-\d{2}\.jsonl$/.test(f)); for (const f of files) { const { changed, total } = retrofillFile(join(OBS_DIR, f), map, { dryRun }); console.log(`${dryRun ? '[dry-run] ' : ''}${f}: ${changed}/${total} lines get chain_ref`); } } if (process.argv[1]?.endsWith('observer-retrofill-chain-ref.mjs')) main();