R-283: tell a reworded bestiary sentence apart from a missing one

R-282 matched the page with String.includes, so rewording the exemption sentence
reported "BESTIARY never says it is off the stripping ladder" -- sending a
maintainer after a sentence that is sitting right there, and never naming the
real problem, which is a pattern that has silently stopped reading.

Each textual rule now reads twice. Strict is the sentence as it stands and is
tighter than before (the bold and the full stop, not the bare clause a substring
accepted); loose is the same claim in any wording. Strict passes, loose-only is
reported as a reword with the line quoted and the page presumed right, neither is
the omission.

The loose anchor was wrong on its first pass in the way that matters: "a sentence
with 40% and 80%" also matched the courier's own statblock line, so deleting the
sentence reported a reword and quoted the statblock back. It now excludes that
generated marker, which makes it a test of the claim and not of the digits, and
degrades to omission rather than to a false reword.

Four discriminations proved in a worktree with the message read in each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 15:12:33 +01:00
co-authored by Claude Opus 5
parent e70d25271f
commit 327d060eb1
2 changed files with 101 additions and 9 deletions
+27
View File
@@ -6610,3 +6610,30 @@ new `reachBonus` effect fails for having no page rule. Exit codes read directly.
assertions are textual — they check that the page says a thing, not that what it says is
true. A `damageFactor` rule is registered as a no-op because nothing wires one today, and
it will need writing when something does; the guard will demand it, but it cannot write it.
## R-283 — a reworded sentence and a missing one are different failures
R-282 checked the bestiary's prose with `String.includes`, which has one bad failure and it
is the one this suite keeps finding elsewhere: **a guard that reports the wrong thing
confidently.** Reword the exemption sentence and the message read "BESTIARY never says it is
off the stripping ladder" — sending a maintainer to look for a sentence that is sitting
right there, while the real problem (a pattern that has quietly stopped reading) goes
unmentioned.
Each textual rule now reads the page twice. **Strict** is the sentence as it stands, and
tighter than before: `**Name is not on this ladder at all.**` with its bold and its full
stop, not the bare clause a substring would have accepted. **Loose** is the same claim in
any wording. Three outcomes, three messages — strict passes; loose-only is named a REWORD,
quotes the line it found, and says the page may well be right; neither is the omission.
**The loose test was itself wrong first, and the way it was wrong is the point.** Anchoring
the courier's rule on "a sentence containing 40% and 80%" matched its own statblock line —
`**Attacks with** Grasping limb at 40% ... **dodges** 80%` — so *deleting* the sentence
outright reported a reword and quoted the statblock back as the surviving claim. Confident
and wrong, one level below the defect the two-tier read exists to prevent. The loose anchor
now excludes that generated marker, which makes it a test of the claim rather than of the
digits; reword the statblock too and this degrades to OMISSION, the safe direction.
Four discriminations proved in a worktree, message read in each: reword kept → REWORD;
line deleted → OMISSION; wrong number, right wording → mismatch naming both figures; redcap
back on the ladder → the R-282 contradiction, unchanged.
+74 -9
View File
@@ -116,6 +116,54 @@ if (!page) {
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
literal `includes` says the document never makes the claim — when the claim is sitting
there and only the wording moved. A maintainer then goes looking for a missing sentence
that exists, and the true problem (a pattern that has quietly stopped reading) is the one
thing the message does not mention.
So each textual rule reads the page twice. STRICT is the sentence as it stands, tight
enough that a changed word fails rather than passing on a substring — `**Name is not on
this ladder at all.**` with its bold and its full stop, not the bare clause. LOOSE is the
same claim in any wording: the subject and the idea inside one sentence. Three outcomes,
three different messages:
strict matches the page says it, in the form this guard reads pass
loose only the page still says it; this guard cannot read it REWORD
neither the page does not make the claim OMISSION
Only the last is a defect in the document. The middle one is a decision for whoever
changed the prose — restore the wording, or move the pattern — and reporting it as the
last would send them to fix a page that is already right. */
const rx = t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const SENTENCES = page.split(/\n+|(?<=\.)[ \t]+/);
const sentencesWith = (...needles) => SENTENCES.filter(s => needles.every(n => s.includes(n)));
const quote = s => s.trim().replace(/\s+/g, " ").slice(0, 120);
/** strict -> [] ; 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
as something else. The exclusion is not fussiness. The courier's factored-attack
sentence prints 40% and 80%, and so does its own statblock line — `**Attacks with**
Grasping limb at 40% ... **dodges** 80%` — so deleting the sentence outright first
reported a REWORD and quoted the statblock back as the surviving claim. The message was
confident and wrong, which is the exact defect the two-tier read exists to prevent, one
level down. Excluding the statblock's own generated marker makes the loose test about
the claim rather than about the digits; reword that marker too and this degrades to
OMISSION, which is the safe direction to fail. */
const reads = ({ who, strict, loose, still, omission }) => {
if (strict.test(page)) return [];
const has = Array.isArray(loose) ? loose : loose.has;
const not = Array.isArray(loose) ? [] : (loose.not ?? []);
const near = sentencesWith(...has).filter(s => !not.some(n => s.includes(n)));
if (!near.length) return [omission];
return [` ${who}: BESTIARY still ${still}, but no longer in the form this guard reads, so `
+ `nothing checks it any more. The line it found: "${quote(near[0])}". The page may well `
+ `be right — this is a reword, not a missing claim. Restore the wording or update the `
+ `pattern in DOCUMENT_RULES. A guard that has silently stopped reading its own sentence `
+ `is worse than one that never read it, because the build stays green.`];
};
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
@@ -127,14 +175,26 @@ 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 (!page.includes(`${spec.name} is not on this ladder at all`)) {
out.push(` ${spec.key}: BESTIARY never says it is off the stripping ladder. A reader `
out.push(...reads({
who: spec.key,
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`,
omission: ` ${spec.key}: BESTIARY never says it is off the stripping ladder. A reader `
+ `who takes the section's advice against it is being told to do the one thing that `
+ `does not work.`);
}
+ `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/);
if (!m) out.push(` BESTIARY no longer says how many creatures spend defences at all`);
if (!m) {
const near = sentencesWith("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 `
+ `no longer reconciled against powers.mjs. This is a reword: fix the wording or the `
+ `pattern, not the count.`
: ` BESTIARY no longer says how many creatures spend defences at all`);
}
else if (Number(m[1]) !== dodgers.length - exemptDodgers) {
out.push(` BESTIARY counts ${m[1]} creatures that spend defences; the game has `
+ `${dodgers.length} that dodge and ${exemptDodgers} exempt, so it should be `
@@ -148,10 +208,15 @@ if (!page) {
const arm = c.arms?.[0];
if (!arm) return [];
const effective = Math.floor(arm.rating * effect.attackFactor);
return page.includes(`**${effective}%**, not ${arm.rating}%`) ? []
: [` ${spec.key}: BESTIARY does not state the rating the simulator actually uses `
+ `(${effective}%, not ${arm.rating}%). Without it the page prints the statblock and `
+ `every measured figure beside it is from a different creature.`];
return reads({
who: spec.key,
strict: new RegExp(`\\*\\*${effective}%\\*\\*, not ${arm.rating}%`),
loose: { has: [`${effective}%`, `${arm.rating}%`], not: ["**Attacks with**"] },
still: `prints ${effective}% and ${arm.rating}% together`,
omission: ` ${spec.key}: BESTIARY does not state the rating the simulator actually uses `
+ `(${effective}%, not ${arm.rating}%). Without it the page prints the statblock and `
+ `every measured figure beside it is from a different creature.`
});
},
damageFactor: () => []
};