feat(observer): one-shot chain_ref retrofill script (idempotent, atomic)
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
#!/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();
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { retrofillLine } from './observer-retrofill-chain-ref.mjs';
|
||||
import { loadChainMap } from './observer-chain-detector.mjs';
|
||||
|
||||
const map = loadChainMap();
|
||||
|
||||
describe('retrofillLine', () => {
|
||||
it('adds chain_ref to a v2 episode with a known node', () => {
|
||||
const ep = { schema_version: 2, primary_rationale: { node_chosen: 'billing-audit' } };
|
||||
const out = retrofillLine(ep, map);
|
||||
expect(out.primary_rationale.chain_ref).toEqual(['L13']);
|
||||
});
|
||||
|
||||
it('sets chain_ref null for a direct v2 episode', () => {
|
||||
const ep = { schema_version: 2, primary_rationale: { node_chosen: 'direct' } };
|
||||
expect(retrofillLine(ep, map).primary_rationale.chain_ref).toBeNull();
|
||||
});
|
||||
|
||||
it('is idempotent — does not overwrite existing chain_ref', () => {
|
||||
const ep = { schema_version: 2, primary_rationale: { node_chosen: 'direct', chain_ref: ['L1'] } };
|
||||
expect(retrofillLine(ep, map).primary_rationale.chain_ref).toEqual(['L1']);
|
||||
});
|
||||
|
||||
it('skips v1 episodes (no schema_version 2)', () => {
|
||||
const ep = { foo: 'bar' };
|
||||
expect(retrofillLine(ep, map)).toEqual({ foo: 'bar' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user