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 <!-- cast: --> 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 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 09:45:38 +01:00
co-authored by Claude Opus 5
parent 996bd248d3
commit 60995b28c2
2 changed files with 67 additions and 6 deletions
+18
View File
@@ -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 `<!-- cast: ... -->` 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.
+49 -6
View File
@@ -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(/<!--\s*cast:\s*([^>]*?)\s*-->/i);
if (!m) {
console.error(`first-blood: ${path.relative(ROOT, SCENARIO)} declares no cast. Add the `
+ `<!-- cast: ... --> 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);