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:
co-authored by
Claude Opus 5
parent
aa3ae24ef2
commit
67e76639f3
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user