#!/usr/bin/env node // apply-o2-ratified.mjs — R11 §9.4. Applies the O2 subtractions the operator // ratified 2026-08-03, and only those. // // Ratified: idx 17, 19, 33 as attested; idx 14 as the REDUCED subtraction // (`/ SharePoint` only). Deliberately NOT ratified and therefore absent from // RATIFIED: idx 26 (renumbering artifact unresolved), idx 27 (cond 3 still // `human_must_confirm`), idx 36 (needs the out-of-envelope companion edit at // line 310, now owned by gap G7), idx 18 (no reduction exists — also G7). // // Every string comes from the tracked evidence in data/r11-o2-returns/, never // from transcription. The one amendment (idx 14) is expressed as a derivation // over the attested verbatim and asserts its own effect, so a drifted record // aborts rather than silently writing something else. // // Anchoring is on `file_text_verbatim`, NEVER on a line number: `line` differs // from `real_line` in 9 of 17 records, and idx 17 shifts idx 19's lines in the // file they share. The verbatim must occur EXACTLY once or the run aborts. // // Recovery contract: writes are crash-safe (atomicWriteSync tmp+rename — a reader // sees the old file or the new one, never a partial). An interrupted run is // recovered by re-running: an already-applied edit no longer finds its verbatim, // which aborts the run, writing nothing, rather than corrupting the file. // // Usage: node scripts/kb-eval/apply-o2-ratified.mjs [--dry] import { readFileSync, readdirSync, realpathSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { isDeletionOnly, novelWordForms } from './lib/o2-return-check.mjs'; import { atomicWriteSync } from '../kb-update/lib/atomic-write.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PLUGIN_ROOT = join(__dirname, '..', '..'); const RETURNS = join(PLUGIN_ROOT, 'scripts/kb-eval/data/r11-o2-returns'); /** * The ratified reduction for idx 14: delete only the `/ SharePoint` alternative. * `Automatically` MUST survive — line 566 of the same file restates automaticity * in Norwegian, so deleting it would leave a remainder the file contradicts. * @param {string} verbatim * @returns {string} */ export function reduceSharePointOnly(verbatim) { const out = verbatim.replace('Dataverse / SharePoint', 'Dataverse'); if (out === verbatim) throw new Error('idx 14 reduction is a no-op — record drifted'); if (!out.includes('Automatically add')) throw new Error('idx 14: `Automatically` must survive'); return out; } // Frozen manifest — the operator's ratification, 2026-08-03. `amend: null` means // apply the attested `proposed_remainder` byte-for-byte. export const RATIFIED = [ { idx: 17, amend: null }, { idx: 19, amend: null }, { idx: 33, amend: null }, { idx: 14, amend: reduceSharePointOnly }, ]; /** * The remainder actually written for a record: attested, or the ratified amendment. * @param {object} row * @param {{amend: ((v: string) => string) | null}} entry * @returns {string} */ export function amendedRemainder(row, entry) { return entry.amend ? entry.amend(row.file_text_verbatim) : row.proposed_remainder; } /** * Replace the anchored block with its remainder. Pure — no I/O. Throws on any * condition that would make the write unsafe rather than writing something else. * @param {string} content * @param {string} verbatim * @param {string} remainder * @returns {string} */ export function applyEdit(content, verbatim, remainder) { const occurrences = content.split(verbatim).length - 1; if (occurrences !== 1) { throw new Error(`ABORT — anchor occurs ${occurrences} times, expected exactly 1`); } if (!isDeletionOnly(verbatim, remainder)) { throw new Error('ABORT — remainder is not deletion-only w.r.t. the anchor'); } const novel = novelWordForms(verbatim, remainder); if (novel.length) { throw new Error(`ABORT — remainder introduces novel word form(s): ${novel.join(', ')}`); } return content.replace(verbatim, remainder); } function loadRows() { return readdirSync(RETURNS).filter((f) => f.endsWith('.json')).sort() .flatMap((f) => JSON.parse(readFileSync(join(RETURNS, f), 'utf8'))); } export function run({ dry = false } = {}) { const byIdx = new Map(loadRows().map((r) => [r.idx, r])); // Group by file so two edits sharing a file compose in memory and write once. const perFile = new Map(); for (const entry of RATIFIED) { const row = byIdx.get(entry.idx); if (!row) throw new Error(`ABORT — no return record for idx ${entry.idx}`); if (row.verdict !== 'O2_CANDIDATE') { throw new Error(`ABORT — idx ${entry.idx} is ${row.verdict}, not an O2 candidate`); } if (!perFile.has(row.file)) perFile.set(row.file, []); perFile.get(row.file).push({ entry, row }); } const planned = []; for (const [rel, edits] of perFile) { const abs = join(PLUGIN_ROOT, rel); const before = readFileSync(abs, 'utf8'); let out = before; for (const { entry, row } of edits) { out = applyEdit(out, row.file_text_verbatim, amendedRemainder(row, entry)); } // Post-condition: every anchor is gone, and the file actually changed. for (const { row } of edits) { if (out.includes(row.file_text_verbatim)) { throw new Error(`ABORT — idx ${row.idx} anchor still present after edit`); } } if (out === before) throw new Error(`ABORT — ${rel} unchanged`); planned.push({ rel, abs, out, idxs: edits.map((e) => e.entry.idx) }); } console.log(`Ratified edits: ${RATIFIED.length} across ${planned.length} files`); for (const p of planned) console.log(` ~ ${p.rel} (idx ${p.idxs.join(', ')})`); if (dry) { console.log('\n(dry run — no writes)'); return planned; } for (const p of planned) atomicWriteSync(p.abs, p.out); console.log(`\nWrote ${planned.length} files.`); return planned; } const isMain = (() => { try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })(); if (isMain) run({ dry: process.argv.includes('--dry') });