R-286: one cast declaration, and a mention of one is not a declaration
CLEAN_GROUND.md contains two things matching <!-- cast: ... -->: the real declaration, and the warning ten lines below it that quotes the marker inside backticks and matches with an empty capture. check-rollable and declared-cast both took .match(), first hit wins, so the arrangement has been correct only because the declaration comes first. Move that warning above the list and check-rollable reads a cast of nobody, falls back to ROSTER_BEST, and prints the same OK line having held every skill in the document to the full duty roster instead of the declared six. Instrumented and read off: "the duty roster" against "its declared cast of 6". castMarkersIn blanks code spans and fenced blocks before scanning (padded, so line numbers still point at the source) and refuses more than one surviving marker, naming every line. check-rollable imports it instead of carrying a second regex, so the guard that checks the cast and the guards that measure the fight cannot disagree about who is in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ecbad45fc6
commit
0453255ecc
@@ -39,6 +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";
|
||||
|
||||
/**
|
||||
* The lowest chance that still counts as a route, in percent.
|
||||
@@ -92,10 +93,20 @@ for (const [label, path] of scenarioFiles()) {
|
||||
const raw = readFileSync(path, "utf8");
|
||||
const text = scenarioText(raw);
|
||||
|
||||
// Read the declaration off the RAW text: scenarioText strips HTML, comments included.
|
||||
const declared = raw.match(/<!--\s*cast:\s*([^>]*?)\s*-->/i);
|
||||
/* Read the declaration off the RAW text: scenarioText strips HTML, comments included.
|
||||
Through castMarkersIn rather than a regex of its own, so this guard and the one that
|
||||
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);
|
||||
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 `
|
||||
+ `skill in the document to it; the others were never seen. Keep one declaration and `
|
||||
+ `put any example inside backticks, where the reader ignores it.`);
|
||||
}
|
||||
const declared = markers[0];
|
||||
const cast = declared
|
||||
? declared[1].split(",").map(x => x.trim()).filter(Boolean).map(k => {
|
||||
? declared.keys.split(",").map(x => x.trim()).filter(Boolean).map(k => {
|
||||
const spec = ROSTER.find(r => r.key === k);
|
||||
if (!spec) {
|
||||
problems.push(`${label}: cast declares "${k}", which is not on the duty roster`);
|
||||
|
||||
+33
-4
@@ -27,6 +27,27 @@ export const SCENARIO = path.join(ROOT, "docs", "scenarios", "CLEAN_GROUND.md");
|
||||
a reordering of the cast list and quietly measure different people. */
|
||||
const DROPPED_AT_FOUR = ["pollard", "okonkwo"];
|
||||
|
||||
/* A DECLARATION AND A MENTION OF ONE LOOK IDENTICAL TO A REGEX. CLEAN_GROUND.md carries the
|
||||
real marker under "## Casting" and, ten lines later, the warning that explains it —
|
||||
`<!-- cast: -->` inside backticks, which matches this pattern with an empty capture. Both
|
||||
readers took `.match()`, the FIRST hit, so the whole arrangement worked because the
|
||||
declaration happens to come first. Move the warning above the list, or add a second
|
||||
worked example, and the guards read a cast of nobody without saying so; check-firstblood
|
||||
and check-attackers would then measure a fight this scenario does not cast, which is the
|
||||
defect declared-cast.mjs was written to prevent, arriving through the door it left open.
|
||||
R-268 already had this exact bug from the other side, when a tools file's own example was
|
||||
read as a real declaration.
|
||||
|
||||
So: code spans and fenced blocks are blanked before scanning (padded, so line numbers
|
||||
still point at the source), and more than one surviving marker is fatal rather than
|
||||
first-wins. */
|
||||
const CODE = /```[\s\S]*?```|`[^`\n]*`/g;
|
||||
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 }));
|
||||
}
|
||||
|
||||
/** The six the document declares, without their `pc_` prefixes. Fatal if it declares none. */
|
||||
export function declaredCast(who = "scenario-cast") {
|
||||
if (!existsSync(SCENARIO)) {
|
||||
@@ -34,14 +55,22 @@ export function declaredCast(who = "scenario-cast") {
|
||||
+ `from it. Nothing here can be measured against a scenario that is not there.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const m = readFileSync(SCENARIO, "utf8").match(/<!--\s*cast:\s*([^>]*?)\s*-->/i);
|
||||
if (!m) {
|
||||
console.error(`${who}: ${path.relative(ROOT, SCENARIO)} declares no cast. Add the `
|
||||
const rel = path.relative(ROOT, SCENARIO);
|
||||
const markers = castMarkersIn(readFileSync(SCENARIO, "utf8"));
|
||||
if (!markers.length) {
|
||||
console.error(`${who}: ${rel} 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);
|
||||
if (markers.length > 1) {
|
||||
console.error(`${who}: ${rel} carries ${markers.length} cast markers, at lines `
|
||||
+ `${markers.map(m => m.line).join(", ")}. Reading the first one is how this guard would `
|
||||
+ `measure a fight the scenario does not cast while reporting success. One declaration; `
|
||||
+ `write any example inside backticks or a fenced block, where this reader ignores it.`);
|
||||
process.exit(1);
|
||||
}
|
||||
return markers[0].keys.split(",").map(x => x.trim().replace(/^pc_/, "")).filter(Boolean);
|
||||
}
|
||||
|
||||
/** The declared cast and its four-player cut, checked for the shape the guards recorded. */
|
||||
|
||||
Reference in New Issue
Block a user