Cut the register mechanic, the census creature, the paperwork route and
half the clues. The under-twelves know Mr Smith's song from the nightly
creche speaker, so Act Three is a choice: burn the tape and take the
children into care, keep the families below, or cure them with a cut
tape first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The agents now cross into Bellhouse on a diplomatic brief to bring its people
home. A thousand people (118 of the 1974 intake, 881 born below) run the Seat
like a 1970s council, kept down by ninety-day renewals from "Centre", which
is Mr Smith, planted in THROUGH TRAIN. His experiment is procedural and
quiet: a bulletin response, a crèche lullaby, a shared dream, the children's
drawings, a steered establishment. Act One is the briefing, the moor and the
crossing into Enquiries. Act Two is the site, and the detention the Controller
orders on Centre's circular, measured and cited. Act Three is Annex D: at a
thousand the Seat broadcasts an Address that enrols whoever hears it, and the
table must silence it before or while they release the Seat under clause 4.
Same site, people and plates. Two cast actors (the wardens, Teague). New
handouts SO-H05 (Centre's circular) and SO-H06 (terms of engagement). The
section map's key is relabelled (issue 2). Status is honest: v0.7's five desk
passes tested a different plot, and this needs its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The man who told Vane what the chart had to say now has a name: a Mr Smith,
with the company's paper, whom the company never employed. Vane names him if
asked and cannot picture his face. A "do not resolve" line says who he is not
to be explained here: he is the thread into STANDING ORDER.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A second scenario gets pack-tables' arrangement: four configs (Teague plus
one, three or five wardens) against the declared six and the four-player
cut, 2000 runs at seed 11, cap 400, recorded to detention-baseline.json,
citeable as "detention", and --check added to npm run check.
standingorder joins all-specs' SCENARIOS, and simulate.mjs now reads that
list instead of keeping its own copy, so a new cast is fightable the day it
is registered. The lethality, focus and pack baselines are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New pack ringbrp.standingorder, in the Scenarios folder. It holds the GM
journal (13 pages, one per section), the three player handouts, nine plates
to show, six faces and the Bellhouse section map as a scene. No cast actors,
because nobody in the case has a stat block.
The GM pages are read from docs/scenarios/STANDING_ORDER.md at build time
rather than transcribed, so the compendium cannot drift from the document the
desk passes ran against. STATUS and Open questions are left out as authoring
notes; STATUS's "not obvious" list leads page 1.
Doc fixes found on the way: the Act Two heading still said ~55 min against a
75-minute budget; STATUS said four passes and "no desk playtest run" after
five; the runtime open question still called 45/55/50 unbacked.
figures-baseline: generator enrolled at 0; STANDING_ORDER.md leaves the
uncited list because it cites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two hundred and six people went underground in 1974 to govern the country after
the war. There was no war. The Deputy Controller filed the drill as live, on
purpose, to save the site a fortnight of arguing about whether it was allowed to
start — and reality took the filing.
The apocalypse in this one is clerical. At renewal 211 the site's population
record exceeds the surviving population of the region it governs, and a record
that cannot hold both resolves by correcting the region. Act Two shows the
players the sum; Act Three is the decision, and all three endings cost
something. Cancelling the order kills the sixty-one children born below, who
have no record above ground. The text says so and does not offer the GM a way to
make it painless.
Deliberately not CLEAN GROUND. That case is a filed valley found by accident and
resolved by cancellation; this is a filed institution the department built on
purpose, and its best ending amends the order's scope rather than cancelling it
— a Borrowed Authority problem, which is the setting's own thesis about
arrangements beating fights.
Three existing bestiary creatures, nothing new to build: the census counts in
the Registry, the overwriter performs the correction if the renewal goes
through, and the quarantine unit is the failure state you argue down.
Cast declared as a different six from CLEAN GROUND's so the two cases exercise
different sheets. All twenty-one guards pass: check-scenarios resolves 115 tags,
check-rollable holds every route to the declared cast, and check-figures records
the document at zero bare figures — it cites no measured number because it
prints none, the quarantine unit's lethality included.
The section drawing is hand-built SVG and must stay that way: every label on it
is load-bearing and no generator holds legible text. Numbered callouts with a
key beneath, per AFTERIMAGE v3.1.
NOT playtested. Not once, desk or human. Every timing in it is a guess.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
R-308. Read the fold as a stranger at its author's request. Two of the three
risks they flagged are sound: isProse drops nothing that carries a citation
(scanned the whole corpus), and the duration exclusion earns its place, since
"runs" now means three things in this corpus and only the cell can tell them
apart.
The third is real. numEnd used line.indexOf(m[1], m.index), which finds the
first copy of the digits at or after the match start rather than the copy that
was captured:
"The wipe rate of 74 in ten is 74%<!-- cite: ... -->."
value=74 numAt=17 marked=FALSE
A correctly cited figure reported bare, because numEnd lands mid-sentence and
the marker test reads " in ten is 74%...". Fixed with the d flag: m.indices[1]
gives the capture's real position and there is nothing to search for.
Narrow to reach — it needs the wipe-rate shape, the only one with a wide gap
before its capture, and an integer duplicate inside that gap; a decimal cannot
do it because [^.] will not span a decimal point. Three attempts failed for
that reason before the fourth worked, which is why this is recorded as narrow
rather than theoretical.
Worth fixing anyway because its direction is the bad one: a miss costs one
figure, a false positive on correct prose costs the guard.
Seven-case battery re-run, every restore byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
R-300 was right, and it was right with a live defect rather than an argument:
a bare **24** in a table whose own header row says "Median rounds", reported
OK by this guard, against a baseline holding 25. The scan read one line at a
time, so measurement status living two lines above was invisible.
Fixed by reading the header. A cell now inherits the measurement status of its
column — which is a claim the DOCUMENT makes, rather than one this file's
vocabulary has to anticipate. That is the narrow repair, and it is deliberately
a different mechanism from the shapes: the shapes guess at phrasing, the header
does not have to.
The general point in R-300 still stands and the file now says so where the
shapes are defined: a vocabulary learned from the marked figures cannot contain
the phrasing of the figure nobody marked. check-unmarked attacks that from the
other end, treating bold as the corpus's own mark of a published figure.
Two bugs found while testing, both mine, both caught before commit:
- A citation marker is full of digits and none of them are figures. "packs
line.hollow6.hurt" holds a 6; "fight-tail cut.over15" holds a 15. Scanning
raw cell text reported roughly 130 correctly-cited rows as unheld — the
best-marked tables in the corpus. Comments are now blanked rather than
removed, so every offset still points at the right character.
- "3.48 of 6" — the 6 is the party size the mean is out of, not a measurement.
Coverage goes from 25 figures, 7 marked, to 120 figures, 102 marked.
Proved again by breaking it, five ways, every restore byte-identical: the
historical bare **24** fails at its own line, a stripped prose marker fails, a
planted "median 24 rounds" fails, a new bare figure in an uncited document
trips the ratchet, and neither a threshold column header nor any of the 102
cited cells fires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
check-cited resolves every citation marker against the artifact it names, and
has one structural blind spot: it can only resolve the markers that exist. A
measured figure written into prose with no marker beside it is not a failed
citation, it is not a citation at all, and nothing looks at it again.
Desk pass 11 proved it. "A median 24 rounds" sat in the Pacing Note and "24 if
the column joins" in EXPOSURE, against a baseline holding 21 at two of the
column joining and 25 at three, through every pass that checked citations.
This reads the figures instead of the markers. Narrow by design: an earlier
draft matched any number within 45 characters of a measurement word and found
282 candidates, nearly all prose ("down 140 steps", "an engineer on his
rounds", "01:06"). The shapes here are the phrasings the documents actually use
when quoting the simulator, each read off a figure that is cited somewhere.
Opt-in rule: a document that uses citations must mark every measurement figure.
One that cites nothing is held by a ratchet instead — turning three unguarded
scenarios red is how a guard gets switched off on the day it is written — and
joins the strict regime the moment it gains its first marker.
It found three things in CLEAN GROUND before it was wired in:
- 74.9% printed bare twice while cited correctly four times, and that is the
figure that was published at 58.7% until the truncated sweep was found.
- "the longest fight is 120 rounds" — 120 is exactly packs cut.hollow6.longest,
the field check-cited REFUSES to let anybody cite because a sample maximum
moves by a third on a re-seed. Refusing the citation while printing the number
left the least stable figure in the suite as the only one nothing held. The
sentence now leans on the guarantee that is actually strong: check-packs
refuses to record a sweep in which anything reached the cap.
Proved by breaking it, four ways, with byte-identical restores: a stripped
marker fails, a planted "median 24 rounds" fails at its own line, a new bare
figure in an uncited document trips the ratchet, and a threshold column header
("Past 15 rounds") does not fire — that last one was a real false positive in
the first draft, which tested the matched text rather than its context.
v0.23. 21 guards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matching the step-5 prose against the derived source before placing markers left
five figures unmatched: 21.9, 74.3 and 3.8, twice each. They are the thing against
Ashcroft's undiminished 85 -- the contest that never happens, which Act Four prints
on purpose to price what THE OFFER sold.
Derived now as hadHeRefused, so the counterfactual is held to the rule like
everything else and carries a name that cannot be mistaken for an event. Quoting
those three as odds a GM meets is what went wrong in two sentences and a table.
Markers not placed: CLEAN_GROUND.md is c0's and they are in it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Desk pass 9 found the scenario's most consequential table held by nothing:
~30 sampled figures, no baseline, catchable only by re-running the exact
command. lethality-baseline.json does not cover them — it measures creatures
SOLO against a frozen four-agent party that is not this cast.
Building the measurer found the figures were also wrong.
simulate.mjs's CLI calls runFight with no options, so every published figure
was measured at the default 40-round ceiling — and runFight does not report
truncation, it scores whoever is standing when the loop stops. Measured at
400:
line.column6 14.8% -> 15.2% 34/2000 truncated
line.hollow6col2 7.8% -> 8.3% 129
line.hollow6col3 28.7% -> 30.1% 264
cut.hollow6 58.7% -> 74.9% 578 <- 29% of runs never finished
Fourteen points on the single most alarming number in the case, and the one
v0.16 added specifically to warn four-player tables. fight-tail learned this
in R-270 and carries an assertUncensored; EXPOSURE's own tables never got
one. The longest fight at cap 400 is 120 rounds and 400 vs 2000 are
identical, so the cap is comfortable rather than merely sufficient.
tools/pack-tables.mjs measures all 17 configs against the DERIVED cast via
castAndCut, using measure()'s exact discipline — one rng threaded through
every run, not a reseed per run, because reseeding is a different stream and
would not reproduce the published table. It refuses to report or record a
truncated sweep.
tools/check-packs.mjs holds the baseline against the game, so the pair is not
a loop: check-cited holds the prose against the record, this holds the record
against the harness. Hard claims read `now` and never `base`: nothing
truncated, the party equals the declared cast, config floor, and more of the
same creature may not make the party safer. Figures compare exactly, since
the runs are deterministic.
Verified by breaking it: a drifted baseline, CAP lowered to 40, and a removed
config each turn it red, and --update refuses outright rather than recording
a truncated sweep. The removed-config test first passed for a bad reason —
MIN_CONFIGS is a floor and 16 clears it — so a dropped-config check was added
and re-tested on a row in no monotonic chain. All files restored
byte-identical after each probe.
EXPOSURE's three tables and the nine prose figures around them are rebuilt
from the artifact with citation markers; the multi-seed stability claims were
re-measured too (the cut is 74.9/72.7/75.0/73.5 across four seeds, not
58.7/60.5/60.3/59.8). CLEAN GROUND v0.20.
npm run check: 20 guards pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
Applies desk pass 8's five fixes, and adopts R-292's TAG_OPEN.
1. Step 5 now has all three of its outcomes. opposedContestFor returns taken,
held and unsettled; the document printed the first and the third. "They
hold" is 48.1% against the default target, 52.4% against Braithwaite and
74.3% against an Ashcroft who refused — which v0.17 established is what
most tables have, so the middle column is the one they will play. It gets
its own countdown row, its own table, and read-aloud of its own, and the
COMES TO NOTHING paragraph is now explicitly the unsettled case so it can
stop being the nearest text to an outcome it is not about. The reason
they held is the case's own argument arriving as good news: a person with
a life on the record is a difficult document to overwrite. Every figure
re-enumerated over all 10,000 roll pairs before writing.
2. EXPOSURE's five things are six, in order, under a heading that says six.
Introduced in my own v0.16 and survived two versions; asserting a unique
match protects against editing the wrong text and not against inserting in
the wrong place.
3. EXPOSURE opens with a one-minute box. The body is untouched — nothing in
it is padding — but 3,176 words is sixteen minutes about the encounter the
document exists to prevent, and a GM with thirty minutes of prep now has
somewhere to stop.
4. The stale open question is closed. Braithwaite has not been on the
critical path since v0.14 un-gated the tell, and pass 6 ran the act with
the Xenology and the Psychology both failed.
5. Act Four's Psychology has a special: which file it thinks it is, and how
recently it read it. It corrects them on Prichard's service history — it
is not remembering, it is citing.
Coverage 6 -> 7 specials, so the cited figure moved 85% -> 83.7% and
check-cited caught the prose before I did, which is what it is for.
R-292 adopted: outcome-coverage now composes its body onto check-scenarios'
TAG_OPEN through tagRe, so the two readers cannot drift on what starts a tag
while keeping the different bodies they need — and tagRe's fresh matcher per
call avoids the shared-lastIndex defect, this file being the second caller
that would have found it. Verified identical across all seven spellings.
npm run check: 19 guards pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c0 asked whether tagsIn should return indices so beatLikeIn could take spans from
it. No -- that widens this file's signature to serve another, and R-291 already
fails when the two disagree. But detection is not prevention, and there is a third
thing to share.
The two patterns differ in their bodies for good reasons: tagsIn captures the whole
tag for skillsIn to split, outcome-coverage stops at the em-dash because the
outcome follows. What was copied into both files, and what drifted, is the opening
-- the literal \[CUS:, widened by R-290 here and left behind there. TAG_OPEN is now
exported as a string with tagRe(body) composing a fresh matcher onto it; fresh
because a shared /g regex carries lastIndex between callers.
22 tags before and after, c0's body composed onto TAG_OPEN gives the same 22 its
own pattern does, 19 guards green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-291 measured check-scenarios' tagsIn against this file's readers and found
them apart on three spellings. tagsIn was widened for case and spacing around
the colon; outcome-coverage's strict half still carried the case-sensitive
literal, so `[cus: Spot — x]` was READ by one guard and reported unparseable
by the other. The build failed, which is the safe direction, but it failed
saying the tag could not be read while another guard had just read it.
beatsIn and beatLikeIn now share one TAG pattern, widened to agree with
tagsIn. Measured across seven spellings: the five both strict readers accept
now agree in all three, and `[CUS Spot]` and `[CUS= Spot]` are still refused
by both and still named by the loose counter, so the canary keeps its teeth.
Verified on the real document that this is a spelling fix and nothing else —
beats 21, quoted 1, unparsed 0, stated 18/6/6, session figures unchanged, so
the baseline is untouched. A lowercase tag planted in the acts is now read by
check-outcomes, check-scenarios and check-rollable alike; the file was
restored byte-identical after the test. R-291's own assertion still passes.
Found by the peer session measuring my file rather than trusting it, which is
the fourth reader this session to be caught describing or reading its own
format wrongly.
npm run check: 19 guards pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Swept every reader that parses a human-written marker. Two more had R-289's
fail-open.
tagsIn matched a literal [CUS:, so [cus: Spot], [Cus: Spot], [CUS : Spot] and
[ CUS: Spot] all read as nothing -- and it is the corpus reader behind
check-scenarios' skill validation and check-rollable's reachability, so a beat
spelled any of those four ways was checked by neither while both printed OK. The
corpus contains no such tag today; the 88-to-89 roll count during this work was c0
writing v0.18, verified against HEAD's reader on the same tree.
The POWER: marker had it with nothing covering it. bestiary.mjs and check-powers'
own scan both used the literal, so a creature added with "Power:" generates no
entry line and is never reported unclassified -- check-powers passes green on it,
measured on a clone with its dodge skills stripped so the dodger-count assertion
could not fire instead. Reader now takes POWER\s*: and stays uppercase, because
power: occurs in ordinary prose; an asymmetric counter names the rest, and was
measured against content.mjs first (41 strict, zero loose outside them).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
c0's point on R-286: the reader is unambiguous now, but the roster fallback that
made the failure look like success is still reachable. Narrowly closed -- sixteen
of the seventeen scenarios declare no cast and are rightly checked against the
whole roster, so only a marker that is PRESENT and names nobody is refused, with
its line named. Replacing the declaration with <!-- cast: --> passes before this
change and fails after it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
CLEAN_GROUND.md contains two things matching <!-- cast: ... -->: the real
declaration, and the warning ten lines below it that quotes the marker inside
backticks and matches with an empty capture. check-rollable and declared-cast both
took .match(), first hit wins, so the arrangement has been correct only because the
declaration comes first.
Move that warning above the list and check-rollable reads a cast of nobody, falls
back to ROSTER_BEST, and prints the same OK line having held every skill in the
document to the full duty roster instead of the declared six. Instrumented and read
off: "the duty roster" against "its declared cast of 6".
castMarkersIn blanks code spans and fenced blocks before scanning (padded, so line
numbers still point at the source) and refuses more than one surviving marker,
naming every line. check-rollable imports it instead of carrying a second regex, so
the guard that checks the cast and the guards that measure the fight cannot
disagree about who is in it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-284 scoped the factored-rating rule to the creature's entry and left the
defence-stacking rules matching anywhere on the page, so the exemption sentence
and the "Across N creatures that spend defences" count were accepted wherever they
happened to sit. Moving the exemption line out of its section into the redcap's
statblock -- the page silent exactly where a GM reads the stripping advice --
passes at R-284 and fails here; confirmed by running HEAD's copy against the same
tree.
The owning slice is not always the creature's entry. A factored rating belongs to
the creature; the ladder exemption is an answer to the paragraph it sits in and is
generated into "## Shooting at something that moves", so scoping that rule to
"### Redcap" would have failed a correct page. sliceOf now takes a heading at any
level and each rule names the slice that owns its claim.
A renamed section fails by name rather than scoping to nothing, which is the shape
of every guard that passes because it found nothing to check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Applies desk pass 6's seven fixes.
1. EXPOSURE now measures the fight the table will have. Every row in the old
table was one force fighting alone, and the six at the back are never
alone — they walk inside forty-one refugees. Six hollow men wipe 0.0%;
with two of the column joining, 7.8%; with three, 28.7%. Two is now the
stated default, because two people out of forty-one losing their heads
while their neighbours are shot is not a large number.
2. The four-player block has hollow-man rows: 0.3% at three, 58.7% at six,
99.9% with two of the column. Its only hollow-man figure before was the
six-player 0.0%, so a GM running the cut was reading somebody else's
table — for the encounter the party is likeliest to choose, because the
six at the back are the only figures the scenario says are not people.
GM ESSENTIALS item 3 carries the same correction.
3. Act Three's warning named a roll that does not exist. Its twenty-minute
bomb hangs off "Anomaly Lore — what a peg is"; the depot entry is a
Research roll. Pass 4's post-pass inherited the conflation from this
warning and is corrected too.
4. GM ESSENTIALS states the real fumble band. fumbleStart is
101 - ceil((101-band)/20), tested before the 96-99 clause: 00 at 85,
99-00 at 63, 98-00 at 53, 97-00 at 40. Four times what "00 always
fumbles" implies, in a case that rolls Spot 40 across two acts. Pass 4's
correction was right at 63 by luck and would have been wrong at 53.
5. Sixth EXPOSURE lesson: a fight costs the session. Median 13 rounds on the
printed row, 24 mixed, 30 at four players — sixty to ninety minutes in an
act budgeted at fifty. The Pacing Note now says what to do when one
starts, and not to absorb a fight and the peg fumble in the same act.
6. The fumbled Xenology is written. At 53 it fumbles on 98-00 and Braithwaite
puts his name to a baseline human in front of everybody. The un-gated tell
still arrives; it now costs the party its expert.
7. R-283: simulate.mjs described a mixed force as N of whichever spec came
first, so six hollow men and six of the column printed as "12 x Hollow
man" — two fights 88 points of wipe rate apart under one label. The
composition was always right and the report was not. Uniform and --spread
output are byte-identical, so nothing already published goes stale.
Every figure re-run before writing rather than carried over. npm run check:
18 guards pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-283 left the intact-sentence-wrong-number case falling through to omission, so
the guard said BESTIARY "does not state" a rating the page was stating. reads()
now has a shape tier between strict and loose: the strict pattern with its value
slot loosened, reporting which of the two numbers moved.
Scoped the per-creature rules while adding it. They read the whole page, and each
is the only rule of its kind today, so a page-wide match found the right line by
luck; a second attackFactor creature would have let the courier's rule match that
creature's sentence and report the courier correct. They now read the creature's
own "### Name" entry -- proved by deleting the courier's line and planting an
identical one under the redcap: still omission, where before it would have passed.
Five discriminations plus the decoy, proved in a worktree with the message read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-282 matched the page with String.includes, so rewording the exemption sentence
reported "BESTIARY never says it is off the stripping ladder" -- sending a
maintainer after a sentence that is sitting right there, and never naming the
real problem, which is a pattern that has silently stopped reading.
Each textual rule now reads twice. Strict is the sentence as it stands and is
tighter than before (the bold and the full stop, not the bare clause a substring
accepted); loose is the same claim in any wording. Strict passes, loose-only is
reported as a reword with the line quoted and the page presumed right, neither is
the omission.
The loose anchor was wrong on its first pass in the way that matters: "a sentence
with 40% and 80%" also matched the courier's own statblock line, so deleting the
sentence reported a reword and quoted the statblock back. It now excludes that
generated marker, which makes it a test of the claim and not of the digits, and
degrades to omission rather than to a false reword.
Four discriminations proved in a worktree with the message read in each.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check-bestiary proves the page matches its generator, and the generator had never
heard of powers.mjs -- so the redcap sat in "the ones that take the most
stripping" under eighteen green guards while its own entry said it never spends a
defence. Two files agreeing with each other while both disagree with the engine
is a quorum, not a check.
check-powers now asserts per effect kind what the page must say: defenceStacking
requires the creature off the stripping list, named as exempt, and the "across N
creatures that spend defences" count reconciled against powers.mjs; attackFactor
requires the rating the simulator actually uses printed as a number, which the
courier's entry now carries.
The clause that matters is the failure on an unknown effect kind -- a wired effect
with no DOCUMENT_RULE fails the build, so the next one cannot arrive without
somebody deciding what the document owes it. Without that this would guard the
mistake already made and nothing else.
Proved three ways in a worktree, exit codes read directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things in the generated document had gone stale the moment powers reached the
simulator, and eighteen guards were green over both because nothing connects
powers.mjs to bestiary.mjs.
The dodge-ladder section listed Redcap among "the ones that take the most
stripping" and counted it in "across 47 creatures", when NOT TIRED means it never
spends a defence at all -- the exact opposite of what its own entry says three
pages down. It is off the ladder now, the count reads 46 that spend defences, and
the page names it: stripping is not a plan against it, killing it is.
And the paragraph listing what the harness does and does not model never mentioned
that it fights 39 of the 41 creatures with a power without it. It now says so, and
counts from powers.mjs rather than stating it, including the 14 that are fight
rules it cannot express -- so every figure for one of those is the creature with
its best trick taken away.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-280 widened admission from a [15,85] band to "the spread rate stands clear of
both ends by more than its own noise", which admits fights that are nearly
settled -- the_choir at 99.3%, the_stanchion at 6% -- as long as their noise is
smaller still. The page went on saying "whose outcome was ever in doubt", which
was a fair description of the band and is a loose one of the rule.
It now says "whose odds leave room for a difference to show", and the provenance
line prints the admission rule itself, read from the artifact rather than
paraphrased, so the page cannot drift from the guard again. The rule is phrased
as a clause in check-focus for that reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-280 left the claim satisfiable by a gain of 0.2. The artifact now records how
many measurable packs clear their own noise -- reliable: {aboveNoise: 36, of: 38}
-- and the check refuses if that share falls. It may rise freely.
A ratchet rather than a threshold: any threshold here would be a number I chose,
and choosing one just under the current value is what produced MEASURABLE =
[15,85]. A share rather than a count, so widening admission cannot pay it off.
Proved three ways in worktrees: making focus fire actively bad fires the drift
check first, which is correct; making it unreliable and re-recording fires the
older claim at 37 of 38; and claiming a better past, 38 of 38, is refused by the
ratchet itself. The ratchet bites exactly where the old claim does not -- between
"still helps everywhere" and "helps as reliably as it did", which is where a slow
degradation lives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MEASURABLE = [15,85] proxied for "can this fight move at all". The direct form,
in the units the guard already uses: the spread rate must stand clear of both
ends by more than its own noise. Deliberately blind to the gain -- admitting the
sizes where focus fire clears its noise would make the guard's claim true by
construction. Size is still picked on nearest-an-even-fight.
31 measurable became 38. the_arrears returns at 6.3, and switchboard arrives at
8.1 -- the second largest gain in the artifact, thrown away for being one point
past a round number. Four of the eight carry effects larger than most rows the
band already admitted.
Two of them do not clear their own noise: the_choir has 0.7 points of headroom
and used 0.2, the_stanchion has six and used 0.2. Above-noise falls 31/31 to
36/38 and the bestiary prints 36. That is two measurements reporting no
detectable effect, which the band suppressed by refusing to take them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Asked to raise the scan until the redcap's pack size stopped flipping. Measured
the threshold -- unstable at 3000 and 4000, stable across twenty seeds at 6000 --
raised it, and the re-record took fifteen seconds, which was impossible.
winRate takes three parameters and pickSize passed SCAN_RUNS as a fourth.
JavaScript discards it, so every scan has always run at RUNS and SCAN_RUNS has
never been read by anything. The fix I was asked to make was inert in the same
way as the thing it was fixing.
winRate takes runs now. The redcap is still n=3 with gain 6.3, arrived at stably
rather than luckily; the_arrears drops out of the measurable band at an honest
scan, 31 packs to 30; the_committee moves 2 to 6 and stays pinned. Claim check
still passes, bestiary regenerated.
The only signal was a number being too small. A fifteen-second re-record is good
news, and good news is what nobody investigates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
The content has called for 'opposed POWx5' since before the rule existed -- the
hollow man's tactics, CLEAN GROUND twice, slice-text three times -- and rules.mjs
defined none. A scenario carried a local ruling that said in its own text it was a
ruling and not a rule.
Generalises that ruling rather than inventing another: both sides roll, the better
band wins, only the ladder the game already has. Ties go to whoever is being acted
upon, which is what defenceOutcomeFor has always said; the scenario's 'favour the
agent' gave the same answer only because no agent ever initiates one. Neither side
succeeding leaves the contest unsettled rather than won, which the two beats need
in opposite directions.
Two exports at the scenario session's request: opposedOutcomeFor compares graded
levels and carries both, so a caller can price a fumbled attempt without this file
deciding what a fumble costs; opposedContestFor runs it from ratings and rolls with
per-side difficulty, so 'resists at Difficult' does not put applyDifficulty back
into a document. Spot-checked against the real Act Four beat: 55 against 85 at
Difficult, which is 42.
Page 1 states it by asking it -- the tie-break and the margin are computed from the
rule at build time, so the book cannot drift from the engine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fourth variant of the same defect, found by the peer session probing sideways.
Neither CITE nor ANY_CITE carried the `i` flag, and ANY_CITE required a literal
"cite:" with no space before the colon. So three plausible spellings were
invisible to the reader AND to the counter that exists to catch invisible
markers:
<!-- Cite: first-blood cut.swing --> OK 25, exit 0
<!-- CITE: first-blood cut.swing --> OK 25, exit 0
<!-- cite : first-blood cut.swing --> OK 25, exit 0
Each of those was verified carrying a citation printing 41 against an artifact
holding 40, and each reported OK with a count byte-identical to a clean tree.
The count check could not see them because it was looking for the same literal
the reader was. Capitalising the first word of a comment is not an exotic
mistake.
The two patterns are now deliberately asymmetric, which is the actual fix:
CITE tolerates case and spaces around the colon, so those spellings
simply work when correctly placed.
ANY_CITE stays looser still, so a spelling neither of us anticipated is
counted and named rather than skipped.
The reader accepts only what the format specifies; the counter recognises
anything a person might have meant as a citation; the difference is reported.
The failure text now covers misspelling as well as misplacement, since the
unread set can be either.
Matrix verified, exit codes read directly: a wrong value fails under all four
spellings including no-spaces; a right value passes under all of them, counting
26; a misplaced-and-capitalised marker fails; a marker missing its path fails;
clean tree OK 25 with no false positive.
Four iterations of this guard, four variants of one defect -- something that
exists and is never read -- and all four were found by the session that did not
write it.
npm run check: 16 guards, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two residual defects in the marker check added at 4d7d93a, both found by the
peer session that found the original.
1. THE REGEX WAS LOOSER THAN ITS DOCUMENTATION. The failure text has always
promised the comment must follow its figure "immediately, with nothing but
markup between", but CITE's separators were \s*, which spans newlines — so
a marker on the line after its number resolved and was checked, contrary to
the stated rule. Tightened to [ \t] throughout.
Verified safe before changing it: matched under the loose pattern 25, under
the tight pattern 25, so no citation in CLEAN GROUND relies on crossing a
newline. There is no finding in the document.
2. THE OFFENDER REPORT WAS RIGHT ABOUT THE COUNT AND WRONG ABOUT THE LINES.
It filtered line by line, so a valid cross-line citation was printed as an
offender whenever some other marker was genuinely unread — a maintainer
told "line 15 is broken" would have edited a working citation. That is the
headline defect fixed an hour ago one level down: correct verdict, wrong
reason.
Offenders are now located by position across the whole text, so the lines
named are exactly the markers no CITE match covers. This is the fix that
matters independently of the regex: a marker spanning lines would still be
counted once by ANY_CITE and named nowhere, so tightening alone leaves a
narrower version of the same bug, and position-based reporting stays
correct if anyone ever loosens the separator again.
Placement battery, exit codes read directly: clean tree OK 25 no false
positive; one word between number and marker fails; bare marker alone fails;
two on a line with one attached fails naming only the unattached; cross-line
now fails rather than silently passing.
Three iterations of this guard, three variants of one defect — a marker or a
message that exists and is not read — and all three were found by the session
that did not write it.
npm run check: 16 guards, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A misplaced marker failed open. CITE requires the comment to sit immediately
after its figure; put one word between them and the citation is silently
skipped while the run still reports OK with the same count as a clean tree:
The longest fight seen ran **67** rounds.<!-- cite: fight-tail cut.longest -->
check-cited: OK — 25 cited figures ...
That marker names a field the guard is supposed to refuse outright, and the
guard never saw it. Worse than an absent citation, because whoever wrote it
believes they added a check — and worse still in a guard written yesterday to
catch exactly this shape of defect.
Every `<!-- cite:` occurrence is now counted and compared against what the
parser actually read; any difference fails and prints the offending line.
Verified on both placement failures: a marker one word from its number, and a
marker on its own line with no number at all. The failure headline was also
wrong for this class -- it claimed a figure mismatched its artifact -- and now
covers both.
Found by a peer session writing a bad citation by accident while testing.
npm run check: 16 guards, exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
R-266 said a fighter goes down at half maximum hit points and called it one rule
in both directions. conditionFor puts someone down at hp <= 2; majorWoundFor is
ceil(hp/2) and feeds only how long the dying last; a location is disabled by
resolveLocationHit at its own locationMaxHp capacity.
The reason it survived reading: for a 10-point neighbour majorWoundFor is 5 and a
leg, abdomen or chest holds exactly 5, and for a 12-point agent both are 6. On the
locations that get hit most the invented rule returns the real one's answer, and
the narration prints MAJOR WOUND and disabled on the same line. Arms and heads are
where they part, and I had not looked at an arm.
Found by the scenario session going to rules.mjs to verify a different correction
of mine and reading the next function along. Both errors made the fight look
easier than it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seed 16 is the missing corner -- party takes first blood in round 1 and is wiped
anyway -- and it runs 24 rounds. Its last five are Neil dropped by a critical
through armour, then Dominic alone at 1%, failing three times, then dying. That
is what three effective attackers costs at a table when a fight goes long.
Corrects a mechanism I had written twice: a disabling hit does not remove an
attacker, it charges 30 points off physical or manipulation per locationEffectsFor.
Bhattacharya keeps swinging at 10 for four rounds. A slope, not a cliff.
Neither guard fired and neither was wrong: the fight sits in the 25.5% the swing
does not cover, and no guard measures the tail because all of them average.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
I told the scenario session that this guard turns a silent re-cast into a build
failure, and it accepted the coupling on that basis. The cast was hardcoded, so
a re-cast would have left it measuring the old six and reporting success.
Reads the <!-- cast: --> marker check-rollable established, drops the two the
scaling note drops by name rather than by position, and refuses to run if the
marker is gone. Verified by re-casting CLEAN GROUND in a throwaway worktree and
watching the guard name the change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Reading both ends of the four-player cut — seed 5's sweep and seed 2's wipe —
says the fight is not decided by round five but by whoever lands the first
disabling hit, on average in round two. Both sides disable on one good blow, so
each one thins the return fire and makes the next likelier; nothing pulls a
fight back toward the middle.
tools/first-blood.mjs measures it: 49.7/50.3 on who strikes first, and 23.1% vs
66.5% wipes on either side of that. Six against six is the control at 8.6% vs
22.4%. A reading tool, not a guard — it records nothing, and no figure from it
goes on a generated page.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tools/playthrough.mjs exists to show a reader why a measured number is what it is. I cited
it to another session — read seed 2, see the wipe the 45% row is made of — and the
reproduction failed in front of them. They reported different outcomes at both seeds and
guessed the cause correctly from outside: the two tools were not drawing from the same
stream.
They were not. simulate.mjs used a private mulberry32 makeRng; playthrough.mjs had its own
LCG written to look like it. Both deterministic, both reproducible alone, and "seed 2"
named a different fight in each — which breaks the only thing the tool is for. Its own
comment claimed a seed here names the same fight there. check-focus carried a third copy of
that LCG, so the two guards described the same game with different dice.
makeRng is exported and both files use it. A playthrough seed is now exactly the first
fight of simulate.mjs --seed <n>: --runs 1 --seed 2 and the playthrough give 9 rounds, 4 of
4 down, 1 dead, both.
The other half was my citation rather than the code: the command I sent omitted --mode, so
it plays both targeting arms and prints two fights. They read the last line, I quoted the
first. The summary line now names the arm.
check-focus re-recorded under the shared stream; figures move a point or two. What it buys
is that the bimodality analysis now reproduces the published means exactly — 2.31, 3.03,
0.40, 0.42 against the four rows CLEAN GROUND publishes. Under the old LCG it agreed to
within a decimal, which looked like corroboration and was two experiments landing near each
other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>