From 60995b28c27a46cd9f282d112e7d5a3e28e9a890 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Sun, 13 Sep 2026 09:45:38 +0100 Subject: [PATCH] R-267 addendum: read the cast, do not copy it I told the scenario session that this guard turns a silent re-cast into a build failure, and it accepted the coupling on that basis. The cast was hardcoded, so a re-cast would have left it measuring the old six and reporting success. Reads the marker check-rollable established, drops the two the scaling note drops by name rather than by position, and refuses to run if the marker is gone. Verified by re-casting CLEAN GROUND in a throwaway worktree and watching the guard name the change. Co-Authored-By: Claude Opus 5 --- docs/REVIEW_LOG.md | 18 ++++++++++++++ tools/first-blood.mjs | 55 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index d2f6057..5b2c7fd 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -5898,3 +5898,21 @@ not wrong enough to mislead anybody and it was wrong enough to print. at twelve, which is exactly the hardcoded-list failure its own comment warns about two lines above, committed by the file that exists to prevent it. Extended to sixteen, which defers the same defect rather than curing it. + +**Addendum — the coupling did not actually bite.** I told the scenario's session that this +guard converts a silent re-cast into a build failure. It believed me and accepted the +coupling on that basis. It was not true: the cast was written out by hand here, so a +re-cast would have left this measuring the old six and reporting success — the project's +own signature defect, committed inside the guard written to prevent a weaker version of +it, three hours after writing "a claim nothing checks" about something else. + +It now reads the `` marker check-rollable already established, derives +the cut by dropping the two the scaling note drops — by name, since "the first four" would +survive a reordering and quietly measure different people — and refuses to run at all if +the marker is missing. Proved by re-casting CLEAN GROUND in a throwaway worktree and +watching it fail: *"cut: the scenario now casts different people — no longer braithwaite, +now agyeman ... re-record with --update, do not revert the cast."* + +**Which is the third time in this sequence that a thing was true only because nobody had +tested it.** The generator comment, the costed restraint, and now the coupling — each +stated confidently, each wrong, each found by somebody trying it rather than reading it. diff --git a/tools/first-blood.mjs b/tools/first-blood.mjs index f8f7e13..18cfaac 100644 --- a/tools/first-blood.mjs +++ b/tools/first-blood.mjs @@ -29,7 +29,7 @@ * cut rather than of the creature. */ import { readFile, writeFile } from "node:fs/promises"; -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { NPCS } from "./content.mjs"; import { ROSTER } from "./roster.mjs"; @@ -47,9 +47,43 @@ const RUNS = Number(opt("--runs", 2000)); const SEEDS = [11, 4242, 90210]; /* CLEAN GROUND's own cast, not the frozen party of the other two guards: this measures a - scenario's encounter, and the whole finding is about what four of these people can do. */ -const CUT = ["ashcroft", "bhattacharya", "renshaw", "braithwaite"]; -const LINE = [...CUT, "pollard", "okonkwo"]; + scenario's encounter, and the whole finding is about what four of these people can do. + + Read from the document rather than copied out of it. A cast written here by hand would + go on measuring the old six after a re-cast and report success, which is the defect this + project keeps finding in itself and the one thing a guard must not do. check-rollable + established the marker; this reads the same one. */ +const SCENARIO = path.join(ROOT, "docs", "scenarios", "CLEAN_GROUND.md"); +/* The scaling note drops Pollard at five and Okonkwo as well at four, so the cut is the + declared cast less those two. Named rather than sliced: "the first four" would survive + a reordering of the cast list and quietly measure different people. */ +const DROPPED_AT_FOUR = ["pollard", "okonkwo"]; + +function declaredCast() { + if (!existsSync(SCENARIO)) { + console.error(`first-blood: ${path.relative(ROOT, SCENARIO)} is missing, and the cast is read ` + + `from it. Nothing here can be measured against a scenario that is not there.`); + process.exit(1); + } + const raw = readFileSync(SCENARIO, "utf8"); + const m = raw.match(//i); + if (!m) { + console.error(`first-blood: ${path.relative(ROOT, SCENARIO)} declares no cast. Add the ` + + ` marker check-rollable already reads, or this guard is measuring ` + + `a fight the scenario no longer casts.`); + process.exit(1); + } + return m[1].split(",").map(x => x.trim().replace(/^pc_/, "")).filter(Boolean); +} + +const LINE = declaredCast(); +const CUT = LINE.filter(k => !DROPPED_AT_FOUR.includes(k)); +if (CUT.length !== 4 || LINE.length !== 6) { + console.error(`first-blood: CLEAN GROUND now casts ${LINE.length} and its four-player cut ` + + `works out at ${CUT.length}. The recorded fights are six against six and four against ` + + `three; re-read the scaling note and re-record with --update rather than reverting.`); + process.exit(1); +} const CONFIGS = [ { id: "cut", label: "four of the roster vs three of the column", party: CUT, creature: "quiet_neighbours_npc", count: 3 }, { id: "column", label: "six of the roster vs six of the column", party: LINE, creature: "quiet_neighbours_npc", count: 6 } @@ -188,8 +222,17 @@ const now = {}, problems = []; for (const cfg of CONFIGS) { const was = base.configs[cfg.id]; if (!was) { problems.push(` ${cfg.id}: not in the baseline — re-record with --update`); continue; } - if (was.party.join() !== cfg.party.join() || was.creature !== cfg.creature || was.count !== cfg.count) { - problems.push(` ${cfg.id}: recorded against a different fight (${was.party.length} vs ${was.count} x ${was.creature})`); + if (was.party.join() !== cfg.party.join()) { + const gone = was.party.filter(k => !cfg.party.includes(k)); + const added = cfg.party.filter(k => !was.party.includes(k)); + problems.push(` ${cfg.id}: the scenario now casts different people — ` + + (gone.length ? `no longer ${gone.join(", ")}` : "the same six, reordered") + + (added.length ? `, now ${added.join(", ")}` : "") + + `. The recorded swing describes the old cast; re-record with --update, do not revert the cast.`); + continue; + } + if (was.creature !== cfg.creature || was.count !== cfg.count) { + problems.push(` ${cfg.id}: recorded against ${was.count} x ${was.creature}, now ${cfg.count} x ${cfg.creature}`); continue; } const m = now[cfg.id] = measure(cfg);