ms-ai-architect/scripts/kb-update/apply-verified-stamp.mjs

121 lines
6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// apply-verified-stamp.mjs — R7R10 surgical born-verified stamp driver.
//
// Applies the judge-pass ledger's `pass` verdicts (scripts/kb-eval/data/judge-pass-manifest.json,
// R6 Step 4) to disk: for every record with per_file_verdict='pass', insert/update ONLY the
// **Verified:** and **Verified by:** header lines via transform.insertVerifiedFields — the body
// below the header is byte-identical (NFR non-destruktivt; composeKbFile would rebuild the file
// and diff the body, which is why this driver exists — R6 plan Step 2). `flagged` records are
// never touched. An unstampable file (stamp would land past the 500-byte window —
// insertVerifiedFields throws) is collected as an error and reported; R7 flips that record to
// flagged «header-slanking kreves», never a forced stamp. Other files still stamp (exit 1).
//
// INVARIANT (asserted per file BEFORE any write; breach aborts that file, never writes):
// - body (from first `## `/`---`) byte-identical
// - header minus Verified/Verified by-lines byte-identical (only the stamp lines differ)
// - parseVerifiedHeader/parseVerifiedByHeader read back exactly the stamped values
// Idempotent: a re-run finds the stamp in place and is a no-op. Crash-safe: atomicWriteSync
// (tmp+rename) — a reader sees the old file or the new one, never a partial.
//
// Usage: node scripts/kb-update/apply-verified-stamp.mjs [--manifest <path>] [--root <dir>] [--write]
// (default: dry-run — prints the plan, writes nothing)
// Exit codes: 0 ok · 1 one or more files failed (unstampable/missing/invariant) · 2 usage
import { readFileSync, existsSync, realpathSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { insertVerifiedFields } from './lib/transform.mjs';
import { parseVerifiedHeader, parseVerifiedByHeader } from './lib/kb-headers.mjs';
import { atomicWriteSync } from './lib/atomic-write.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..', '..');
const DEFAULT_MANIFEST = join(__dirname, '..', 'kb-eval', 'data', 'judge-pass-manifest.json');
/**
* Pure stamp plan for one file: returns { content, changed }. `changed:false` means the exact
* stamp is already in place (idempotent re-run). Throws when the stamp cannot land inside the
* 500-byte header window (caller records the failure — the R7 «header-slanking kreves» path).
*/
export function planVerifiedStamp(content, { verified, verified_by }) {
const stamped = insertVerifiedFields(content, { verified, verified_by });
return { content: stamped, changed: stamped !== String(content) };
}
// Header/body split mirroring strip-stale-verified-pipe.mjs / transform.mjs headerEndIndex.
function splitHeaderBody(content) {
const lines = String(content).split('\n');
let i = 0;
for (; i < lines.length; i++) {
if (/^##\s/.test(lines[i]) || /^---\s*$/.test(lines[i])) break;
}
return { header: lines.slice(0, i).join('\n'), body: lines.slice(i).join('\n') };
}
const RE_STAMP_LINE = /^\*\*Verified(?: by)?:\*\*/i;
// The hard per-file invariant: only the stamp lines may differ; the body never.
function assertInvariant(rel, oldC, newC, { verified, verified_by }) {
const o = splitHeaderBody(oldC);
const n = splitHeaderBody(newC);
if (o.body !== n.body) throw new Error(`${rel}: body changed — refusing to write`);
const strip = (h) => h.split('\n').filter((l) => !RE_STAMP_LINE.test(l)).join('\n');
if (strip(o.header) !== strip(n.header)) {
throw new Error(`${rel}: header lines other than Verified/Verified by changed — refusing to write`);
}
if (parseVerifiedHeader(newC) !== verified || parseVerifiedByHeader(newC) !== verified_by) {
throw new Error(`${rel}: stamped values do not read back — refusing to write`);
}
}
function usageExit(msg) {
process.stderr.write(`${msg}\nusage: apply-verified-stamp.mjs [--manifest <path>] [--root <dir>] [--write]\n`);
process.exit(2);
}
function main(argv) {
const args = argv.slice(2);
let manifestPath = DEFAULT_MANIFEST;
let root = PLUGIN_ROOT;
let write = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--manifest') { if (!args[i + 1]) usageExit('--manifest requires a path'); manifestPath = args[++i]; }
else if (args[i] === '--root') { if (!args[i + 1]) usageExit('--root requires a dir'); root = args[++i]; }
else if (args[i] === '--write') write = true;
else usageExit(`unknown flag: ${args[i]}`);
}
if (!existsSync(manifestPath)) usageExit(`manifest not found: ${manifestPath}`);
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
const passRecs = (manifest.files ?? []).filter((r) => r?.per_file_verdict === 'pass');
let stamped = 0, noop = 0;
const failed = [];
for (const rec of passRecs) {
const abs = join(root, rec.file);
try {
if (!rec.verified || !rec.verified_by) throw new Error('pass record missing verified/verified_by');
if (!existsSync(abs)) throw new Error('file not found');
const oldC = readFileSync(abs, 'utf8');
const { content: newC, changed } = planVerifiedStamp(oldC, { verified: rec.verified, verified_by: rec.verified_by });
if (!changed) { noop++; continue; }
assertInvariant(rec.file, oldC, newC, { verified: rec.verified, verified_by: rec.verified_by });
if (write) atomicWriteSync(abs, newC);
stamped++;
console.log(`${write ? 'stamped' : 'would stamp'} ${rec.file}`);
} catch (e) {
failed.push([rec.file, e.message]);
console.error(`FAILED ${rec.file}${e.message}`);
}
}
console.log(`\n${write ? '' : '(dry-run) '}pass records: ${passRecs.length} · stamped: ${write ? stamped : 0}${write ? '' : ` (would stamp: ${stamped})`} · already stamped: ${noop} · failed: ${failed.length}`);
if (failed.length > 0) process.exit(1);
}
const isMain = (() => {
try {
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
} catch {
return false;
}
})();
if (isMain) main(process.argv);