Commit Graph
24 Commits
Author SHA1 Message Date
slaguru666andClaude Opus 5 d0db7954bc check-figures: the guard's body belongs behind the entry-point check
The fold exported scanDocument for the string tests, and importing it also ran
the guard. check-behaviour imports that module, so a figure defect called
process.exit(1) inside check-behaviour: it reported check-figures' failure under
its own name having run zero of its 108 tests.

The build went red, which is why this was survivable, but it went red in the
wrong place and every behavioural test was silently not running while appearing
to. A guard that stops another guard from running, and cannot say so, is the
worst version of the fault this file exists to catch.

Body now sits behind import.meta.main, the idiom step5-split.mjs already uses.
Importing yields the four readers and nothing else. Tested by spawning a fresh
process, because the property is "importing has no effect" and a source
assertion would pass on a file that grew a second side effect elsewhere.

Reverting the check fails exactly one test. With a defect planted,
check-behaviour runs 108 green and check-figures fails in its own slot.

Mine, introduced by R-307. R-309.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:39:46 +01:00
slaguru666andClaude Opus 5 b946a28312 check-figures: the fold — one guard, three readers, and the boundary between them
check-unmarked is gone and check-figures holds all of it. Three readers, each
authoritative where the corpus gives it authority: a named phrasing anywhere,
a column header inside tables, and bold in prose only.

The boundary is the finding rather than the union. Bold is a publication mark
in prose and an emphasis mark in a table — nine measurement columns mix bold
with plain, all correctly cited, and in the mixed-force table the two bolded
rows are exactly the two the prose underneath singles out. So the bold reader
stays silent in a table and the header rules there alone. Neither guard could
have found this alone: each had half the evidence and read it as the other's bug.

Union of both word lists, because each had a gap the other covered — wipes and
runs. A proposal to drop runs? was made and withdrawn; it would have dropped the
p99 column, which is R-299's own defect committed a second time.

Exclusions test cells, never header words: prose, denominator, duration. No
threshold rule touches a header, or "Past 15 rounds" loses two cited figures.

Two integration defects, both caught by the ported tests: the readers stopped at
different ends of one figure so the dedupe missed it (identity is where the
number starts), and blanking prose cells for every reader dropped twelve real
figures out of THROUGH_TRAIN's ratchet (the prose rule belongs to the column
pass alone).

Kept from R-299: opt-in rule, ratchet and its leave-the-loose-set branch, the
UNCITEABLE check, NOT_A_MEASUREMENT, comments blanked not stripped. Added
waivers, with an empty reason fatal and every waiver printed on a green build.

21 guards, 107 behavioural tests, 139 figures — 115 marked, 6 waived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:29:14 +01:00
slaguru666andClaude Opus 5 960fb6be27 check-unmarked: the figures a vocabulary cannot name, and a repair to HEAD
eead663 committed a document citing `step5 unsettledFactor` while the module
that would export it was still uncommitted in another session's working tree,
so `check-cited` fails on a fresh checkout of HEAD. step5-split.mjs is included
here to repair that; the key derives Act Four's 1.29 from the raw proportions,
which is the figure R-294 exists about.

check-figures (R-299) reports OK on the "24" it was written to catch. Its own
entry names why: the eight shapes were read off figures that are already cited,
so the vocabulary is learned from the marked figures and cannot contain the
phrasing of the one nobody marked. Line 1272 is a table cell whose measurement
status lives in the header two rows above it, and figuresIn reads one line.

check-unmarked reads structure instead of vocabulary: a bold percentage or
decimal in an opted-in document, and a bold number in a table whose header row
names a measurement. Twelve unmarked figures in CLEAN GROUND; eleven correct
and unheld, one the stale 24 that line 1142 had been citing correctly as 25 for
130 lines. Waivers carry a reason, an empty one is fatal, and every waiver
prints on a green build.

Two guards now cover one question, which is one too many. The right end state
is the structural classes folded in beside check-figures' SHAPES, keeping its
opt-in rule and ratchet. That fold is offered to its author rather than taken.

Twelve string tests on unmarkedIn (88 -> 100), both halves mutation-checked.
Twenty-two guards green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:56:38 +01:00
slaguru666andClaude Opus 5 fb6e2cf321 R-294: check-cited resolves derived figures against the rule, not a stored copy
c0 argued a baseline for the step-5 split would be a cache of the rule and a guard
over it would mostly assert that arithmetic has not changed. Right objection,
wrong conclusion: do not store it. ARTIFACTS now takes a { derive } entry as well
as a file path -- enumerated on this build, nothing stored, and no --update able
to silence a real disagreement between the document and the game.

tools/step5-split.mjs enumerates all 10,000 pairs through opposedContestFor with
its targets derived: 55 and 60 are the lowest POWx5 in the six and in the cut, 42
is applyDifficulty(85, "difficult"). Two different rules produce those three
numbers -- the accepted row is a named exception, not the lowest of anything -- and
a test fails if anyone unifies them. A tie in "the lowest POW in the room" is
fatal rather than silently resolved; it fired for real in testing.

Proved four ways, including raising Okonkwo's POW to 13: the lowest moves to
Braithwaite, refused.held goes 48.1 to 52.4, and the citation that was correct a
moment earlier fails. The figure follows the rule.

Landed unused -- the markers are c0's to place in their own file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:25:49 +01:00
slaguru666andClaude Opus 5 daf77f4aac R-293: a desk playtest is a record, not a scenario with rolls in it
Counting the readers that had broken on their own format turned up one nothing had
caught. docs/scenarios holds scenarios, eight playtest records and two art prompt
sheets, and every guard treated all three as scenarios -- harmless for citations
and skill spellings, false for reachability. Six quotations across passes 4, 6 and
7 were checked as live rolls, so a record of a session already played could fail
the build over a skill nobody can reach. Planting Science (Physics) in pass 4 fails
before the split and passes after; the same skill in CLEAN_GROUND still fails.

Classified by the document's own H1, not its filename, because tools/scenario-*
naming is what swept a tools file into this corpus in R-268. An unclassified
document is fatal: an allowlist that silently drops what it does not recognise
would take a new scenario out of reachability checking on the day it was written.

89 rolls across 18 files becomes 83 across 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:05:03 +01:00
slaguru666andClaude Opus 5 ed851b76e3 R-291: assert that a tag one reader accepts stays visible to the other
c0 asked whether R-290's widening of tagsIn reaches past outcome-coverage's loose
counter, which would zero its unparsed count for the wrong reason and retire a
canary. It does not -- 22 beats from each reader on CLEAN_GROUND.md, nothing that
tagsIn accepts invisible to the other guard -- but "does not today" decays quietly,
so it is now a test importing both real functions rather than a copy of either
pattern. Mutation-checked: widening tagsIn to accept [CUS Spot] turns it red.

Measuring it found something the question did not ask about, recorded in the log:
for [cus: ...], [CUS : ...] and [ CUS: ...] the two guards disagree -- tagsIn reads
the tag, beatLikeIn calls it unparseable -- because beatsIn's strict half kept the
case-sensitive literal. The build stops, which is the safe direction, but names the
wrong thing. outcome-coverage.mjs is c0's file and the call is theirs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:50:05 +01:00
slaguru666andClaude Opus 5 2cf9721fa3 R-289: test the cast reader on strings, and fix the spelling that found
Both earlier cast defects were shown by editing CLEAN_GROUND.md in the shared tree,
which is how this session came within a git checkout of c0's uncommitted work and
is also the weaker test. Seven cases now live in check-behaviour as strings.

Writing them found a live one: "<!-- cast : ... -->", one space before the colon,
matched nothing -- not an empty declaration R-288 would refuse but no declaration
at all, so check-rollable widened to the whole duty roster and printed its usual OK
line. check-cited's three spellings again. The reader now takes cast\s*:.

castLikeIn adds the asymmetric half: anything comment-shaped containing cast\w* the
reader did not consume is named by check-rollable, so a spelling nobody anticipated
fails the build instead of silently declaring nobody. Looser than the reader on
purpose -- a false alarm costs a reword, the opposite error costs a guard that
checks the wrong six people and says OK.

Tests then mutation-checked for being load-bearing, each mutation asserted to have
applied after a first pass where three silently did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:41:19 +01:00
slaguru666andClaude Opus 5 f3caaab04d R-287: assert the scope anchors in check-behaviour, and end them structurally
indexOf returns -1 for a name that is gone and slice(0, -1) is everything but one
character, so a scope anchored on a moved name does not shrink or fail -- it
becomes the file. Renaming the end anchor took one test's body from 3,184
characters to 30,825 with the assertion still passing. The other site windowed
burstAttack at 4,000 characters over a function that runs 4,305.

bodyOf asserts the anchor and ends at the next top-level function. A missing anchor
now says which anchor and what would have happened, where the old code reported
"burstAttack still calls rollWeaponDamage" -- a claim about a call when the truth
was a claim about a name.

Also records the rest of the sweep, including the guards deliberately left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:30:36 +01:00
slaguru666andClaude Opus 5 c0d33ee81d Hide 9, grounded 4, vital exempt — the borough is worth 56 points again
R-254 shipped the belly rule and called +6.4 points "modest on purpose". That read the
wrong statistic. +6.4 measured whether grounding helps; the question was whether the
borough still matters, and the gap between fighting the supporter on borough ground and
off it had fallen from 60.6 points to 44.9. Half damage from every departmental weapon is
meant to be the fight's central problem, and I had quietly taken sixteen points off it
while reporting a small number about something else. Same error as R-251.

Three changes that only work together:

  - hide 7 -> 9, restoring the weight on the axis that should hurt
  - grounded still halves it, 9 -> 4, which an ordinary round beats
  - exposedHideFor now takes the location's kind and leaves `vital` unhalved: a beast on
    its belly is not presenting its chest to the floor

Measured: 28.4% on borough ground flying, 68.4% grounded, 84.6% off it — a 56.2 point gap
against 44.9 before. Predicted 28.4 / 84.3 / 55.9 before building.

The composition of the three hide functions lives in prepareDerivedData where no test
could reach it, and check-anatomy's guard 6 is deliberately one-sided so it would not
have caught the vital losing its exemption. check-behaviour now composes the same
expression from the real supporter spec and the real tables. Negative-tested four ways:
removing the exemption, un-baring the wings, removing the halving, and inflating the hide
past what a round can beat each fail it with the right message.

Cost: the supporter goes from fifth to second deadliest creature in the book (2.84 -> 3.38
down, 46.8% -> 69.3% wiped). check-lethality refused the build until that row was
deliberately re-recorded.

BESTIARY.md printed the new hide and said nothing about it halving, because the body-plan
blurb predates the rule. The README said "eight guards" in three sentences above a
generated block listing nine. Both are the signature defect in prose: a number written
down that nothing reads. Both now generate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:15:35 +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 24b1aaa682 Development: skills improve from what beat you, not what worked
Reversed experience. A skill is marked when it FAILS - failure or fumble - and
never when it works, and the improvement roll is the same reversal: roll OVER
your rating and the skill goes up 1d6. Nobody learns anything from the lock that
opened first time.

The two halves pull against each other on purpose, so ratings converge instead of
running away. Simulated over 2000 campaigns: a 10% and an 85% skill start 75
points apart and are 11 apart after 60 cases.

- rules.mjs owns it: DEVELOPMENT, developmentMarksOn, developmentImproves,
  developmentCeiling, developmentGainFor
- trainingCap has been on the skill schema and the item sheet since it was
  written and nothing has ever read it. Development reads it now: a skill stops
  at its own cap, and everything else stops at DEVELOPMENT.ceiling, because a
  d6 gain could otherwise push a 98% skill past 100
- GM opens the phase from a GM-only toggle in the token controls; it posts a
  card naming every agent and their marked count, and puts a Development button
  on each sheet. Players press their own
- the toggle re-reads the setting on change, since getSceneControlButtons only
  fires at load and the button would otherwise lie if the phase were opened by
  the API or a second GM
- rules journal and player primer rewritten from the tables; the old text
  described the pre-reversal rule on both halves
- 7 spot-checks, 4 behaviour tests; verified end to end in a running Foundry

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 23:23:03 +01:00
slaguru666andClaude Opus 5 37b4043746 Play a full case, then fix what it found
Ran THE HOLLOW ROAD with five agents — approach, crossing, contact, the thing on
the road, extraction, debrief — with panic and Coherence both live. It ran clean
and it found one thing that mattered.

ANOMALY LORE 0. Marta Holloway, Field Lead, Senior Field Officer, could not roll
the department's signature skill. Nor could Okonkwo. Induction was gated to
TRADES on the reasoning that a career posting is itself the agency training —
but 17 of the 25 postings do not list anomaly lore, its printed base is 0 on
purpose, and nothing else supplies it. So the people who joined the department
straight out of school were the only ones who never received its basic training,
which is exactly backwards.

Induction is a FLOOR for everybody now, posting or trade, in the live generator
and the pack builder both. It never demotes: Bhattacharya keeps Registry's 63,
Holloway goes 0 → 43, Okonkwo 0 → 33. All twenty built agents verified non-zero.

Two GM tools the case showed were missing, both found by feeling the friction
rather than by reading anything:

  - clearPanic(actors). Panic clears when the scene does — one line in the book
    and, until now, five sheets edited by hand while the table waited. It leaves
    Coherence alone, which is the whole point of having two tracks.
  - partyState(). Everybody's hit points, panic and Coherence in one card. With
    five agents the alternative is asking "what are you on?" five times a scene,
    which is friction a table absorbs silently and then blames on the game.

And the player-facing gap: the primer never mentioned panic. Players got a
one-page aide-memoire for a track they now meet in the first scene. It has a
page of its own and a row on the cheat sheet, both generated from the same
tables as the rules.

One thing I reported as a bug and withdraw: Okonkwo's "Firearm (Pistol) 5%" was
me rolling the wrong weapon. He carries a marksman rifle at 53%. check-kits was
right.

Version 1.2.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 00:10:28 +01:00
slaguru666andClaude Opus 5 5aaae186e0 A fumbled relief attempt puts a point on
Trying to steady somebody and making it worse is the most familiar thing on that
list, and until now it was the only outcome that cost nothing.

Done as a NEGATIVE relief rather than a second code path: panicReliefFor returns
-1 on a fumble, and easePanic already subtracts what it is given, so subtracting
a negative raises the track. One number, one direction, no branch that could
drift from the other. The clamp still holds — fumbling at 5 stays at 5.

The chat card had to be told, because otherwise it prints the word "Psychology"
over a number that has just gone UP and the table reads that as a bug rather than
as the rule. It now leads with a red FUMBLED tag.

Guarded three ways: a spot-check in check-rules for the -1 itself, and two
behaviour tests — one that the sign survives easePanic's subtraction and
actually raises the track, one that it cannot push past the top. A test on the
constant alone would have passed even if easePanic had healed on a fumble.

Verified live: 2 → fumble → 3 → success → 2 → special → 0, and a fumble at 5
staying at 5, with the cards reading correctly.

Version 1.1.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:25:46 +01:00
slaguru666andClaude Opus 5 cbad87ba98 Add Occult, Religion and Psychology, and the Panic track
From the first game's list. The three skills are not decoration — they are the
levers on the new track, which is why they arrived together.

PANIC, 0 to 5, up. It is the other half of Coherence and the rules page says so
in the first line, because a table that confuses them will make the wrong call
at the worst moment:

    Coherence   what knowing has cost you       weeks     10-0, down
    Panic       what is happening to you NOW    minutes    0-5, up

A trigger gives a point with NO ROLL. Asking for a die to find out whether you
are frightened slows the scene and tells the player how to feel; the die is for
getting rid of it, which is the interesting direction.

    0 steady · 1 rattled · 2 shaken -5% · 3 losing it -10%
    4 overwhelmed -20% · 5 broken

The first point is free on purpose, so the opening shock of a scene costs the
table nothing. At 5 you break — freeze, flee or fixate, choose fast — and come
back at 3, not at 0, because breaking is not a rest.

Relief is where the skills come in. A success takes one point, a special takes
two. Psychology is the only thing that steadies somebody who is NOT you.
Religion steadies you alone and does not care whether you are right. Occult is
the odd one: naming a thing correctly, out loud, helps everybody who can hear —
the department's actual work, and the only relief that reaches a room. Plus
POW×5 to steady yourself, and your anchor object once per case, which is what
that briefing scene was for.

The three skills are placed in six registers rather than left floating: Occult
to the professor and to Registry, Psychology to the doctor and to Interview,
Religion to Antiquities and Pathology.

Guards, because a track nothing reads is this project's oldest failure: the five
new rules carry spot-checks in check-rules, and check-behaviour gains nine tests
— the free first point, monotonic penalties, only 5 breaking, clamping at both
ends, breaking not clearing the track, special taking two, every band/trigger/
relief having a label, and every relief naming a skill the catalogue actually
has.

Verified in a live game: penalty reaches the roll (58 → 58 → 53 → 48 → 38 for
panic 0-4, criticals and specials recomputed with it), the full arc runs 0 → 2 →
4 → eased to 2 → pushed to 5 → breaks → 3 with no errors, the sheet shows the
band, and the rules journal has a Panic page generated from the same tables.

Version 1.1.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:55:11 +01:00
slaguru666andClaude Opus 5 0852427586 Fix the pause: it would not hide, it was off centre, and it was too small
Three faults in 1.0.4, and the first was the serious one.

  1. It never went away. Core hides the overlay with `#pause { display: none }`
     and shows it with `#pause.paused { display: flex }`. 1.0.4 styled the BARE
     #pause with display:flex, which wins on specificity, so unpausing removed
     the class and changed nothing — GAME PAUSED sat over the table for the rest
     of the session. Every rule here is now scoped to `.paused` and `display` is
     not set anywhere.

  2. It was off centre because I repositioned it. Core already centres it: a
     full-width band at top calc(50vh - 100px), column flex, centred both ways,
     pointer-events none. That layout was correct and 1.0.4 fought it with
     left/bottom/transform. It is left alone now; only the height changes, and
     the top moves up by exactly half the extra so the centring holds.

  3. It was small: a 322x116 panel in the corner. It is now core's full-width
     band at 300px with a 170px dial — against core's own 100px — and the
     caption at 24px to match core's.

Guarded: check-behaviour now parses the stylesheet and fails any rule that sets
display on an unscoped #pause. Verified it bites. It strips CSS comments first,
because the first version of the guard reported the comment explaining the
guard — which is exactly what the template guard did last time, so that is twice
now and the lesson is written down properly.

Verified by driving the real toggle: running → display none, box 0x0, no class;
paused → display flex, 341x302, centre X exactly on the viewport centre;
unpaused again → display none, box 0x0.

Version 1.0.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 19:31:35 +01:00
slaguru666andClaude Opus 5 51bd8e618a Audit every remaining sheet and dialog; add the Culland Hall scene
Opened all of it this time, rather than driving the system through its API.

Two more defects of the same family as the NPC sheet, and in the same place —
the types added last:

  - Talent and Art had no TYPES.Item.* label, so their sheets printed the raw
    key "TYPES.Item.talent" under the name.
  - Neither had a branch in item-sheet.hbs or an isTalent/isPower flag, so the
    Detail tab rendered EMPTY over a document holding category, rule, uses per
    case, Coherence cost, push cost and origin. Eleven of thirteen types were
    handled, which is exactly the shape an eye skips.

Both now have a header statline and a full Detail section, and a guard walks
template.json: every declared type must have a label, a flag set in ringbrp.mjs,
and an {{#if isX}} section in the sheet.

One hardening fix: loseCoherence applies the cost and THEN announces it, so
anything that throws in between leaves the actor quietly poorer with no message.
localize() throws on a non-string, which an unrecognised reason key is.

Everything else came back clean, and it is worth recording what was checked so
the next audit knows what "checked" meant: 13 item types opened, all render; the
expedition sheet (4 tabs); all 7 chat cards driven through their real entry
points — roll, coherence, reaction via Combat.rollAll, hit location, weapon
damage, major wound, and contact through a case file that actually has a group;
all 3 dialogs opened by clicking the controls a player clicks. No errors, no raw
localisation keys anywhere.

Also: Culland Hall as a playable Scene. Drawn as SVG rather than generated,
because a room count, a door position and a label are all load-bearing here and
a generator cannot be trusted with any of them — three label-collision passes in
the browser before it was clean, which is the documented cost of doing it this
way. 2800x2000 at 100px to the square so the Foundry grid lands on the grid
drawn into the image; 5ft squares; no walls, because half-done walls are worse
than none and a GM who wants vision can add them in ten minutes.

Verified in Foundry: the scene imports, activates, and draws.

Version 1.0.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 19:02:18 +01:00
slaguru666andClaude Opus 5 c595bf5a33 Tidy the compendium, and release 1.0.0
Three things were untidy, and one of them was a stale declaration of exactly the
kind this project keeps producing.

  - packFolders named ten packs of seventeen. Talents, the Quiet Arts, the
    primer, the rules and all three scenarios were sitting loose in the sidebar
    because the declaration was written when there were ten packs and never
    revisited. It now covers all seventeen in four subfolders under one parent
    — Scenarios, Characters, Equipment, Rules & reference — and the build
    asserts the folder list and the pack list are the same set, so it cannot
    drift again.

  - Labels were off-pattern in three places ("Transport", and the two prologues
    reading "Custodians: Prologue — X"). Everything is "The Custodians · X".

  - Thirty-six pregens in one flat list. The pack now carries three compendium
    folders — Duty roster (16), Before the department (16), Original pregens
    (4) — and every actor is filed. Nothing unfiled.

Worth knowing, and verified against Foundry's own source rather than guessed:
packFolders only assigns a pack that has NO existing entry in the world's
core.compendiumConfiguration, and the routine only runs when the set of active
packs changes. So a world that already has this system installed keeps whatever
layout it had; the new one applies to a new world. The local world was brought
into line by writing the configuration directly.

Version 1.0.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:29:44 +01:00
slaguru666andClaude Opus 5 1bf8d7f6fb Playtest LAST ADMISSION: five fixes, one new guard
Played it through in Foundry with the six on their civilian sheets. Five
defects, all of the house's favourite kind — a number written down that nothing
reads.

  1. Pollard and Agyeman were armed and armoured as CIVILIANS. The police trade
     kit carries a revolver and a stab vest, which is right for a serving
     officer and wrong for a British civilian, and it ended the scenario's only
     fight in a single round. expandFromRegister's civilian mode now strips
     firearms and body armour from trade kit as well as induction and standing
     issue, and the build throws if one gets through. Wren Finch keeps her
     knife — a knife is not an issued weapon.

  2. Old Marrow's shears were a utility knife. The page said 1d8+1; the actor
     in the pack held 1d4+2, so a GM running off the token had a different
     fight from the one written down. He now carries a billhook — an actual
     garden tool that is already in the catalogue — and the page says 1d8+2,
     which is what a billhook does.

  3. He was too soft to be the fight the scenario promises. At the original
     numbers he died in an average of 2.4 rounds and in 6 runs of 20 he was
     dead before he ever swung. Retuned over thirty runs to HP 20, armour 2:
     about four and a half rounds, somebody hurt in 28 of 30, somebody dropped
     in 9 of 30. The verification run took four rounds and left Pollard on
     eight hit points.

  4. Two stat lines disagreed with the generated sheets — Marrow 16 against 15,
     Enderby 13 against 12.

  5. Two rolls had no failure written: the body, and Sammy's ankle pull. The
     body is now marked "no roll" outright, because there is no better answer
     to buy. The pull has a fumble result.

New guard: check-behaviour parses the scenario's own fight line out of the page
HTML and compares it to the actor the build will generate — hit points against
hitPoints(CON,SIZ), armour against naturalArmour, and shear damage against the
catalogue entry for whatever he is actually holding. Verified it bites by
reverting the damage to 1d8+1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:21:45 +01:00
slaguru666andClaude Opus 5 f576c7977d Add a second intake: six more agents, with portraits
Written for the group that actually plays. Three postings and three trades
again, and none of the six repeats a posting or a trade already on the sheet,
so the roster now covers sixteen different jobs and no two players can pick the
same character by accident.

  Gareth Pollard      Night Warden          Field Officer
  Dominic Ashcroft    Anchor Officer        Senior Field Officer
  Neil Braithwaite    Pathology Officer     Field Officer
  Kate Renshaw        Interview Officer     Senior Field Officer
  Danny Sharrock      Construction Worker   Probationary
  Colin Ferriby       IT Specialist         Probationary

Each also gets a before-the-department entry, because without one they cannot
play OPEN DAY — the prologue builds its civilians off the BEFORE table, and the
build throws on a roster agent that has no entry in it. Two of the six were
already trades, so their civilian self is the same job with none of the kit.

Two guards had to be corrected rather than satisfied, both for the same reason:
they had encoded an accident of the roster being exactly ten.

  - "the ten cover all ten trades exactly once" cannot survive a sixteenth
    agent. The property that actually matters is that no trade is missing, so
    that is what it asserts now.
  - "the roster should be half trades" would mean picking characters to satisfy
    a ratio. It now asserts both halves are present and neither dwarfs the
    other, and separately that no job appears twice — which is the check with
    real teeth, and it passes.

The portrait pass caught an arithmetic slip of mine: Sharrock came back looking
early forties against a sheet that said 35, and his biography says twenty years
on sites, which would have him starting at fifteen. He is 42.

Verified in Foundry: 36 pregens in the pack, all twelve import, sheets render
with their portraits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:19:58 +01:00
slaguru666andClaude Opus 5 26ad00174f Add the same ten before the department found them
Ten civilian counterparts, one per roster agent: same person, same
characteristics, same face, no induction and nothing ever issued to them. A
player can hold the two sheets side by side and read exactly what the job did.

Keyed off ROSTER rather than written out again, so the pair cannot drift apart.
The five who joined straight into a posting get the trade they came out of, and
between them the ten now cover all ten trades exactly once — which makes this
set a demonstration of the whole trade half of character creation as well as a
set of characters.

expandFromRegister grows a `civilian` mode: no induction skills, no standing
issue, no tier issue, no trade cant, and nothing anomalous or Coherence-costing.

Three things the build guard caught, each of which would have shipped:

  - Holloway was "carrying issued kit: notebook". Wrong: an interpreter owned a
    notebook long before anyone issued her one. The rule is now kit that could
    ONLY have come from the department.
  - Same again for skills — the linguist's core list contains trade cant,
    because it is a language and she is a linguist.
  - Wren Finch was carrying the black mirror, which is era "anomalous", from a
    transposed tradition, and costs Coherence to look into twice. That is
    something you acquire after the night the department notices you, not
    before. It is the only anomalous item in any trade kit.

The guard runs over the built objects rather than trusting the flag that made
them, because the flag is three call-frames away from the result.

Verified in Foundry: 24 pregens, the before sheets show the civilian job in
place of rank, no armour, nothing readied but fists, and the right face.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 16:19:50 +01:00
slaguru666andClaude Opus 5 c15b13c3b9 Add the duty roster: ten ready-to-play agents with generated portraits
Five postings and five trades, so a group choosing from this sheet meets both
halves of character creation without anyone having to explain them.

Skills and kit are not written out per agent. They are expanded from the
register at build time by the same helper the starter scenario uses, which is
hoisted out of the BLACK PLATFORM block for the purpose — two copies of that
expansion is two places for the induction rule to drift. Ratings sit at the
middle of each band rather than being rolled, because a pregen that varies
between installs is not a pregen.

Portraits are generated art, resized to 512 and stored as webp (12MB of PNG
became 210KB). buildActor now takes a portrait and gives the same face to the
prototype token: a pregen you can recognise on the map is worth more at a
convention table than a correct one you cannot tell apart. It throws on a
portrait that is not on disk, because a broken image on a pregen sheet is the
sort of thing that is first noticed on a projector.

Guard: check-behaviour gains a test that every roster agent names a real
posting or trade and has its portrait on disk, verified to bite. It caught
"archive", which is not a posting — Bhattacharya is Registry.

Verified in a running Foundry: 14 pregens in the pack, sheets render with the
portraits, coherence and hit points fill from the template, no system errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:06:04 +01:00
slaguru666andClaude Opus 5 d9bce12c27 Replace the starter with BLACK PLATFORM, and make the dialogs ask questions
The starter is now an action case: a train running through a station whose rails were
lifted in 1976, two engineers aboard, eleven minutes until it returns. Descend, take the
platform, board, rescue, fight forward along a moving train, stop it.

Simpler than PAPER WINDOW on purpose — one clue scene, no social subsystem — which also
means it cannot reproduce R-248, because there are no groups or standings to leave
unwired.

Every agent can fight, deliberately: floors of firearm 42%, melee 40%, Brawl 40%,
Dodge 40% so nobody is a spectator on their first evening. Floors only RAISE, so a
posting already better stays better — Callum's shield lands at 53, not the floor's 45.

The builder now handles combat floors, extra weapons issued for a case, and a scenario
with NO Borrowed Authority at all; the last was assumed to exist and would have thrown.
Two new tests: every floor names a real skill, and every agent clears the fighting
floors the handout promises.

Sixty-two dialog strings reworded. They were naming mechanics at people: "Situational
modifier %" is now "Anything else helping or hurting? (%)", "Target cover" is "Is the
target behind anything?", "Difficult (÷2)" is "Difficult — half your skill". Labels ask
the question; hints say what it does to the number; notifications say what to do next
rather than what went wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:08:07 +01:00
slaguru666andClaude Opus 5 4e28de2a64 Play PAPER WINDOW with the shipped pregens; fix four silent defects
Ran the starter end to end. It closed, and it found four defects in what I shipped an
hour earlier — all in the scenario data, all silent.

R-247: the Borrowed Authority did not work. I wrote scope "operate", which is not one
of the five scope tiers; an unrecognised scope ranks 0 and every action came back
"impossible" with no error. The first scene of the starter is the Borrowed Authority
tutorial. Fixed, guarded in check-kits, and covered by a test that documents the
failure mode.

R-248: the contact subsystem was unreachable in the scenario built around it. The case
file carried a standing for the Returned Clerks and no GROUP ITEM; resolveContact
returns null without one, so READ, SIGNAL and OFFER did nothing. Groups are data now,
and a test asserts every standing on a shipped case file has a group behind it.

R-249: legs wrote `kindId` where resolveLegDialog reads `leg.kind`. It worked only
because the terrain text happens to begin with "Extraction".

Not a defect: phase four went Difficult because Resources hit 0 and destitution makes
every remaining phase Difficult. Best beat in the session.

All four phases failed and the case still closed — Containment 8 to 2, Resources 8 to
0 — which is what the scenario is built to survive. Two of six closing requirements
succeeded and the settlement still completed. The Cancellation Man opened with a
critical against a raised riot shield and went through 10 points of armour, teaching
the shield and graded-defence lessons in one roll.

Also documented: nobody on the starter team can aim the return, because only the Anchor
Officer trains Transposition. The extraction page now says to narrate it rather than
roll six dice that will all fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:39:43 +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