Commit Graph
25 Commits
Author SHA1 Message Date
slaguru666andClaude Opus 5 9c2ce9d905 R-290: check-outcomes — a beat that names a roll must say what it does
Desk pass 7's finding, turned into the nineteenth guard. check-rollable asks
whether a skill is reachable at 25% by the declared cast; pass 5 found that
is rollability, not competence; pass 7 found it is also not coverage. A beat
could be reachable, well-rated and silent about every band but the one the GM
improvises, and the whole suite stayed green.

tools/outcome-coverage.mjs measures. Beats are located structurally — the
bullet that owns the tag and its children, ending at the next bullet of the
same or shallower indent or the next heading, because R-287 is what an
unbounded scope does. Band widths are counted by grading all 100 results
through gradeRoll, never by arithmetic on fumbleStart, which is off by one
and was written wrongly twice this session before being caught. Ratings come
from the declared cast via R-286's reader, best-in-cast per skill, because
that is the die a table actually rolls.

tools/check-outcomes.mjs holds it, in two kinds:

  RATCHET — stated failure/fumble/special counts may improve and may not
  regress; unwritten-band exposure may fall and may not rise. --update
  re-records these, because freezing them would make every improvement fail.

  HARD CLAIMS — read from what is measured NOW, never from the baseline, so
  --update cannot silence them: a floor of 18 beats (check-cited once passed
  with zero citations), no beat bare of every band, every scope structurally
  bounded and none over 5% of the file, no unparseable tag, no beat naming a
  skill the cast has no rating for.

Verified by breaking it: a stripped beat, a broken tag regex, an unparseable
tag and a removed failure case each turn it red; the file and baseline were
restored byte-identical after each; and --update with a bare beat present
re-records the baseline and still fails.

Two bare beats found and filled while building it — the six at the back, and
the Insight that is deliberately indistinguishable on a success and a miss,
which now says so rather than saying nothing. Coverage 16/21 failure, 5
fumble, 4 special at the start; 18/21, 6 and 6 now.

The three figures are cited against the artifact rather than typed, which was
the peer session's condition and the right one: pass 7 hand-counted 22 beats
where there are 21 (Act Three's warning QUOTES a beat, and a hand count reads
the quotation as one — R-286 in the other direction), and its other three
counts were stale within one commit.

Its first catch was its author: the STATUS line announcing this guard
contained a beat-shaped tag and was counted as a beat. The reader was not
changed — prose shaped like a beat is what this file is for — and the error
message now names the fenced block as the place to write an example. Minutes
later check-cited refused a citation-shaped comment in the post-pass about
citations. Three readers, three authors describing their own format inside
it, each caught by a guard built for a different pass.

CLEAN GROUND v0.18. npm run check: 19 guards pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:47:25 +01:00
slaguru666andClaude Opus 5 32ca982369 R-275: the harness had never read a creature's power
41 statblocks carry a POWER in their tactics and simulate.mjs read none of them,
so a redcap that ignores the cumulative defence penalty has been measured as a
creature that tires -- in check-lethality, in check-focus, and in every figure
published about it. The defect was not that the powers were unimplemented, it was
that nothing said they were not.

powers.mjs classifies all 41: 2 wired, 14 notSimulable with a stated reason, 25
not fight rules. check-powers refuses an unclassified POWER and refuses a
notSimulable without a reason -- and it does not test that the harness imports a
power, it fights the creature with and without and requires the two to disagree.

Moved: the courier 9.4% to 1.0% wiped (it attacks at half while carrying), the
redcap 0.7% to 0.9% (small, because these fights rarely spend a second defence).

ARGENT AND GULES was wired and then un-wired: it tripled the supporter's wipe rate
to 75.2% because the harness has no ground and applied the borough-ground condition
unconditionally. Same reason THE PULL is not wired. I had wired one and refused the
other on identical facts.

Lethality and focus re-recorded, bestiary regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:05:11 +01:00
slaguru666andClaude Opus 5 9f3a63174f CLEAN GROUND v0.12: convention one-shot, the print pack, and the real opposed roll
Tim settled the last two questions: convention one-shot, and rules.mjs gains a
real opposed roll (landed by a peer session at R-273). Both decisions change
the document, and the one-shot unblocked the print pack.

THE PRINT PACK — docs/scenarios/CLEAN_GROUND_HANDOUTS.html, guard 17.

Four A4 sheets, self-contained, no external fonts or assets: H01 the coroner's
note on white, H02 the 1962 committee minute on cream with photocopy grain,
H03a and H03b the almanac on ruled paper. 11pt floor throughout.

The binding constraint is that H03a and H03b must print identically or the
almanac trick dies, so that is structural rather than careful: they share one
`.almanac` class and every dimension comes from a variable defined once. There
is no selector anywhere that names one page and not the other, and
check-handouts fails the build if one appears, if their markup structures
diverge, if their columns differ, if any row stops reading "41 mi", if the
counts stop being forty-one now against fifty-three then, or if the pack and
the scenario drift apart.

It earned itself immediately: its first run failed my own pack for five rules
at 10.5pt, under the house 11pt floor. Rendering was checked visually too,
which caught two things no guard would have — the "TO CLEAN GROUND" header
colliding with NOTES, and the writing crossing the red margin rule instead of
starting right of it.

THE ONE-SHOT. Countdown step 6 said "and this is a campaign", which the
decision contradicts. Rewritten, and the Close's "leave it filed" ending now
says how to land it tonight: do not end on "you'll be back", because the table
never will and a hook they cannot take reads as an unfinished scenario. Name
the next agent who gets sent, and have Registry thank them.

THE OPPOSED ROLL. The scenario-local ruling is deleted; GM ESSENTIALS points
at the game. Both beats were re-priced against the real rule by enumerating
all 10,000 roll pairs -- exact, no seeds -- and two things fell out:

  THE OFFER IS PRICED. Ashcroft refusing is taken 21.9% of the time, the
  safest file at the table. Accepting: 48.7%, past Braithwaite's 37.6%.
  Accepting does not make him a bit more vulnerable, it makes him the easiest
  person in the room, and the countdown reaches for the easiest.

  STEP 5 CAN COME TO NOTHING, 14.5% of the time against an Ashcroft who
  accepted, and that row fires once and is marked permanent. Previously a
  silent gap in a climactic beat. It is now a written outcome with read-aloud
  text: the reach fails, it wears the wrong face for a moment, the party learns
  what it is and cannot prove it, and the clock still turns to step 6.

Also: update-readme's count-word list ran out at sixteen, one guard after its
own comment warned about hardcoded lists going stale. It failed loudly rather
than silently, so it is an inconvenience and not a defect. Extended.

npm run check: 17 guards, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:50:03 +01:00
slaguru666andClaude Opus 5 8279cdfda8 R-272: guard the tail, and stop prose drifting from its artifacts
Three things Tim asked for together: measure the long end of a fight, make the
swing check bite on stale prose, and record in rules.mjs the coincidence that
hid an invented rule from two readers.

GUARD 15 — check-fight-tail, on tools/fight-tail.mjs.

Fourteen guards measured this scenario's combat and none could see a long
fight, because every one of them averages. The cap is the measurement here, so
the tool passes its own (400, not runFight's default 40) and --update refuses
to record anything reaching it. Verified by lowering it back to 40: the cut
loses 20 fights to the ceiling, the six-a-side 117, 107 of those ending
neither won nor wiped, and the recorder stops.

Two claims, both read from a fresh measurement rather than the baseline, so
--update cannot silence them. "The long end is twice the median" was rejected
as a claim because it is true of both encounters and so distinguishes nothing.
Instead: a fight past 15 rounds wipes the party materially more often than a
short one, and the SIX-A-SIDE fight is the longer one (median 14 vs 10.7) --
R-270 showing up as duration, since a disabled fighter keeps fighting 30
points down. The cut is shorter because it is decisive, not safer.

GUARD 16 — check-cited, on tools/check-cited.mjs.

check-firstblood and check-attackers catch the game changing; neither reads
the document. Re-record after a re-cast and the artifact updates, the guard
goes green, and the prose keeps printing the old number under a citation
saying where the new one lives. So citations are now machine-readable --
**40**<!-- cite: first-blood cut.swing --> -- and resolved on every build. 25
of them. It failed three times on its first runs, all real: a config keyed
"column" that the prose called "line", two figures rounded 32.3 -> 32, and a
vacuous pass on zero citations, now fatal in its own right.

It also refuses citation of unstable fields. fight-tail.longest may not reach
prose: same party, same seeds, same runs, and renaming a config moved it 71 ->
90 rounds, because seedFor derives the stream from the id. Across seven
labels -- median spread 0, p95 1, p99 3, longest 21. A sample maximum reads
like a bound and is a property of the label. EXPOSURE states p99 instead.
Same discipline on the deadlier ratio: 2.42 with seed spread 1.1, so the
document gives its direction and declines to quote its size.

RULES.MJS — one comment, no rule change.

Over resolveLocationHit: its two thresholds are unrelated and usually agree.
disabled is a fraction of the pool per location; majorWound is ceil(hp/2) and
feeds only dyingLimitFor; neither removes anyone from a fight, which is
conditionFor at 2 hit points or a destroyed head. At 10 hp, leg/abdomen/chest
capacity is 5 and majorWoundFor is 5, and those locations take 12 of 20 melee
results and 15 of 20 ranged -- so two readers reconstructed a rule that does
not exist, checked it against the log, and were confirmed by it. The note
says to test an arm, the only place the difference shows.

Guards verified to bite, not assumed: drift, re-cast, censoring, the longest
refusal, the rounding catch and the vacuous-pass catch were each forced and
each failed the build with the right guidance, then restored.

npm run check: 16 guards, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:20:25 +01:00
slaguru666andClaude Opus 5 0b86c5ffc3 R-268: guard the sentence that names a player's character
CLEAN GROUND prints that the four-player cut is three effective attackers and
that Ashcroft is not one. Nothing checked it, and a GM reads it aloud to decide
who a real player spends four hours being.

effective-attackers.mjs measures per attack, not per fight: counted per fight
Braithwaite leads on disables, but only because his armour buys him a third more
swings -- per attack he is the weakest of the three. Both rates are recorded and
only the per-attack one is reasoned from. Asserts exact drift, then the sentence:
three clear 5% of attacks disabling, one does not, and that one is Ashcroft.
Re-recording does not silence the claim check; verified in a worktree.

declared-cast.mjs holds the cast marker reading both scenario guards need, rather
than a copy in each. It was briefly named scenario-cast.mjs, which check-scenarios
sweeps into the scenario corpus -- its own example marker was read as a real cast.

update-readme dropped any guard that exited non-zero, so check-rollable vanished
from the README and the count word fell to thirteen while fourteen guards ran. A
missing line is now fatal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:56:01 +01:00
slaguru666andClaude Opus 5 ce1103ec0d R-267: a guarded home for the first disabling blow
Another session verified R-266's measurement, agreed the frame was better than
its own, and declined to publish it because the figure had no guarded lineage a
doubter could re-derive. It was right, and the fix costs 0.7s of build time --
which is the number I should have checked before calling it a reading tool and
not a guard.

first-blood.mjs gains --update/--check and is now both the reader and the
measurement of record; check-firstblood.mjs is a thin wrapper over it, the same
shape as check-bestiary. Compares the recorded figures exactly, then asserts only
what a page would claim: first blood lands within 5 points of even in the cut,
and its swing exceeds the six-a-side line's by more than both noises.

Recording it moved the swing from the scratch run's 43 to 40 against a
seed-to-seed spread of 2.1 -- high by more than its own noise, which is the
argument in miniature. update-readme's COUNT_WORD could not reach thirteen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:42:31 +01:00
slaguru666andClaude Opus 5 5dcaaf519a The simulator can be read now, not just totalled (R-263)
Every guard here reports a number per creature, and a number cannot say why a fight went
the way it did. That gap is what produced R-258 through R-260: each written from a side
harness built to watch a fight, each modelling something slightly different from the game,
two of the three wrong in ways no guard could catch because no guard was involved.

runFight takes an optional `say` sink. It reports what the simulator already decided — the
attack roll and its target, a defence and the penalty it was made at, the landing level
after a dodge downgrades it, damage against armour before and after armourAgainst, the
location, major wounds, disablement, death. It never touches the generator, so a narrated
fight and a silent one are the same fight; check-lethality and check-focus both still
match their baselines exactly with the hook in place.

tools/playthrough.mjs (npm run play) is its consumer, in the same commit deliberately: a
hook with no reader is the exact defect this project keeps finding in its own rules, and
adding one to the measurement pipeline with only a scratchpad file calling it would have
been committing the thing I have spent the session removing. Pack size comes from
focus-baseline.json so the fight you read is the fight check-focus measures; creatures
recorded as pinned are played solo.

Three redcaps, seed 20260913, same seed both ways. Spread fire: wiped in 10 rounds, 3
dead, and all three redcaps still standing — 39 hit points spread three ways so that none
of it finished anything. Focus fire: same opening, diverging at one target choice in round
1, party wins in 20 with two up. The +13.2 points check-focus records, seen once instead
of averaged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:30:14 +01:00
slaguru666andClaude Opus 5 1d915c55cf check-focus: the focus-fire claim is an artifact, not a sentence (R-261)
R-258 put a tactic on a GM-facing page from an unguarded harness, R-259 found it worth
nothing, R-260 found the replacement right for the wrong reason. Three corrections, each
landing as prose with nothing checking it — the arrangement that let ddc4f99 describe a
game nobody was playing.

tools/focus-baseline.json records, per creature, the pack size and the spread-fire and
focus-fire win rates. check-focus re-measures all of it on every build and compares
exactly, with the party, seeds, run count and seeding scheme recorded alongside so numbers
taken under different conditions are refused rather than compared. The bestiary READS the
artifact instead of restating it, and check-bestiary refuses a page that has fallen behind
it. Page, guard and simulator cannot disagree.

The pack size is recorded rather than re-chosen: a fight at 0% or 100% cannot show an
effect, and a guard that picked again each run would let a changed creature move quietly
to a different question and pass. 31 of 47 creatures land in the measurable band; the
other 16 are recorded as pinned, with the rate that pinned them.

It guards the claim as well as the numbers. The page says focus fire helps in every fight
in doubt; check-focus fails if any row's gain reaches zero or stops clearing its own
noise. That failure means rewrite the page, not re-record the baseline.

The run count was chosen by evidence. 1000 x 3 seeds costs 8.5s and takes the suite from
2.5s to 13.7s. I tried 500 to halve it and the claim-check failed — at 500 runs one row
no longer clears its noise, so "without exception" is not supported by that much
sampling. Recording twice at 1000 gives byte-identical files.

Negative-tested four ways, all firing: a creature quietly made nimbler (redcap dodge
75 -> 85, spread 40.6% -> 27.6%), a baseline under different seeds, a hand-edited page,
and the claim failing at 500 runs.

Guard eleven (check-rollable) arrived from another session mid-build; this is twelve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:13:28 +01:00
slaguru666andClaude Opus 5 555fa761c0 check-rollable: guard eleven, because a real skill is not the same as a route
check-scenarios resolves every skill a scenario NAMES against the catalogue, which
is a different question from whether anybody present can roll it. CLEAN GROUND
shipped four commits with three clues gated on Track (base 10), Navigate (base 10)
and Science (Botany) — base 1, so the 1% floor was the whole of it — and every
guard passed, because all three are perfectly real skills. A desk playtest found
it by auditing the acts against the sheets the scenario casts. That audit is
mechanical, so it belongs in the build.

Two tiers. Corpus-wide, some roster agent must reach VIABLE for every skill any
scenario names. Per scenario, a document that DECLARES its cast is held to that
cast instead, and that is the tier that catches this defect class.

The cast is declared rather than inferred, and that is the interesting part. The
first version scraped pc_ keys out of the prose and swept up the substitutes named
in CLEAN GROUND's player-count scaling — a cast of nine instead of six, which put
Sandoval and his Track 35 in scope and made the guard pass the very bug it was
written for. A guard that guesses the cast is worse than none, because it reports
success. CLEAN GROUND now carries a cast comment and the guard reads it off the raw
text, since scenarioText strips HTML comments.

Verified load-bearing rather than assumed: re-injecting the original Track tag into
the real CLEAN_GROUND.md fails the guard, naming the skill, the base chance and the
declared cast. Worth recording that the corpus-wide tier would never have caught
it — Lindqvist trains Botany at 40, so it is rollable by the roster and simply not
by the six who were cast. The first fixture test passed for that reason and misled
me; only the declared-cast tier finds this.

VIABLE is 25 and is justified, not picked: it is the commonest base chance in the
catalogue, what an untrained agent brings to Spot, Listen or Brawl, so it is the
level the game itself treats as worth attempting. Below it a clue is not gated, it
is buried.

check-scenarios exports its tag parser rather than growing a second copy, behind
the invokedDirectly pattern simulate.mjs already uses; a duplicated parser is
exactly what this repository's one standing law forbids. update-readme then caught
me fairly — it cross-checks the advertised guard list against npm run check — so
the guard is registered there too and the README advertises eleven in all three
places.

No REVIEW_LOG entry: the log is clean at R-260 and is being appended to every few
minutes by concurrent work, so the end of that file is the likeliest place to
collide. Left for whoever next touches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:10:00 +01:00
slaguru666andClaude Opus 5 f932a79121 The firing order, in the bestiary, generated (R-258)
DEFENCE_STEP is -30, so a creature's Dodge is not a percentage it has for a round — it is
one it has once or twice. The median dodge in the book is 45% and goes 45 -> 15 -> 1
inside a single round. No player-facing document said so, which hid a real decision:
what order the party shoots in.

The bestiary now has a section on it. Only a landing hit spends a defence, so a miss
strips nothing; each landing hit makes the next attack of that round easier; therefore
the cheapest reliable weapon fires first and the one you most need to land fires last —
against the supporter, the weapon the borough does not own.

Everything it states is generated from DEFENCE_STEP, resolveBands and the statblocks: the
step, the ladder, the median, the spread (13 of 47 creatures stripped by one landing hit,
25 by two, 9 by three) and the list of the hardest to strip. Negative-tested by setting
the step to -15 and -20; the page rewrites itself both times. No win rates are published:
the ordering measurements come from a table-side harness that ignores wound penalties,
bleeding and dying, so the page states the mechanism and quotes no percentages.

My first draft used the dodgiest creature in the book as the worked example — 80%, three
landing hits to strip — directly beneath a claim that the first attack of the round eats
the dodge. True of a modest dodge, false of a good one, and both were on the same page.

tools/bestiary.mjs --check has existed since the document did, with the comment "(for the
guards)", and no guard ever ran it: a check written and wired to nothing, the same shape
as every rule found computed and never read. That was tolerable while the page restated
statblocks and is not now that it derives from rules — change DEFENCE_STEP and the
document lies to a GM. check-bestiary.mjs is the tenth guard, negative-tested both ways
it can fail: a rule change that staler the page, and a hand-edit of the page.
update-readme flagged it as "run by the build, not advertised" before I listed it, and
the README's count word went from nine to ten by itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:51:04 +01:00
slaguru666andClaude Opus 5 ddc4f99ff4 Hit locations reach ordinary combat, and creatures get a body
THE DEFECT. locationFor had ONE caller in the engine, hitLocationRoll, and
hitLocationRoll was called from fall, burn, detonate and burstAttack. Falling,
fire, explosions and automatic fire. Nothing else. rollWeaponDamage rolled dice,
printed a number and returned it, so the chat Damage button, the character
sheet's damage button and the item sheet's all stopped there.

A grenade knew which arm it took off. A sword did not.

Every individual piece was already correct - the tables, per-location armour and
coverage, disable and destroy thresholds, Major Wound, dying, death, the figure
on the sheet. They simply were not joined up, which is why this reads as a wiring
change rather than a new subsystem.

- rollWeaponDamage now carries through to hitLocationRoll: it rolls the location,
  applies that location's armour, applies the wound, and handles the rest through
  the machinery that already owned it. Nothing here re-implements any of that
- locationModeFor picks the melee or ranged column from the weapon's class. The
  two columns have always differed and nothing was choosing between them
- with no target it does exactly what it did before and SAYS SO on a card, rather
  than failing quietly
- burstAttack opts out with locate:false - it rolls its own location per round,
  and letting both fire would have applied every round of a burst twice

BODY PLANS. Two new tables: quadruped (four legs, fore/hindquarters, neck, a head
that is hard to reach) and winged (that, plus wings). Forequarters are the vital.
DISABLE EITHER WING AND IT CANNOT FLY, which is the fight a party can win.
Barghest, Kelpie and Church grim were all species "baseline" - a black dog, a
horse and a grave-hound, each hit-located with two arms and a chest. Fixed. Added
"The supporter", a winged heraldic beast, so the new table is exercised by real
content instead of existing unused.

Body plan is a NEW FIELD, not a species. A bear is not a playable people with
characteristic dice and talents, so creature specs take bodyPlan and the playable
species list is untouched.

THE NPC SHEET had no hit locations at all - 102 lines against the character
sheet's 627 - while every NPC in the game had them derived and wired to
consequences. It now carries the body-plan selector, the figure, the per-location
table, the condition and a GROUNDED flag. The sheet class already extended the
character sheet, so it already HAD the context; the template never used it.

FOUND IN PASSING, by the new guard: kind "body" - the abdomen, the trunk, the
hindquarters - did nothing whatever. Its printed effect has always promised "-30%
to all Physical actions and bleeding 1 hit point per round until First Aid" and
the code delivered none of it, on every body plan including the humanoid default.
It now bleeds 1, or 2 destroyed, exactly as bodyX says.

NEW GUARD: check-anatomy. Every body plan must cover 1-20 exactly once in BOTH
modes, every location must be drawn and every drawn shape must be a location, and
every kind must be one locationEffectsFor acts on, has effect text for, and has a
destruction outcome. Verified it fails on a d20 gap, a shapeless limb and an
unhandled kind before wiring it in. Ninth guard.

VERIFIED IN A RUNNING FOUNDRY, not just unit-tested: melee bar reads the Melee
column and the rifle reads Ranged on the same creature; forequarters graded as a
vital hit; a maxed wing set flightLost and the sheet said GROUNDED; the real chat
Damage button rolled 1D20 15, found the right foreleg, applied 12 through armour,
disabled the leg and took the dog from 16 to 4; and with nothing targeted nothing
was touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 15:46:11 +01:00
slaguru666andClaude Opus 5 339d5dd8dc test runs the guards through bun, not through npm
`test` was "npm run check", so reaching bun meant shelling out to npm
first. It is "bun run check" now — one runner, and npm is out of the
path.

Not the eight-guard chain copied into `test`: that is the same shape of
mistake the rule formulas made before rules.mjs existed, a list written
twice and changed once. `check` stays the only place the guards are
named.

Verified: npm test exit 0, bun run test exit 0, bun run check exit 0.

Worth knowing: bare `bun test` does NOT run this. It is bun's own test
runner, it matches no files here, and it exits with an error. The ways
in are `npm test`, `bun run test` and `bun run check` — which is what
the README already tells you to use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:44:16 +01:00
slaguru666andClaude Opus 5 54a759e057 Run the guards under bun, including the two new ones
Rebased onto the creature-forge work, which had added check-creatures
and check-lethality to the chain and had `check` back on node. Taking
either side whole would have been wrong: the node side loses bun, the
bun side silently drops the two new guards. So the runner is bun and the
guard list is upstream's, all eight of them.

Only `check` and `readme` move. build, icons, portraits, bestiary, mj
and simulate stay on node — they were never part of this change, and
build writes LevelDB packs through classic-level.

Verified before committing: each guard run individually under bun, then
`npm run check` end to end, exit 0.

  check-rules       103 rules, 33 files
  check-kits        OK
  check-lang        995 keys, no collisions
  check-templates   19 templates compile
  check-behaviour   67 behavioural tests
  check-scenarios   9 files, 62 mechanic tags
  check-creatures   101 actor specs across 8 sources
  check-lethality   46 creatures, none drifted more than 6 points

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 17:41:27 +01:00
slaguru666andClaude Opus 5 322389bc8a A bestiary a GM can actually read, and the defect it found immediately
Forty-six creatures and no way to look at them. They live in content.mjs, which is
source code, and in the compendium, which shows one actor at a time behind two clicks.
A GM prepping a case can browse neither, so in practice the bestiary was whatever the
GM happened to remember writing.

docs/BESTIARY.md is generated, never hand-written, for the reason the rules journal is
generated from rules.mjs: a reference that can disagree with the thing it references is
worse than no reference. It opens with a one-page table sorted by how much of a party
each creature puts on the floor - the part that gets printed - then an entry each with
characteristics, attack, the POWER line pulled out of the tactics, and the measured
figure read straight from the lethality baseline. Every number is derived, so the
document cannot drift, and check-lethality already refuses the build if the measurements
move.

IT FOUND SOMETHING IN ITS FIRST RUN. The generic heavy natural attack is keyed `grasp`
and was NAMED "Vesh grasp limb". Twenty-two creatures carry it and five are Vesh, so the
at-a-glance table cheerfully printed "Vesh grasp limb" against a black dog, a kelpie, a
church grim and an apple tree. Renamed to "Grasping limb"; the key is untouched, so
every kit reference and every built pack still resolves. That defect was invisible for
as long as nobody could see the bestiary in one place, which is the argument for the
document.

The caveats are given more room than the numbers, deliberately. The measured column is
SOLO against ONE frozen party, and the document says so three times: encounter lethality
moves with how many there are and, much harder, with who the players brought - the same
fight runs 0% to 100% across party choice. It points at --spread rather than pretending
a single figure is an answer.

Also records the real remaining gap: 21 creatures fight with nothing but their own
bodies and still carry human anatomy, so the barghest can be shot in the left arm. The
machinery to fix that is built and measured; what is missing is anatomy tables for the
shapes they actually are - a quadruped, an immobile object, a swarm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 16:43:39 +01:00
slaguru666andClaude Opus 5 4b718595fe check-lethality: guard eight, and the README stops advertising a subset
Seven guards checked that content is WELL FORMED. None checked what it DOES, so a
change to a damage modifier, a hit-point formula or the armour value on a service vest
could double a creature's lethality without touching one line of that creature - and
nothing in the build would notice, because the creature did not change.

Built as a regression test rather than the hand-declared bands the creature-forge plan
described. Bands are the wrong shape here: declaring "keepers: dangerous" across
forty-six creatures means inventing forty-six judgements, and after --spread it is
clear the interesting question is not "is this dangerous" - that has no single answer -
but "is this the same as it was". A recorded baseline answers exactly that, needs no
authoring, and cannot be argued with.

Every creature is measured solo against a FROZEN party of four, two armed postings and
two trades, at a fixed seed, so the only thing that can move a number is a change to
the rules or to the creature. Tolerances are 6 points of wipe rate and 0.35 agents,
which is outside the noise floor of 200 runs - a guard that cries wolf gets deleted.

Verified by tampering: told the baseline the nuckelavee was harmless and the guard
caught it at +16.5 points and +1.67 agents down, exit 1. Runs in half a second, so it
joins the pre-build checks rather than being something to remember to run.

Also: the README's guard block was a hardcoded list of four while the suite was seven.
It had silently stopped mentioning every guard added after it was written, including
check-creatures and check-scenarios. Now all eight, and the prose says eight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 21:20:09 +01:00
slaguru666andClaude Opus 5 9ddea43720 make-portraits: every creature gets art it cannot disagree with
Sixty of the seventy-six actors had no portrait. The sixteen that did are the
hand-made duty roster; the whole bestiary and every scenario NPC fell back to a
generic silhouette.

The point is not that this is free. A portrait composed from anatomy.mjs cannot
contradict the statblock, because it is drawn from the numbers the hit-location roll
uses: silhouette from BODY_ART, height from SIZ, build from STR against SIZ, plating
from naturalArmour, a mark for the primary weapon's class. A Cadence would get five
bodies because its table has five. Nothing has to be kept in step.

Colour carries what a GM wants off a thumbnail — whether the thing is a person,
folklore, recovered machinery, or the far side. That reads the shouted category the
role lines already carry (FOLKLORE, HORROR, FORWARD), which nothing was using, and
falls back to structure for the ones with no tag.

build-packs resolves portraits by stem and extension rather than an exact .webp
filename, so a creature gains art without an edit to content.mjs and a hand-made
raster always beats the generated diagram. A spec that NAMES a portrait and has none
is still a hard build failure.

These read as diagrammatic field-guide plates and will not match the Midjourney agent
portraits. That split is deliberate: the agents are the cast, the bestiary is case
material.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:32:53 +01:00
slaguru666andClaude Opus 5 2c547efe72 simulate: a lethality harness that uses the real rules
THROUGH TRAIN publishes "MEASURED, 400 runs each" and a 24% wipe rate; the starter
publishes its own table. Nothing in this repository could produce either number, so both
were claims rather than results and neither could be re-checked after a rule moved.

The harness knows no rules of its own. Everything comes out of rules.mjs, because a
harness with its own copy of the combat loop measures a game nobody is playing and
drifts silently the first time a rule changes. Two things had to move for that to be
possible:

  applyDifficulty, resolveBands and gradeRoll -> rules.mjs. The whole of d100 resolution
  lived in ringbrp.mjs, which node cannot import. All three are pure, so they moved and
  the engine re-exports them; no caller changes. check-rules then refused them for having
  no spot-check, so they now have fifteen, covering the 1% floor, the 96-99 rule and
  00-always-fumbles. They had none in their entire life inside the engine.

  expandFromRegister -> tools/expand-spec.mjs. A roster agent is written as a job and a
  rank and carries no skills or kit; everything it can do is derived at build time by a
  function locked inside build-packs.mjs, which cannot be imported because importing it
  runs the build. A simulated agent expanded by a second copy of that logic is not the
  agent that gets packed. Moved verbatim; both sides import one copy.

Deterministic throughout: same creature, party and seed, same numbers, which is what
lets a published table be verified rather than remembered.

What it finds: for the Act Three standoff the seed barely matters (0.5-2.8% wipe across
five seeds, eight rounds throughout) and party composition dominates everything. Four
support agents against the six miners wipe 92.8% of the time; swap in the heavy and the
warden and it is 0.3%. The published 24% is not a property of the encounter, and no
single figure can be.

Models Reaction re-rolled each round, banding, graded defences, the stacking defence
penalty, damage by band, major wounds and the dying clock. Does NOT yet model hit
locations, bleeding, panic, cover, range bands or fire modes, all of which make a losing
fight worse. Read the output as a floor, never a ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:10:05 +01:00
slaguru666andClaude Opus 5 c3002e93c9 check-creatures: validate every actor spec, and fix the vesh talents it found
Nothing validated creature specs. check-kits validates postings and check-scenarios
validates prose; buildActor throws on an unknown skill or item key and is silent about
everything else, so species, marksmanship style, characteristic blocks, species-locked
talents and misspelt field names could all be wrong and still build, pack and play.
Every packed actor once shipped as species "hominid" and the whole cast was hit-located
as something the rules do not contain.

tools/creature-schema.mjs derives every allowed value from the catalogues rather than
retyping them: skills from SKILL_CATALOGUE, kit from the item catalogues, species from
SPECIES, stats from CHARACTERISTIC_DICE, and the two marksmanship styles out of the
language file. It reports every problem with a spec rather than dying on the first, and
names the nearest real key. It also reports fields buildActor never reads, because
"armours:" is not an error, it is an actor that silently equips nothing.

On its first run it found six baseline humans carrying vesh biology. "unblinking" is
cat: species, species: vesh, and its own rule text reads "A Vesh has no face to read".
Out of Step is reclassified as anomalous with a Coherence cost, which is what every
talent in that category has and what the Anchor who is three years older than his
birthday says always wanted to be; the other five are swapped to agency and field
talents that fit them.

Not fixed here: the root cause is upstream in postings.mjs, where twelve posting talent
pools still offer unblinking, distributed or quorum to any agent. Picking replacements
across those is a design decision, not a mechanical one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:09:43 +01:00
slaguru666andClaude Opus 5 973b4a53aa THROUGH TRAIN rewritten into five acts
Act One is now an induction at the Bureau, Act Two puts the players through the
crossing and makes them find the train, Act Three is a town-side obstacle, Act
Four is the boarding, Act Five is the four minutes. 4h00 in five acts, was 3h30
in three.

The restructure earns one thing the old shape could not: THE CROSSING OPENS WHERE
THE RECORD SAYS THE LINE IS, which is an empty bench of scrub on the wrong side of
the wash. The agents arrive where the paperwork put the railway, not where the
railway is, so the fraud is the ground under their feet and finding the train is
real work. Registry's file has this wrong and Bewley believes the file.

- Act Three: Rance Doyle and five unpaid miners take the agents for company
  detectives, and they are not wrong to. Resolves on what the players SAY - no
  roll makes it go away, no roll makes it a fight. Losing costs them the 1881
  issue, not their lives. His brother Tom is aboard and dies at minute two of
  every pass, which is the wiring between the acts
- new cast: Bewley, Prosser, Rance Doyle, Shard, Tom Doyle, Cordera man
- new handouts: the company notice, and the six names in Ambrose's hand
- 8 GM pages, 10 handouts, 15 actors, 13 plates

MEASURED, 400 runs each: the Act Three standoff is 0.4/4 hurt from cover and
1.25/4 in the open, both ending early - but FOUGHT TO A FINISH it puts 1.75 of 4
down and wipes the party in 24% of runs. The early stop is what makes the act
survivable, not the miners being weak. My first draft of that table was guessed
and wrong in all three rows.

FOUND AND FIXED: there is no Ride skill in this game and never has been. The
boarding set-piece has asked for "[CUS: Ride, then Athletics]" since it was
written, through a playtest, which is an instruction to roll a thing nobody has.
Horses are now handled by a local and the agent rolls Athletics.

NEW GUARD: check-scenarios resolves every [CUS: Skill] tag in every scenario
against SKILL_CATALOGUE and fails the build on a skill that does not exist.
Verified it fails on the real bug before wiring it in. Sixth guard.

NOTE: the three new plates (Bureau, north bench, standoff) are placeholders in
this commit - Midjourney is still rendering them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 19:25:20 +01:00
slaguru666andClaude Opus 5 ab619f4b73 Remove the dice textures and the whole Dice So Nice path
Deleted: the seven PNGs in dice/, tools/make-dice-textures.mjs, the `npm run
dice` script, the RB_DICE colourset and texture tables, diceSetFor, the
diceSoNiceReady hook, and rbDressDice — which was tagging ten roll sites with
coloursets that no longer existed. 99 lines of module, plus the assets.
README and DEPLOY updated; they still advertised all of it.

Recoverable with `git revert` if it is ever wanted back.

This also explains the 01-that-was-12. The RESULT was never wrong: skillRoll
rolls one 1d100 and grades on roll.total, and the chat card derives the tens and
units it prints from that same total. Checked all 100 values of percentileFaces
and 60 live rolls — the printed faces reconstruct the graded number every time,
with no mismatches.

What disagreed was the ANIMATION. The custom addDicePreset labels registered for
d100 and d10 remapped which physical face means what, so Dice So Nice could land
showing 01 while the roll behind it was 12. Nothing downstream ever read the
animated faces, so no roll was ever adjudicated on the wrong number — but a
table watching the dice had every reason to believe otherwise, which is its own
kind of wrong. Disabling the skins in 1.0.6 already removed it; deleting them
makes it permanent.

Verified in a running Foundry after removal: skill roll, weapon damage, hit
location, major wound and Coherence all resolve and post their cards, no errors,
and game.ringbrp.diceSetFor is gone from the API.

Version 1.0.7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:45:52 +01:00
slaguru666andClaude Opus 5 02ec47a351 Fix every NPC sheet, which has never opened
npc-sheet.hbs line 43 read

    {{#if (lookup ../skillsByCategory key).length}}

and Handlebars cannot read a property off a subexpression. That is a PARSE
error, so the template never compiled, so every NPC sheet in the system threw
on click — Natives & Threats, the starter's enemy roster, and the whole LAST
ADMISSION cast. #if already treats an empty array as falsy, so the .length was
never doing anything in the first place.

It was invisible to all four existing guards, and the reason is worth writing
down: check-rules reads .mjs, check-behaviour says in its own header that it
cannot reach sheets, and every play-through in this project has driven the
system through its API rather than by clicking. Nobody had ever opened an NPC
sheet, including me, in any of the sessions that "played through" scenarios
full of NPCs.

So: check-templates.mjs, a fifth guard, compiling every .hbs in about a tenth
of a second, wired into the build ahead of check-behaviour. It also names the
specific shape — a property read off a subexpression — because the raw parse
error says only where. Verified it bites by reintroducing the bug. Handlebars
is a devDependency now.

Opening the sheet then showed a second thing nobody had seen: the NPC skill
rows carry four children and were using the character sheet's seven-column
grid, so the name landed in a 13px column and the whole list overlapped
itself. The NPC row now has its own four-column template.

Verified by opening all six LAST ADMISSION cast sheets with an error hook
attached: six rendered, no errors, and Old Marrow reads HP 20/20, major wound
10, armour 2, Brawl 60%, Melee Weapon (Polearm) 60%.

Version 1.0.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:42:05 +01:00
slaguru666andClaude Opus 5 7f8355f0d0 Add the player primer and cheat sheet, and brand every journal
Ten pages a player reads before session one, in their own compendium at OBSERVER
ownership: nine of setting, one cheat sheet.

The draft called the organisation "the Bureau"; everything else in the game calls it
the department or the agency, so that is what it is called. Thin areas filled rather
than padded: the seven character principles as a list, the real Coherence band table
instead of a paragraph describing one, and "what continuity is not" pulled out of the
middle of a page because it is the most important sentence for a new player.

The cheat sheet's numbers are computed from rules.mjs and lang/en.json at build time.
A hand-typed one would have been the sixth time this project shipped a number that
disagreed with the code, and the one the players were holding.

R-250: the design tokens were never on :root. Journals rendered as default parchment,
and four things were wrong in sequence — v14's hook is renderJournalEntrySheet; its
`element` is the node, so `[0]` returns the first CHILD and put the class on a header
button; there is no .journal-page-content in v14; and then the real one: with the
selector matching, font-size, line-height and padding all applied while every colour
did not, because the palette is declared on .ringbrp-sheet/.ringbrp-chat/.ringbrp-window
and never on :root. A journal inherited no tokens, so every var() silently became
nothing. A rule that HALF applies is not a specificity problem — I spent three attempts
treating it as one, including a version bump on a cache theory that was wrong.

Branded: primer, rulebook, starter GM text and handouts. Scoped by flag, so other
modules' journals are untouched. Version 0.9.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 10:19:41 +01:00
slaguru666andClaude Opus 5 43508259ac Consolidation pass after a Codex review
Four real defects, each verified against the source before acting on it.

Trade expertise was coupled to agency rank. A probationary burglar came out at 35-50%
and a veteran at 65-80%, as though fifteen years of picking locks were something the
department conferred. TRADE_BANDS is now fixed at 50-65/30-50 at every rank, and
INDUCTION scales with service instead (20-32 probationary to 45-60 veteran) because
that half genuinely is the department's. The two axes were the wrong way round. This
is the thing I flagged myself after PAPER HARBOUR and then left alone — flagging a
defect is not the same as fixing it.

Bonus points were sprayed across every skill in the game. grantFullSkillList runs
immediately before the loop and puts all fifty-nine skills on the sheet, and the loop
picked from actor.items, so a veteran's ninety points landed anywhere. The comment two
hundred lines above has always said "over the core skills"; the code never did.

"Random species" passed "baseline" into generators that support random perfectly well.

No behavioural tests. The strongest point in the review: the three guards are static
and none of them can tell whether a rule is READ, which is the only kind of defect this
project has ever shipped. tools/check-behaviour.mjs now runs 28 deterministic tests,
gated into the build, covering R-231/232/243/245/246, graded defences and the lamp
rule. Proved it bites by re-breaking R-246.

The register moved to postings.mjs, a Foundry-neutral module the engine, rulebook,
guards and tests all import normally — they were previously pulled out of the engine
with a regex and eval, which worked and was a trap. My first version of that used
`export … from`, which re-exports without local bindings, so ROLES was undefined and
the system threw on init: node --check passes that, loading it in Foundry does not.

Also: the contact doc comment still said "only OFFER moves standing"; check-kits
accepted mas/app, which are the DISPLAY names of siz/cha, so a posting declaring one
would have passed and produced an undefined characteristic; package.json disagreed with
the manifest; README counts were three revisions stale and are now generated at build.

Deferred with reasons: splitting the 6,700-line engine into modules. Right, and a
multi-session refactor whose risk is exactly the silent breakage demonstrated above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 22:57:20 +01:00
slaguru666andClaude Opus 5 1734612c06 v0.7.0 — vehicles, Borrowed Authority, and a door for the case generator
Since 0.6.1:

- VEHICLES as their own Item type: crew, seats, cargo, hull, armour, range,
  terrain and the skill that works them, with speed as a band. 14 of them, from
  the pool car and the Custodian van to a sealed crawler and the carriage that is
  already parked outside wherever you were going to go.
- STORES roughly doubled to 118 gear items across four eras — standing issue and
  paperwork, transport stores, and the forward stores Registry will not hand a
  probationary officer. Catalogue now 175 items, 231 icons.
- Agents leave creation properly equipped: standing issue, the posting's kit,
  what seniority adds, and one personal effect that is theirs and not the
  department's.
- All 25 postings now describe themselves in the creation dialog.
- BORROWED AUTHORITY, the access subsystem. No hacking skill and nothing to break
  into: access is a named credential with a scope, a Trust that erodes, and red
  flags describing how its real owner behaves. Inside scope there is no roll. Two
  tiers beyond is impossible at any skill. Coherence widens a record, and pushing
  one past what its Trust will carry merges your file with theirs.
- Defences graded by level, and damage now rolls at the level a blow landed at.
- Dodge's DEX x 2 base is finally evaluated; it read 0% for most postings.
- Brawl is a fallback rather than a peer of the weapon in hand.
- check-lang guard: one key that was both a value and a branch silently
  un-localised the entire system.
- The case generator is reachable from the Actor Directory, and no longer puts
  every character in the world on a new case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:23:52 +01:00
slaguru666andClaude Opus 5 d0144f744e The Custodians v0.6.1 — full system
A Foundry VTT game system: Basic Roleplaying d100 in a drab British agency that
deals with crossings, where reality keeps a record of what belongs where and can be
made to get it wrong.

Contents:
- rules.mjs as the single authority for every rule formula, with 40 rules and 132
  spot-checked values. Nothing may restate a formula it defines.
- 25 postings, each with a key characteristic, trained skills, talents and its own
  kit; a 175-item catalogue across four eras; 231 generated icons.
- Reaction Lanes initiative, re-rolled every round including the first.
- Coherence, and the Quiet Arts, which only ever arrive from a crossing gone wrong.
- Graded defences: only a critical stops a critical.
- Borrowed Authority — access as a record rather than a hacking roll.
- A case generator, and a rules journal generated from rules.mjs so the book cannot
  drift from the code.

Built compendia are committed so a remote Foundry needs only the files.
node_modules is not; `npm install` restores it for rebuilding content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:09:07 +01:00