From fe2555da6370626d51efb6145d1755de75bb72ca Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 16 Sep 2026 18:35:44 +0200 Subject: [PATCH] Repair only the file states the mutation runner itself wrote A SIGKILL leaves the sidecar behind, and the next run wrote it over whatever the file then held. That is right after a kill and destructive after one: a fix written to the source between the kill and the next run was reverted to bytes from before the kill, with a line claiming a repair, and the run then reported mutation results for code nobody had written. The mutation table is the record of what a live run can leave behind: the original (which is the sidecar, and also what the no-op control writes) or the original with exactly one of that target's mutations applied. Repair now recognises those and nothing else. A file matching neither stops the run with its own digest and the sidecar's and changes nothing. Reproduced both halves against the shared target. Killed mid-mutation, then appended a line to the source: the rerun exits 1, prints both sha256s, and the hand-written line is still there afterwards. With the mutant put back and the sidecar unchanged, the rerun repairs as before and now names the mutation the dead run was inside. Co-Authored-By: Claude Fable 5.1 --- scripts/mutate-cultmesh-authority.mjs | 54 ++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/scripts/mutate-cultmesh-authority.mjs b/scripts/mutate-cultmesh-authority.mjs index 1e3e1b32..8aec4eee 100644 --- a/scripts/mutate-cultmesh-authority.mjs +++ b/scripts/mutate-cultmesh-authority.mjs @@ -282,6 +282,54 @@ function occurrences(text, needle) { return text.split(needle).length - 1; } +/** + * Repairs a file left mutated by a run that died before its `finally`, and only + * that. + * + * A SIGKILL leaves the sidecar behind. The repair used to write the sidecar over + * whatever the file now held, which is right after a kill and destructive after + * one: a fix made to the source between the kill and the next run was silently + * reverted to bytes from before the kill, and the run then reported on code + * nobody had written. + * + * So repair only what this script can prove it wrote. The mutation table is the + * record: every file state a live run can leave behind is either the original + * (the sidecar, also what the no-op control writes) or the original with exactly + * one of that target's mutations applied. Anything else — a hand edit, a + * half-written file, a different branch — is somebody else's, and the run stops + * with both digests rather than guessing. + */ +function repair(name, target, own, sidecar) { + const original = readFileSync(sidecar); + const originalText = original.toString("utf8"); + const eol = originalText.includes("\r\n") ? "\r\n" : "\n"; + const withEol = text => text.replace(/\r?\n/g, eol); + const current = readFileSync(target.file); + const currentDigest = digest(current); + const originalDigest = digest(original); + + if (currentDigest === originalDigest || currentDigest === digest(Buffer.from(originalText, "utf8"))) { + unlinkSync(sidecar); + console.log(`${target.file} is already the original; dropped the stale ${sidecar}`); + return; + } + + const currentText = current.toString("utf8"); + const mutant = own.find(mutation => + originalText.replace(withEol(mutation.old), withEol(mutation.new)) === currentText); + if (!mutant) { + throw new Error( + `${target.file} is neither the original nor any mutation of target '${name}', so a previous run did not write it.\n` + + ` ${target.file} sha256=${currentDigest}\n` + + ` ${sidecar} sha256=${originalDigest}\n` + + " Refusing to overwrite it. Reconcile the file by hand, then delete the sidecar.", + ); + } + writeFileSync(target.file, original); + unlinkSync(sidecar); + console.log(`repaired ${target.file} from ${sidecar}: a previous run stopped inside '${mutant.rule}'`); +} + const results = []; let failed = false; @@ -289,11 +337,7 @@ for (const [name, target] of Object.entries(targets)) { const own = mutations.filter(mutation => (mutation.target ?? "shared") === name); if (own.length === 0) throw new Error(`target '${name}' has no mutations`); const sidecar = `${target.file}.mutation-original`; - if (existsSync(sidecar)) { - writeFileSync(target.file, readFileSync(sidecar)); - unlinkSync(sidecar); - console.log(`repaired ${target.file} from ${sidecar}: a previous run stopped before restoring it`); - } + if (existsSync(sidecar)) repair(name, target, own, sidecar); const original = readFileSync(target.file); const originalText = original.toString("utf8");