From ae8179bd9b350e0f98c882d08c7094c37cace744 Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Sun, 13 Sep 2026 15:47:35 +0100 Subject: [PATCH] R-290: the same spelling gap in tagsIn and the POWER marker Swept every reader that parses a human-written marker. Two more had R-289's fail-open. tagsIn matched a literal [CUS:, so [cus: Spot], [Cus: Spot], [CUS : Spot] and [ CUS: Spot] all read as nothing -- and it is the corpus reader behind check-scenarios' skill validation and check-rollable's reachability, so a beat spelled any of those four ways was checked by neither while both printed OK. The corpus contains no such tag today; the 88-to-89 roll count during this work was c0 writing v0.18, verified against HEAD's reader on the same tree. The POWER: marker had it with nothing covering it. bestiary.mjs and check-powers' own scan both used the literal, so a creature added with "Power:" generates no entry line and is never reported unclassified -- check-powers passes green on it, measured on a clone with its dodge skills stripped so the dodger-count assertion could not fire instead. Reader now takes POWER\s*: and stays uppercase, because power: occurs in ordinary prose; an asymmetric counter names the rest, and was measured against content.mjs first (41 strict, zero loose outside them). Co-Authored-By: Claude Opus 5 --- docs/REVIEW_LOG.md | 36 ++++++++++++++++++++++++++++++++++++ tools/bestiary.mjs | 5 ++++- tools/check-powers.mjs | 30 +++++++++++++++++++++++++++++- tools/check-scenarios.mjs | 11 +++++++++-- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index eab4dd5..d1ef9c5 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -6804,3 +6804,39 @@ pass where three silently did not and the green that followed proved nothing: | 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 | + +## R-290 — the same spelling gap in the other two readers + +Swept every reader that parses a human-written marker, after R-289 found `` +invisible. Two more had it. The probe is the same each time and it costs a minute: put the +plausible misspellings to the reader as strings and print what it returns. + +**`tagsIn` in check-scenarios.** `/\[CUS:\s*([^\]]+)\]/g` — no `i`, no space tolerance. +`[cus: Spot]`, `[Cus: Spot]`, `[CUS : Spot]` and `[ CUS: Spot]` all read as nothing. This is +the corpus reader behind check-scenarios' skill validation and check-rollable's reachability, +so a beat spelled any of those four ways was checked by neither guard while both printed OK. +Now `\[\s*CUS\s*:\s*...\]`, case-insensitive. **The corpus contains no such tag today** — the +88-to-89 roll count during this work was c0 writing v0.18, not the widening finding one, and +HEAD's reader gives 89 on the same tree. Checked rather than claimed, because a preventive fix +reported as a discovery is a lie about what the tree contained. + +Worth recording that c0's `outcome-coverage.mjs` loose pattern already catches all four as +unparsed. Its guard was the only thing between those spellings and silence. + +**The `POWER:` marker, which nothing covered.** `bestiary.mjs` and check-powers' own +classification scan both used the literal, so `Power:` and `POWER :` generated no entry line +and were never reported unclassified. A creature added tomorrow with `Power:` in its tactics +**passes check-powers green** — 41 powers classified, the forty-second invisible. Measured on a +clone with its dodge skills stripped, so the dodger-count assertion could not fire and take the +credit: HEAD OK, this change names the marker and the creature. + +The reader now takes `POWER\s*:` and stays uppercase, because `power:` occurs in ordinary +tactics prose and matching it would invent powers. The other half is the counter: anything +power-marker-shaped, case and spacing free, that the strict reader did not consume. Measured +against content.mjs before wiring — 41 strict markers, zero loose ones outside them — so it +starts from a clean corpus rather than from an exception list. + +**All three fixes share one shape, which is check-cited's from R-283 and is now the house +pattern**: widen the reader for the spellings you thought of, and add a looser counter that +names the one you did not. A marker nothing reads is worse than no marker, because whoever +wrote it believes the thing is declared. diff --git a/tools/bestiary.mjs b/tools/bestiary.mjs index 8ea5885..d6c0a6e 100644 --- a/tools/bestiary.mjs +++ b/tools/bestiary.mjs @@ -76,7 +76,10 @@ const essenceOf = spec => String(spec.role ?? "").replace(/^[A-Z]{3,}[^.]*\.\s*/ /** The POWER line is the mechanic a GM needs in front of them; the rest is staging. */ function powerOf(spec) { - const m = String(spec.tactics ?? "").match(/POWER:\s*([A-Z][A-Z '’-]*)\.\s*([^]*?)(?=\s*(?:POWER:|$))/); + /* `POWER\s*:` — the marker stays uppercase, because `power:` occurs in ordinary tactics + prose and matching it would invent powers, but a space before the colon must not hide one. + check-powers carries the counter that names the spellings this deliberately refuses. */ + const m = String(spec.tactics ?? "").match(/POWER\s*:\s*([A-Z][A-Z '’-]*)\.\s*([^]*?)(?=\s*(?:POWER\s*:|$))/); if (!m) return null; let body = m[2].trim().replace(/\s+/g, " "); // Trim to the first two sentences: the rule, and the qualification on it. diff --git a/tools/check-powers.mjs b/tools/check-powers.mjs index f6b78e1..bc7051d 100644 --- a/tools/check-powers.mjs +++ b/tools/check-powers.mjs @@ -40,7 +40,35 @@ const problems = []; /* 1. Classification. The prose is the authority for what a power SAYS; powers.mjs is the authority for what the harness does about it, and every one must be in both. */ -const withPower = NPCS.filter(n => /POWER:/.test(n.tactics ?? "")); +const withPower = NPCS.filter(n => /POWER\s*:/.test(n.tactics ?? "")); + +/* A POWER THIS READER CANNOT SEE IS A POWER NOBODY REPORTS UNCLASSIFIED. The marker was the + literal `POWER:` in both this guard and bestiary.mjs, so `Power:` or `POWER :` generated no + entry line AND passed the classification check — R-275's defect with the guard written to + prevent it looking straight past it, and R-289's fail-open one file over. + + Widening the reader fixes the spellings we thought of. This is the other half: anything + power-marker-shaped, case and spacing free, that the strict reader did not consume. It is + deliberately looser, so a creature whose tactics prose happens to say "power:" trips it and + has to be reworded — which costs a sentence, where the opposite error costs a power the + whole book measures without. Measured against content.mjs before wiring: 41 strict markers, + zero loose ones outside them, so this starts from a clean corpus rather than from an + exception list. */ +{ + const STRICT = /POWER\s*:/g, LOOSE = /\bpowers?\s*:/gi; + for (const n of NPCS) { + const t = String(n.tactics ?? ""); + const spans = [...t.matchAll(STRICT)].map(m => [m.index, m.index + m[0].length]); + for (const m of t.matchAll(LOOSE)) { + if (spans.some(([a, b]) => m.index >= a && m.index < b)) continue; + problems.push(` ${n.key}: tactics carry "${m[0]}", which looks like a POWER marker and is ` + + `not one either reader can see — "…${t.slice(Math.max(0, m.index - 40), m.index + 30) + .replace(/\s+/g, " ")}…". Spelled this way it generates no bestiary line and is never ` + + `reported unclassified. The marker is \`POWER:\` in capitals; if the word is ordinary ` + + `prose here, reword it.`); + } + } +} for (const spec of withPower) { const p = POWERS[spec.key]; if (!p) { diff --git a/tools/check-scenarios.mjs b/tools/check-scenarios.mjs index 403cd6c..e136810 100644 --- a/tools/check-scenarios.mjs +++ b/tools/check-scenarios.mjs @@ -54,10 +54,17 @@ export function skillsIn(body) { .filter(x => x && !NOISE.has(x.toLowerCase()) && !/^\d/.test(x)); } +/* [CUS: ...] as written in the markdown, and the HTML-escaped form in the generators. + Case and spacing are tolerated. The literal `\[CUS:` was measured against the obvious + near-misses and `[cus: Spot]`, `[Cus: Spot]`, `[CUS : Spot]` and `[ CUS: Spot]` all read as + nothing — and reading as nothing is the bad direction here, because this function is what + check-scenarios validates skills through and what check-rollable checks reachability + through. A beat spelled any of those four ways was checked by neither guard while both + printed OK. Same fail-open as R-289's `` and check-cited's three spellings of + `cite:`; found the same way, by putting the spellings to the reader instead of to the tree. */ export function tagsIn(text) { - // [CUS: ...] as written in the markdown, and the HTML-escaped form in the generators const out = []; - for (const m of text.matchAll(/\[CUS:\s*([^\]]+)\]/g)) out.push(m[1]); + for (const m of text.matchAll(/\[\s*CUS\s*:\s*([^\]]+)\]/gi)) out.push(m[1]); return out; }