R-293: a desk playtest is a record, not a scenario with rolls in it
Counting the readers that had broken on their own format turned up one nothing had caught. docs/scenarios holds scenarios, eight playtest records and two art prompt sheets, and every guard treated all three as scenarios -- harmless for citations and skill spellings, false for reachability. Six quotations across passes 4, 6 and 7 were checked as live rolls, so a record of a session already played could fail the build over a skill nobody can reach. Planting Science (Physics) in pass 4 fails before the split and passes after; the same skill in CLEAN_GROUND still fails. Classified by the document's own H1, not its filename, because tools/scenario-* naming is what swept a tools file into this corpus in R-268. An unclassified document is fatal: an allowlist that silently drops what it does not recognise would take a new scenario out of reachability checking on the day it was written. 89 rolls across 18 files becomes 83 across 8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6171d1e9b9
commit
daf77f4aac
@@ -6894,3 +6894,39 @@ refused, 19 guards green.
|
||||
Wrong. `beatsIn` returns every marker and `measure()` filters the quoted one — Anomaly Lore at
|
||||
line 758, Act Three's quotation of the format. 22 markers, 21 real beats. Both figures were
|
||||
right and the disagreement was mine.
|
||||
|
||||
## R-293 — a record of a session is not a scenario
|
||||
|
||||
The fifth self-reference case, and the only live one: counting the readers that had broken on
|
||||
their own format turned up a sixth that nothing had ever caught. `docs/scenarios/` holds three
|
||||
kinds of document — the scenarios, eight desk playtest records, and two art prompt sheets —
|
||||
and every guard reading the corpus treated all three as scenarios. Harmless for citations and
|
||||
skill spellings. False for reachability.
|
||||
|
||||
check-rollable's claim is that **every clue a GM can put behind a roll is reachable by somebody
|
||||
at the table.** A playtest report quoting `` `[CUS: Anomaly Lore — what a peg is]` `` to say
|
||||
which beat a roll hung off had that quotation held to the claim. Six such quotations across
|
||||
passes 4, 6 and 7 were being checked as live rolls, and a record of a session already played
|
||||
could fail the build over a skill nobody can reach. Measured: planting `Science (Physics)` in
|
||||
pass 4 gives *"a clue is behind a roll nobody present can make"* before the split and passes
|
||||
after it, while the same skill in CLEAN_GROUND still fails. 89 rolls across 18 files becomes
|
||||
83 across 8.
|
||||
|
||||
**Classified by the document's own H1, not its filename**, because `tools/scenario-*` naming is
|
||||
what swept a tools file into this corpus in R-268. A generator has no H1 and is a scenario by
|
||||
construction.
|
||||
|
||||
**An unclassified document is fatal.** An allowlist that silently drops what it does not
|
||||
recognise would take a new scenario out of reachability checking on the day it was written —
|
||||
the fail-open this session has spent its whole length removing. `classifiedFiles()` is called
|
||||
by check-scenarios, the first guard over the corpus, so the message arrives at the right guard,
|
||||
and it names the corpus rather than a guard because it runs inside whichever one asked first.
|
||||
|
||||
**Two things I had wrong on the way**, both caught by insisting on a measurement. The first
|
||||
probe used `Demolition` and passed everywhere, which looked like the guard being blind; it is
|
||||
reachable, and the playtest files have no cast marker so they were being checked against the
|
||||
whole duty roster rather than the declared six. The claim "would have failed the build" was
|
||||
hypothetical until a skill nobody on the roster reaches proved it. And the first version of the
|
||||
new test ended in `assert.ok(… || true)` — a tautology that passes on anything, which is the
|
||||
decoration this suite exists to refuse. Mutation-checked after removing it: making
|
||||
`playableFiles` stop excluding records turns it red.
|
||||
|
||||
@@ -32,7 +32,7 @@ import { ROLES, TRADES, INDUCTION, TIERS, TRADE_BANDS, CHARACTERISTIC_DICE }
|
||||
import { STARTER_AUTHORITY, STARTER_GROUPS, STARTER_CASE, STARTER_TEAM }
|
||||
from "./scenario-starter.mjs";
|
||||
import { castMarkersIn, castLikeIn } from "./declared-cast.mjs";
|
||||
import { tagsIn } from "./check-scenarios.mjs";
|
||||
import { tagsIn, classifiedFiles, playableFiles } from "./check-scenarios.mjs";
|
||||
import { beatsIn, beatLikeIn } from "./outcome-coverage.mjs";
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
@@ -851,6 +851,25 @@ test("a tag one reader accepts is never invisible to the other", () => {
|
||||
}
|
||||
});
|
||||
|
||||
/* ── R-293: a record of a session is not a scenario ──────────────────────────────────
|
||||
check-rollable's claim is that every clue a GM can put behind a roll is reachable by
|
||||
somebody at the table. The corpus it read held three kinds of document and only one is a
|
||||
thing a GM runs, so a desk playtest quoting `[CUS: Anomaly Lore — what a peg is]` to say
|
||||
which beat a roll hung off had that quotation held to the claim. Measured, not assumed:
|
||||
planting an unreachable skill in a playtest report failed the build before this split. */
|
||||
|
||||
test("the corpus knows which of its documents are played and which are records", () => {
|
||||
const kinds = classifiedFiles();
|
||||
assert.ok(kinds.length >= 10, "the corpus has shrunk unexpectedly");
|
||||
assert.ok(kinds.every(([, , kind]) => kind === "playable" || kind === "record"),
|
||||
"classifiedFiles exits on an unclassified document, so nothing here should be null");
|
||||
const playable = playableFiles().map(([l]) => l);
|
||||
assert.ok(playable.includes("docs/scenarios/CLEAN_GROUND.md"));
|
||||
assert.ok(!playable.some(l => /PLAYTEST/i.test(l)),
|
||||
"a desk playtest is a record of a session already played; its quoted beats are not rolls");
|
||||
assert.ok(!playable.some(l => /_ART\.md$/.test(l)), "an art prompt sheet has no rolls");
|
||||
});
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
await runAll();
|
||||
|
||||
@@ -38,7 +38,7 @@ import { readFileSync } from "node:fs";
|
||||
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 { playableFiles, scenarioText, tagsIn, skillsIn } from "./check-scenarios.mjs";
|
||||
import { castMarkersIn, castLikeIn } from "./declared-cast.mjs";
|
||||
|
||||
/**
|
||||
@@ -89,7 +89,11 @@ const problems = [];
|
||||
const report = [];
|
||||
let checkedTags = 0;
|
||||
|
||||
for (const [label, path] of scenarioFiles()) {
|
||||
/* playableFiles(), not the whole corpus: this guard's claim is that every clue a GM can put
|
||||
behind a roll is reachable by somebody at the table. A desk playtest report quoting a beat
|
||||
to say which one a roll hung off is not a roll anybody will make, and holding it to that
|
||||
claim means a record of a session already played can fail the build. */
|
||||
for (const [label, path] of playableFiles()) {
|
||||
const raw = readFileSync(path, "utf8");
|
||||
const text = scenarioText(raw);
|
||||
|
||||
@@ -174,5 +178,5 @@ if (problems.length) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`check-rollable: OK — ${checkedTags} rolls across ${scenarioFiles().length} `
|
||||
+ `scenario files, every one reachable at ${VIABLE}% or better by somebody present`);
|
||||
console.log(`check-rollable: OK — ${checkedTags} rolls across ${playableFiles().length} playable `
|
||||
+ `scenarios (records and art sheets are not rolls), every one reachable at ${VIABLE}% or better by somebody present`);
|
||||
|
||||
@@ -107,6 +107,56 @@ export function scenarioFiles() {
|
||||
];
|
||||
}
|
||||
|
||||
/* WHAT A DOCUMENT IS, not what it is named. The corpus above holds three kinds and only one
|
||||
of them is a thing a GM runs: the scenarios, the desk playtest records, and the art prompt
|
||||
sheets. Every guard reading the corpus has treated all three as scenarios, which was
|
||||
harmless for citations and skill spellings and false for reachability — a playtest report
|
||||
quoting `[CUS: Anomaly Lore — what a peg is]` to say which beat a roll hung off had that
|
||||
quotation checked as a live roll, and would have failed the build over a skill nobody can
|
||||
reach in a document that is a record of a session already played.
|
||||
|
||||
Classified by the document's own H1 rather than its filename, because `tools/scenario-*`
|
||||
naming is what swept a tools file into this corpus in R-268. A generator has no H1 and is
|
||||
a scenario by construction.
|
||||
|
||||
AN UNCLASSIFIED FILE IS FATAL. An allowlist that silently drops what it does not recognise
|
||||
would take a new scenario out of reachability checking on the day it was added, which is
|
||||
the fail-open this suite has spent the session removing. Same rule as powers.mjs: a thing
|
||||
the tools read must be classified, and the build says so if it is not. */
|
||||
const PLAYABLE_H1 = /^(Crossing case|Crossing starter|Prologue)\b/;
|
||||
const RECORD_H1 = /desk playtest \d|art(work)? prompt/i;
|
||||
|
||||
/** [label, path, kind] for every corpus file; kind is "playable" or "record". */
|
||||
export function classifiedFiles() {
|
||||
const unknown = [];
|
||||
const out = scenarioFiles().map(([label, abs]) => {
|
||||
if (label.startsWith("tools/scenario-")) return [label, abs, "playable"];
|
||||
const h1 = (readFileSync(abs, "utf8").match(/^#\s+(.+)$/m) ?? [, ""])[1].trim();
|
||||
if (PLAYABLE_H1.test(h1)) return [label, abs, "playable"];
|
||||
if (RECORD_H1.test(h1)) return [label, abs, "record"];
|
||||
unknown.push(`${label} — its heading reads "${h1.slice(0, 70)}"`);
|
||||
return [label, abs, null];
|
||||
});
|
||||
if (unknown.length) {
|
||||
/* Named for the corpus, not for a guard: this runs inside whichever guard asked first,
|
||||
and a message saying "check-scenarios" while check-rollable is on screen sends the
|
||||
reader to the wrong file. */
|
||||
console.error(`scenario corpus: FAILED — ${unknown.length} document(s) in docs/scenarios are `
|
||||
+ `neither a scenario nor a record, so nothing knows whether their rolls are live:`);
|
||||
unknown.forEach(u => console.error(` ${u}`));
|
||||
console.error(` A scenario's heading starts "Crossing case", "Crossing starter" or `
|
||||
+ `"Prologue"; a record's says "desk playtest N" or "art prompt". Guessing would put a `
|
||||
+ `new scenario outside reachability checking on the day it was written.`);
|
||||
process.exit(1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The corpus files a GM actually runs — the only ones whose rolls must be reachable. */
|
||||
export function playableFiles() {
|
||||
return classifiedFiles().filter(([, , kind]) => kind === "playable").map(([l, a]) => [l, a]);
|
||||
}
|
||||
|
||||
/** Lines that say a skill does NOT exist are documentation, not an instruction. */
|
||||
export function scenarioText(raw) {
|
||||
return stripHtml(raw)
|
||||
@@ -115,6 +165,7 @@ export function scenarioText(raw) {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
classifiedFiles(); // every corpus document must be a scenario or a record; fatal if not
|
||||
const files = scenarioFiles();
|
||||
|
||||
const problems = [];
|
||||
|
||||
Reference in New Issue
Block a user