R-289: test the cast reader on strings, and fix the spelling that found
Both earlier cast defects were shown by editing CLEAN_GROUND.md in the shared tree, which is how this session came within a git checkout of c0's uncommitted work and is also the weaker test. Seven cases now live in check-behaviour as strings. Writing them found a live one: "<!-- cast : ... -->", one space before the colon, matched nothing -- not an empty declaration R-288 would refuse but no declaration at all, so check-rollable widened to the whole duty roster and printed its usual OK line. check-cited's three spellings again. The reader now takes cast\s*:. castLikeIn adds the asymmetric half: anything comment-shaped containing cast\w* the reader did not consume is named by check-rollable, so a spelling nobody anticipated fails the build instead of silently declaring nobody. Looser than the reader on purpose -- a false alarm costs a reword, the opposite error costs a guard that checks the wrong six people and says OK. Tests then mutation-checked for being load-bearing, each mutation asserted to have applied after a first pass where three silently did not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3d222cacf
commit
2cf9721fa3
@@ -6765,3 +6765,42 @@ strength of an agent the scenario does not cast.
|
||||
|
||||
Replacing CLEAN GROUND's declaration with `<!-- cast: -->` passes before this change and
|
||||
fails after it, naming the line. A scenario with no marker still falls back, unchanged.
|
||||
|
||||
## R-289 — the cast reader, tested on strings, and the spelling that testing found
|
||||
|
||||
c0's point after R-286: both cast defects were demonstrated by editing CLEAN_GROUND.md in the
|
||||
shared tree to watch what the reader did with it. That is how one session came within a
|
||||
`git checkout` of another's uncommitted work — and it is also the weaker test. It holds one
|
||||
shape at a time, it leaves nothing behind, and it cannot be run again next month.
|
||||
|
||||
Seven cases now live in `check-behaviour`, as strings: the real document's shape; the mention
|
||||
moved above the declaration (the R-286 bug, which must still read the six); two real
|
||||
declarations both seen, with line numbers surviving the code-blanking; a fenced declaration
|
||||
ignored; present-and-empty told apart from absent (R-288); spelling; and the odd-marker
|
||||
counter.
|
||||
|
||||
**Writing them found a live one.** `<!-- cast : ... -->` — one space before the colon —
|
||||
matched nothing. Not an empty declaration that R-288 would refuse: *no declaration at all*, so
|
||||
check-rollable fell back to the whole duty roster and printed its usual OK line having checked
|
||||
every skill in the document against people the scenario does not cast. This is check-cited's
|
||||
defect exactly, where `Cite:`, `CITE:` and `cite :` all failed open while the guard reported a
|
||||
count identical to a clean tree. The reader now takes `cast\s*:`, case-insensitively.
|
||||
|
||||
**And the asymmetric half, because widening only fixes the spellings we thought of.**
|
||||
`castLikeIn` recognises anything comment-shaped containing `cast\w*` that the strict reader did
|
||||
not consume, and check-rollable names it: `cast =`, `casting:`, `the cast is` all fail the
|
||||
build rather than silently declaring nobody. It is deliberately looser than the reader — an
|
||||
ordinary comment near the casting section can trip it, and the message says prose belongs in
|
||||
backticks, where nothing here looks at it. A false alarm costs a reword; the opposite error
|
||||
costs a guard that checks the wrong six people and says OK.
|
||||
|
||||
**The tests were then checked for being load-bearing**, by mutating the reader in a worktree
|
||||
and reading which of them went red — each mutation asserted to have applied, after a first
|
||||
pass where three silently did not and the green that followed proved nothing:
|
||||
|
||||
| mutation | red |
|
||||
|---|---|
|
||||
| `cast\s*:` back to `cast:` | spacing |
|
||||
| code-span blanking removed | four, including both R-286 cases |
|
||||
| marker list truncated to the first hit | two declarations both seen |
|
||||
| odd-marker counter disabled | cast-shaped marker named |
|
||||
|
||||
@@ -31,6 +31,7 @@ import { ROLES, TRADES, INDUCTION, TIERS, TRADE_BANDS, CHARACTERISTIC_DICE }
|
||||
from "../postings.mjs";
|
||||
import { STARTER_AUTHORITY, STARTER_GROUPS, STARTER_CASE, STARTER_TEAM }
|
||||
from "./scenario-starter.mjs";
|
||||
import { castMarkersIn, castLikeIn } from "./declared-cast.mjs";
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
// Async tests are awaited in order rather than fired off — a failure that lands after
|
||||
@@ -756,6 +757,68 @@ test("R-255 the supporter's hide reaches each location at the right value, flyin
|
||||
`grounded hide is ${Math.floor(hide / 2)}, which an ordinary round still cannot beat`);
|
||||
});
|
||||
|
||||
/* ── R-286 / R-288 / R-289: who a scenario casts ─────────────────────────────────────
|
||||
ON STRINGS, NOT ON THE SCENARIO. Both of these defects were first shown by editing
|
||||
CLEAN_GROUND.md in the shared tree to see what the reader did with it, which is how one
|
||||
session came within a `git checkout` of another's uncommitted work, and it is also a worse
|
||||
test: it can only hold one shape at a time and it leaves nothing behind. c0 made the point.
|
||||
The shapes below are the whole finding, they run on every build, and they cost nothing. */
|
||||
|
||||
const SIX = "pc_ashcroft, pc_bhattacharya, pc_renshaw, pc_braithwaite, pc_pollard, pc_okonkwo";
|
||||
const keysOf = t => castMarkersIn(t).map(m => m.keys);
|
||||
|
||||
test("a declaration is read, and a mention of one in backticks is not", () => {
|
||||
// The real document's shape: the marker, then the warning that quotes it. R-286.
|
||||
const doc = `## Casting\n\n<!-- cast: ${SIX} -->\n\nThe \`<!-- cast: -->\` comment above is read by two guards.\n`;
|
||||
assert.deepEqual(keysOf(doc), [SIX]);
|
||||
});
|
||||
|
||||
test("the mention above the declaration still reads the declaration", () => {
|
||||
/* THE BUG ITSELF. Before R-286 this returned the empty capture, check-rollable fell back to
|
||||
the whole duty roster and printed the same OK line. The arrangement was only ever correct
|
||||
because the real marker happened to sort first. */
|
||||
const doc = `The \`<!-- cast: -->\` comment below is read by two guards.\n\n<!-- cast: ${SIX} -->\n`;
|
||||
assert.deepEqual(keysOf(doc), [SIX]);
|
||||
});
|
||||
|
||||
test("two real declarations are both seen, so neither can win by position", () => {
|
||||
const doc = `<!-- cast: pc_ashcroft -->\nfiller\n<!-- cast: ${SIX} -->\n`;
|
||||
const found = castMarkersIn(doc);
|
||||
assert.equal(found.length, 2);
|
||||
assert.deepEqual(found.map(m => m.line), [1, 3], "line numbers must survive the code blanking");
|
||||
});
|
||||
|
||||
test("a declaration inside a fenced block is documentation, not a cast", () => {
|
||||
assert.deepEqual(keysOf("```md\n<!-- cast: " + SIX + " -->\n```\n"), []);
|
||||
});
|
||||
|
||||
test("a declaration that names nobody is read as present and empty", () => {
|
||||
// R-288 needs to tell this from "no marker at all", which falls back on purpose.
|
||||
assert.deepEqual(keysOf("<!-- cast: -->"), [""]);
|
||||
assert.deepEqual(keysOf("no marker here"), []);
|
||||
});
|
||||
|
||||
test("spacing around the colon does not hide a declaration", () => {
|
||||
/* R-289, found by writing these tests. `<!-- cast : ... -->` matched nothing, so the guard
|
||||
saw no declaration, widened to the full roster and went green — and R-288 could not catch
|
||||
it because nothing was present to be empty. check-cited's three spellings, again. */
|
||||
for (const m of [`<!-- cast : ${SIX} -->`, `<!-- Cast: ${SIX} -->`, `<!--cast:${SIX}-->`,
|
||||
`<!-- CAST : ${SIX} -->`]) {
|
||||
assert.deepEqual(keysOf(m), [SIX], `this spelling reads nothing: ${m}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a cast-shaped marker the reader cannot parse is named, not skipped", () => {
|
||||
/* The asymmetric half. Widening the reader fixes the spellings we thought of; this is what
|
||||
names the one we did not, instead of silently declaring nobody. */
|
||||
for (const m of [`<!-- cast = ${SIX} -->`, `<!-- casting: ${SIX} -->`, `<!-- the cast is ${SIX} -->`]) {
|
||||
assert.equal(castMarkersIn(m).length, 0);
|
||||
assert.equal(castLikeIn(m).length, 1, `not flagged as cast-shaped: ${m}`);
|
||||
}
|
||||
assert.deepEqual(castLikeIn(`<!-- cast: ${SIX} -->`), [], "a readable marker is not an oddity");
|
||||
assert.deepEqual(castLikeIn("`<!-- cast = x -->`"), [], "prose in backticks is not an oddity");
|
||||
});
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
await runAll();
|
||||
|
||||
@@ -39,7 +39,7 @@ import { SKILL_CATALOGUE } from "./content.mjs";
|
||||
import { ROSTER } from "./roster.mjs";
|
||||
import { expandFromRegister } from "./expand-spec.mjs";
|
||||
import { scenarioFiles, scenarioText, tagsIn, skillsIn } from "./check-scenarios.mjs";
|
||||
import { castMarkersIn } from "./declared-cast.mjs";
|
||||
import { castMarkersIn, castLikeIn } from "./declared-cast.mjs";
|
||||
|
||||
/**
|
||||
* The lowest chance that still counts as a route, in percent.
|
||||
@@ -98,6 +98,13 @@ for (const [label, path] of scenarioFiles()) {
|
||||
measures the fight cannot disagree about who is cast — and so a marker mentioned in
|
||||
prose is not read as a second declaration. CLEAN_GROUND.md carries one of each. */
|
||||
const markers = castMarkersIn(raw);
|
||||
for (const odd of castLikeIn(raw)) {
|
||||
problems.push(`${label}: line ${odd.line} looks like a cast declaration and is not one this `
|
||||
+ `guard can read — \`${odd.raw}\`. The shape is \`<!-- cast: key, key -->\` (case and `
|
||||
+ `spaces around the colon are fine). Unread, it declares nothing and every skill in the `
|
||||
+ `document is checked against the whole duty roster instead. If it is prose, put it in `
|
||||
+ `backticks or a fenced block, where nothing here looks at it.`);
|
||||
}
|
||||
if (markers.length > 1) {
|
||||
problems.push(`${label}: declares a cast ${markers.length} times, at lines `
|
||||
+ `${markers.map(m => m.line).join(", ")}. This guard read the first and held every `
|
||||
|
||||
+31
-3
@@ -42,10 +42,38 @@ const DROPPED_AT_FOUR = ["pollard", "okonkwo"];
|
||||
still point at the source), and more than one surviving marker is fatal rather than
|
||||
first-wins. */
|
||||
const CODE = /```[\s\S]*?```|`[^`\n]*`/g;
|
||||
const scannable = text => text.replace(CODE, m => " ".repeat(m.length));
|
||||
const lineAt = (text, i) => text.slice(0, i).split("\n").length;
|
||||
|
||||
/* `cast\s*:` rather than `cast:`. Tested on strings rather than by editing the scenario, and
|
||||
the spacing is why: `<!-- cast : ... -->` was invisible — no marker, so check-rollable fell
|
||||
back to the whole duty roster and went green, and R-288 could not catch it because there was
|
||||
nothing present to be empty. Putting a space before a colon is not an exotic mistake. It is
|
||||
the check-cited defect exactly, where `Cite:`, `CITE:` and `cite :` all failed open while the
|
||||
guard reported a count identical to a clean tree. */
|
||||
export function castMarkersIn(text) {
|
||||
const scannable = text.replace(CODE, m => " ".repeat(m.length));
|
||||
return [...scannable.matchAll(/<!--\s*cast:\s*([^>]*?)\s*-->/gi)]
|
||||
.map(m => ({ keys: m[1], line: text.slice(0, m.index).split("\n").length }));
|
||||
return [...scannable(text).matchAll(/<!--\s*cast\s*:\s*([^>]*?)\s*-->/gi)]
|
||||
.map(m => ({ keys: m[1], line: lineAt(text, m.index) }));
|
||||
}
|
||||
|
||||
/* ANYTHING a person might have meant as a cast declaration, however spelled. Deliberately
|
||||
ASYMMETRIC with the reader above: the reader accepts what the format specifies, this
|
||||
recognises anything cast-shaped, and the difference is reported rather than skipped. Widening
|
||||
`castMarkersIn` fixes the spelling we thought of; this is what names the one we did not. A
|
||||
marker nothing reads is worse than no marker, because whoever wrote it believes the cast is
|
||||
declared.
|
||||
|
||||
`cast\w*` catches casting/caster too, so an ordinary comment near the casting section can trip
|
||||
it. That is the intended direction: a false alarm costs a reword, and the message says a
|
||||
comment meant as prose belongs in backticks or a fence, where nothing here looks at it. The
|
||||
opposite error costs a guard that checks the wrong six people and says OK. */
|
||||
export function castLikeIn(text) {
|
||||
const text2 = scannable(text);
|
||||
const covered = [...text2.matchAll(/<!--\s*cast\s*:\s*([^>]*?)\s*-->/gi)]
|
||||
.map(m => [m.index, m.index + m[0].length]);
|
||||
return [...text2.matchAll(/<!--[^>]*\bcast\w*\b[^>]*-->/gi)]
|
||||
.filter(m => !covered.some(([a, b]) => m.index >= a && m.index < b))
|
||||
.map(m => ({ raw: m[0].trim().slice(0, 80), line: lineAt(text, m.index) }));
|
||||
}
|
||||
|
||||
/** The six the document declares, without their `pc_` prefixes. Fatal if it declares none. */
|
||||
|
||||
Reference in New Issue
Block a user