R-284: report a wrong figure as a wrong figure, and read the right entry

R-283 left the intact-sentence-wrong-number case falling through to omission, so
the guard said BESTIARY "does not state" a rating the page was stating. reads()
now has a shape tier between strict and loose: the strict pattern with its value
slot loosened, reporting which of the two numbers moved.

Scoped the per-creature rules while adding it. They read the whole page, and each
is the only rule of its kind today, so a page-wide match found the right line by
luck; a second attackFactor creature would have let the courier's rule match that
creature's sentence and report the courier correct. They now read the creature's
own "### Name" entry -- proved by deleting the courier's line and planting an
identical one under the redcap: still omission, where before it would have passed.

Five discriminations plus the decoy, proved in a worktree with the message read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 15:15:24 +01:00
co-authored by Claude Opus 5
parent 327d060eb1
commit 1a2ca44c52
2 changed files with 66 additions and 7 deletions
+25
View File
@@ -6637,3 +6637,28 @@ digits; reword the statblock too and this degrades to OMISSION, the safe directi
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.
## R-284 — the third tier: a figure that moved is not a sentence that went
R-283 left a case falling through to the omission branch: the sentence intact, the number
inside it wrong. The message said BESTIARY "does not state the rating the simulator actually
uses" about a page that was stating one. It named the right figures, so it pointed at the
fix, but it described a document that does not exist — the same class of confidently-wrong
report R-283 was written to remove, one case further in.
`reads` now runs four tiers: **strict** (the sentence as it stands) passes; **shape** — the
strict pattern with its value slot loosened — reports a MISMATCH and says which of the two
numbers moved; **loose** reports a REWORD; nothing reports the OMISSION. The mismatch text
is the sharpest of the four on purpose: *a figure the page states and the harness
contradicts is worse than one it omits, because a GM can act on it.*
**Scoped while adding it.** The rules read `page`, and every one of them is the only rule of
its kind today, so a page-wide match found the right line by luck. Wire a second
`attackFactor` creature and the courier's rule would have matched that creature's sentence
and reported the courier correct — a guard passing on someone else's evidence. Per-creature
rules now read the creature's own `### Name` entry. Proved by deleting the courier's line and
planting an identical one in the redcap's entry: still OMISSION, where before scoping it
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.
+41 -7
View File
@@ -137,11 +137,25 @@ if (!page) {
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 sentencesIn = (text, needles) =>
text.split(/\n+|(?<=\.)[ \t]+/).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`.
/* 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`);
if (at < 0) return null;
const rest = page.slice(at);
const end = rest.slice(1).search(/\n#{2,3} /);
return end < 0 ? rest : rest.slice(0, end + 1);
};
/** 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
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**
@@ -151,11 +165,19 @@ if (!page) {
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 reads = ({ who, where, strict, shape, loose, still, omission }) => {
const text = where ?? page;
if (strict.test(text)) return [];
/* THE SENTENCE IS INTACT AND THE NUMBER INSIDE IT IS WRONG. Without this tier that case
fell through to `omission`, which said the page "does not state" a rating the page was
stating — the right figures were named, so it pointed at the fix, but it described a
document that does not exist. `shape` is the strict pattern with the value slot
loosened, so the guard can tell a figure that moved from a sentence that went. */
const m = shape && text.match(shape.re);
if (m) return [shape.mismatch(m)];
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)));
const near = sentencesIn(text, 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 `
@@ -187,7 +209,7 @@ if (!page) {
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) {
const near = sentencesWith("spend defences");
const near = sentencesIn(page, ["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 `
@@ -208,9 +230,21 @@ if (!page) {
const arm = c.arms?.[0];
if (!arm) return [];
const effective = Math.floor(arm.rating * effect.attackFactor);
const entry = entryOf(spec.name);
if (!entry) return [` ${spec.key}: BESTIARY has no "### ${spec.name}" entry to check`];
return reads({
who: spec.key,
where: entry,
strict: new RegExp(`\\*\\*${effective}%\\*\\*, not ${arm.rating}%`),
shape: {
re: /\*\*(\d+)%\*\*, not (\d+)%/,
mismatch: ([line, printed, base]) =>
` ${spec.key}: BESTIARY prints "${line}" and the simulator fights it at `
+ `${effective}% of ${arm.rating}% — ${Number(printed) !== effective ? `the effective `
+ `rating is wrong (${printed}%, should be ${effective}%)` : `the rating it is taken `
+ `from is wrong (${base}%, should be ${arm.rating}%)`}. A figure the page states `
+ `and the harness contradicts is worse than one it omits: a GM can act on it.`
},
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 `