R-285: scope the defence-stacking rules to the section that owns them

R-284 scoped the factored-rating rule to the creature's entry and left the
defence-stacking rules matching anywhere on the page, so the exemption sentence
and the "Across N creatures that spend defences" count were accepted wherever they
happened to sit. Moving the exemption line out of its section into the redcap's
statblock -- the page silent exactly where a GM reads the stripping advice --
passes at R-284 and fails here; confirmed by running HEAD's copy against the same
tree.

The owning slice is not always the creature's entry. A factored rating belongs to
the creature; the ladder exemption is an answer to the paragraph it sits in and is
generated into "## Shooting at something that moves", so scoping that rule to
"### Redcap" would have failed a correct page. sliceOf now takes a heading at any
level and each rule names the slice that owns its claim.

A renamed section fails by name rather than scoping to nothing, which is the shape
of every guard that passes because it found nothing to check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 15:22:04 +01:00
co-authored by Claude Opus 5
parent aa3ae24ef2
commit 67e76639f3
2 changed files with 55 additions and 12 deletions
+28
View File
@@ -6662,3 +6662,31 @@ would have passed.
Five cases proved in a worktree, message read in each: effective rating wrong, base rating
wrong, both wrong, reworded with the numbers kept, deleted. Plus the decoy above.
## R-285 — the ladder rules read the ladder, and the owning slice is not always the entry
R-284 scoped the factored-rating rule to `### The Courier` and left the defence-stacking
rules reading the whole page. Same latent defect, other half of the file: the exemption
sentence and the "Across N creatures that spend defences" count were matched anywhere in
`docs/BESTIARY.md`, so the guard would accept the claim wherever it happened to sit.
Proved rather than assumed. Moving the exemption sentence out of its section and into the
redcap's statblock entry — the page now silent exactly where a GM reads the stripping advice
— **passes at R-284** and fails after this change. Same for the counted line.
**The owning slice is not the creature's entry, and assuming it was would have broken a
correct page.** A factored rating belongs to the creature: `### The Courier` owns it. The
ladder exemption does not — "stripping is not a plan against it" is an answer to the
paragraph it sits in, and the sentence is generated into `## Shooting at something that
moves`. Scoping that rule to `### Redcap` would have failed a document that is right. So
`sliceOf` takes a heading at any level and returns it down to the next heading of the same
or higher level, and each rule names the slice that owns its claim.
Renaming the section itself now fails by name — *BESTIARY has no "## Shooting at something
that moves" section, which is where the defence-stacking rules are checked* — rather than
silently scoping to nothing, which is the shape of every guard that passes because it found
nothing to check.
Five cases in a worktree: two decoys (fail here, pass at R-284, the pass confirmed by running
HEAD's copy against the same tree), a reword inside the section, the renamed heading, and the
clean control.
+27 -12
View File
@@ -114,7 +114,6 @@ if (!page) {
problems.push(` docs/BESTIARY.md is missing, and the powers are checked against it`);
} else {
const dodgers = NPCS.filter(n => (n.skills ?? []).some(k => k.fam === "dodge" && k.val > 0));
const strippingList = (page.split("The ones that take the most stripping:")[1] ?? "").split("\n\n")[1] ?? "";
/* MATCHING PROSE HAS ONE BAD FAILURE MODE, and it is the failure this suite keeps finding
everywhere else: a guard that reports the wrong thing confidently. Reword the page and a
@@ -141,19 +140,28 @@ if (!page) {
text.split(/\n+|(?<=\.)[ \t]+/).filter(s => needles.every(n => s.includes(n)));
const quote = s => s.trim().replace(/\s+/g, " ").slice(0, 120);
/* A per-creature rule must read that creature's entry, not the page. Every rule here
happens to be the only one of its kind today, so a page-wide match finds the right line
by luck; wire a second attackFactor creature and the courier's rule would match ITS
sentence and report the courier correct. Entries are `### Name` and run to the next
heading. */
const entryOf = name => {
const at = page.indexOf(`### ${name}\n`);
/* A rule must read the part of the page that OWNS its claim, not the page. Every rule
here happens to be the only one of its kind today, so a page-wide match finds the right
line by luck; wire a second attackFactor creature and the courier's rule would match ITS
sentence and report the courier correct. `sliceOf` takes a heading and returns it plus
everything down to the next heading of the same or higher level. */
const sliceOf = heading => {
const at = page.indexOf(`${heading}\n`);
if (at < 0) return null;
const level = heading.match(/^#+/)[0].length;
const rest = page.slice(at);
const end = rest.slice(1).search(/\n#{2,3} /);
const end = rest.slice(1).search(new RegExp(`\\n#{2,${level}} `));
return end < 0 ? rest : rest.slice(0, end + 1);
};
/* The owning slice is not always the creature's own entry, and assuming it was would have
been wrong here. A factored rating belongs to the creature, so `### The Courier` owns it.
The ladder exemption does NOT: it is advice about a section — "stripping is not a plan
against it" answers the paragraph it sits in — so `## Shooting at something that moves`
owns it, and scoping that rule to `### Redcap` would have failed a page that is correct. */
const LADDER_HEADING = "## Shooting at something that moves";
const ladder = sliceOf(LADDER_HEADING);
/** strict -> [] ; right shape, wrong value -> `shape.mismatch` ; loose only -> a reword
problem naming the line ; neither -> `omission`.
`loose` is {has, not}: the needles the claim must carry AND the ones that mark a line
@@ -186,6 +194,11 @@ if (!page) {
+ `is worse than one that never read it, because the build stays green.`];
};
if (!ladder) problems.push(` BESTIARY has no "${LADDER_HEADING}" section, which is where the `
+ `defence-stacking rules are checked`);
const strippingList = ((ladder ?? "").split("The ones that take the most stripping:")[1] ?? "")
.split("\n\n")[1] ?? "";
const DOCUMENT_RULES = {
/* A creature that never spends a defence is not on the stripping ladder, must not be
counted among those that are, and must be named — because the advice the section
@@ -197,8 +210,10 @@ if (!page) {
+ `and ${POWERS[spec.key].name} means it never spends a defence. That is the opposite `
+ `of the advice, printed for a GM to act on.`);
}
if (!ladder) return out;
out.push(...reads({
who: spec.key,
where: ladder,
strict: new RegExp(`\\*\\*${rx(spec.name)} is not on this ladder at all\\.\\*\\*`),
loose: [spec.name, "ladder"],
still: `mentions ${spec.name} and the ladder in one sentence`,
@@ -207,9 +222,9 @@ if (!page) {
+ `does not work.`
}));
const exemptDodgers = dodgers.filter(n => combatPowerFor(n.key)?.defenceStacking === "ignores").length;
const m = page.match(/Across \*{0,2}(\d+)\*{0,2} creatures that spend defences/);
const m = ladder.match(/Across \*{0,2}(\d+)\*{0,2} creatures that spend defences/);
if (!m) {
const near = sentencesIn(page, ["spend defences"]);
const near = sentencesIn(ladder, ["spend defences"]);
out.push(near.length
? ` BESTIARY still talks about creatures that spend defences — "${quote(near[0])}" — but `
+ `not in the counted form "Across N creatures that spend defences", so the number is `
@@ -230,7 +245,7 @@ if (!page) {
const arm = c.arms?.[0];
if (!arm) return [];
const effective = Math.floor(arm.rating * effect.attackFactor);
const entry = entryOf(spec.name);
const entry = sliceOf(`### ${spec.name}`);
if (!entry) return [` ${spec.key}: BESTIARY has no "### ${spec.name}" entry to check`];
return reads({
who: spec.key,