e8faac73dbbbe944565e1bc8502ac540aef28a2d
117
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e9f7d458f0 |
check-figures: a cell inherits its column's header
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> |
||
|
|
960fb6be27 |
check-unmarked: the figures a vocabulary cannot name, and a repair to HEAD
|
||
|
|
91a104e682 |
check-figures: the twenty-first guard — figures with nothing holding them
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>
|
||
|
|
c2f7249d4d |
R-295: derive the counterfactual too, and name it hadHeRefused
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> |
||
|
|
a5c4378521 |
R-294: EXPOSURE's tables measured into an artifact, and 58.7% was 74.9%
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
6171d1e9b9 |
CLEAN GROUND v0.19 — the outcome where somebody holds
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> |
||
|
|
3a31e0c9b0 |
R-292: share where a tag starts, not what it says
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> |
||
|
|
8a051f7f2b |
R-291 follow-up: one tag pattern, so the two readers cannot disagree
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> |
||
|
|
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> |
||
|
|
ae8179bd9b |
R-290: the same spelling gap in tagsIn and the POWER marker
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> |
||
|
|
9c2ce9d905 |
R-290: check-outcomes — a beat that names a roll must say what it does
Desk pass 7's finding, turned into the nineteenth guard. check-rollable asks whether a skill is reachable at 25% by the declared cast; pass 5 found that is rollability, not competence; pass 7 found it is also not coverage. A beat could be reachable, well-rated and silent about every band but the one the GM improvises, and the whole suite stayed green. tools/outcome-coverage.mjs measures. Beats are located structurally — the bullet that owns the tag and its children, ending at the next bullet of the same or shallower indent or the next heading, because R-287 is what an unbounded scope does. Band widths are counted by grading all 100 results through gradeRoll, never by arithmetic on fumbleStart, which is off by one and was written wrongly twice this session before being caught. Ratings come from the declared cast via R-286's reader, best-in-cast per skill, because that is the die a table actually rolls. tools/check-outcomes.mjs holds it, in two kinds: RATCHET — stated failure/fumble/special counts may improve and may not regress; unwritten-band exposure may fall and may not rise. --update re-records these, because freezing them would make every improvement fail. HARD CLAIMS — read from what is measured NOW, never from the baseline, so --update cannot silence them: a floor of 18 beats (check-cited once passed with zero citations), no beat bare of every band, every scope structurally bounded and none over 5% of the file, no unparseable tag, no beat naming a skill the cast has no rating for. Verified by breaking it: a stripped beat, a broken tag regex, an unparseable tag and a removed failure case each turn it red; the file and baseline were restored byte-identical after each; and --update with a bare beat present re-records the baseline and still fails. Two bare beats found and filled while building it — the six at the back, and the Insight that is deliberately indistinguishable on a success and a miss, which now says so rather than saying nothing. Coverage 16/21 failure, 5 fumble, 4 special at the start; 18/21, 6 and 6 now. The three figures are cited against the artifact rather than typed, which was the peer session's condition and the right one: pass 7 hand-counted 22 beats where there are 21 (Act Three's warning QUOTES a beat, and a hand count reads the quotation as one — R-286 in the other direction), and its other three counts were stale within one commit. Its first catch was its author: the STATUS line announcing this guard contained a beat-shaped tag and was counted as a beat. The reader was not changed — prose shaped like a beat is what this file is for — and the error message now names the fenced block as the place to write an example. Minutes later check-cited refused a citation-shaped comment in the post-pass about citations. Three readers, three authors describing their own format inside it, each caught by a guard built for a different pass. CLEAN GROUND v0.18. npm run check: 19 guards pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
e3d222cacf |
R-288: an empty cast declaration is fatal; a missing one still falls back
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> |
||
|
|
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> |
||
|
|
0453255ecc |
R-286: one cast declaration, and a mention of one is not a declaration
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> |
||
|
|
67e76639f3 |
R-285: scope the defence-stacking rules to the section that owns them
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> |
||
|
|
aa3ae24ef2 |
CLEAN GROUND v0.16 — the fight it was never measuring, and R-283
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> |
||
|
|
1a2ca44c52 |
R-284: report a wrong figure as a wrong figure, and read the right entry
R-283 left the intact-sentence-wrong-number case falling through to omission, so the guard said BESTIARY "does not state" a rating the page was stating. reads() now has a shape tier between strict and loose: the strict pattern with its value slot loosened, reporting which of the two numbers moved. Scoped the per-creature rules while adding it. They read the whole page, and each is the only rule of its kind today, so a page-wide match found the right line by luck; a second attackFactor creature would have let the courier's rule match that creature's sentence and report the courier correct. They now read the creature's own "### Name" entry -- proved by deleting the courier's line and planting an identical one under the redcap: still omission, where before it would have passed. Five discriminations plus the decoy, proved in a worktree with the message read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
327d060eb1 |
R-283: tell a reworded bestiary sentence apart from a missing one
R-282 matched the page with String.includes, so rewording the exemption sentence reported "BESTIARY never says it is off the stripping ladder" -- sending a maintainer after a sentence that is sitting right there, and never naming the real problem, which is a pattern that has silently stopped reading. Each textual rule now reads twice. Strict is the sentence as it stands and is tighter than before (the bold and the full stop, not the bare clause a substring accepted); loose is the same claim in any wording. Strict passes, loose-only is reported as a reword with the line quoted and the page presumed right, neither is the omission. The loose anchor was wrong on its first pass in the way that matters: "a sentence with 40% and 80%" also matched the courier's own statblock line, so deleting the sentence reported a reword and quoted the statblock back. It now excludes that generated marker, which makes it a test of the claim and not of the digits, and degrades to omission rather than to a false reword. Four discriminations proved in a worktree with the message read in each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1a80d3637c |
R-282: guard the bestiary against the powers, including the next one
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> |
||
|
|
81ff44e70e |
The bestiary was still describing the fight R-275 changed
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> |
||
|
|
303472f0ab |
The bestiary said "in doubt" and the rule no longer means that
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> |
||
|
|
fa484071cd |
R-281: a ratchet, because "helps in all of them" survives the advice decaying
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>
|
||
|
|
3d83fbde93 |
R-280: the band replaced by the test it was standing in for
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> |
||
|
|
a7eca91a9c |
R-278: raising SCAN_RUNS found that SCAN_RUNS had never been read
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> |
||
|
|
32ca982369 |
R-275: the harness had never read a creature's power
41 statblocks carry a POWER in their tactics and simulate.mjs read none of them, so a redcap that ignores the cumulative defence penalty has been measured as a creature that tires -- in check-lethality, in check-focus, and in every figure published about it. The defect was not that the powers were unimplemented, it was that nothing said they were not. powers.mjs classifies all 41: 2 wired, 14 notSimulable with a stated reason, 25 not fight rules. check-powers refuses an unclassified POWER and refuses a notSimulable without a reason -- and it does not test that the harness imports a power, it fights the creature with and without and requires the two to disagree. Moved: the courier 9.4% to 1.0% wiped (it attacks at half while carrying), the redcap 0.7% to 0.9% (small, because these fights rarely spend a second defence). ARGENT AND GULES was wired and then un-wired: it tripled the supporter's wipe rate to 75.2% because the harness has no ground and applied the borough-ground condition unconditionally. Same reason THE PULL is not wired. I had wired one and refused the other on identical facts. Lethality and focus re-recorded, bestiary regenerated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9f3a63174f |
CLEAN GROUND v0.12: convention one-shot, the print pack, and the real opposed roll
Tim settled the last two questions: convention one-shot, and rules.mjs gains a real opposed roll (landed by a peer session at R-273). Both decisions change the document, and the one-shot unblocked the print pack. THE PRINT PACK — docs/scenarios/CLEAN_GROUND_HANDOUTS.html, guard 17. Four A4 sheets, self-contained, no external fonts or assets: H01 the coroner's note on white, H02 the 1962 committee minute on cream with photocopy grain, H03a and H03b the almanac on ruled paper. 11pt floor throughout. The binding constraint is that H03a and H03b must print identically or the almanac trick dies, so that is structural rather than careful: they share one `.almanac` class and every dimension comes from a variable defined once. There is no selector anywhere that names one page and not the other, and check-handouts fails the build if one appears, if their markup structures diverge, if their columns differ, if any row stops reading "41 mi", if the counts stop being forty-one now against fifty-three then, or if the pack and the scenario drift apart. It earned itself immediately: its first run failed my own pack for five rules at 10.5pt, under the house 11pt floor. Rendering was checked visually too, which caught two things no guard would have — the "TO CLEAN GROUND" header colliding with NOTES, and the writing crossing the red margin rule instead of starting right of it. THE ONE-SHOT. Countdown step 6 said "and this is a campaign", which the decision contradicts. Rewritten, and the Close's "leave it filed" ending now says how to land it tonight: do not end on "you'll be back", because the table never will and a hook they cannot take reads as an unfinished scenario. Name the next agent who gets sent, and have Registry thank them. THE OPPOSED ROLL. The scenario-local ruling is deleted; GM ESSENTIALS points at the game. Both beats were re-priced against the real rule by enumerating all 10,000 roll pairs -- exact, no seeds -- and two things fell out: THE OFFER IS PRICED. Ashcroft refusing is taken 21.9% of the time, the safest file at the table. Accepting: 48.7%, past Braithwaite's 37.6%. Accepting does not make him a bit more vulnerable, it makes him the easiest person in the room, and the countdown reaches for the easiest. STEP 5 CAN COME TO NOTHING, 14.5% of the time against an Ashcroft who accepted, and that row fires once and is marked permanent. Previously a silent gap in a climactic beat. It is now a written outcome with read-aloud text: the reach fails, it wears the wrong face for a moment, the party learns what it is and cannot prove it, and the clock still turns to step 6. Also: update-readme's count-word list ran out at sixteen, one guard after its own comment warned about hardcoded lists going stale. It failed loudly rather than silently, so it is an inconvenience and not a defect. Extended. npm run check: 17 guards, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cdbb2e34ee |
R-273: the opposed roll, built where the authority lives
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> |
||
|
|
d03b3208e5 |
check-cited: the counter watching for invisible markers was itself case-blind
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>
|
||
|
|
0de463c470 |
check-cited: make the regex match its own error message, and name the right lines
Two residual defects in the marker check added at
|
||
|
|
4d7d93abc8 |
check-cited: a citation marker nothing reads now fails the build
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> |
||
|
|
8279cdfda8 |
R-272: guard the tail, and stop prose drifting from its artifacts
Three things Tim asked for together: measure the long end of a fight, make the swing check bite on stale prose, and record in rules.mjs the coincidence that hid an invented rule from two readers. GUARD 15 — check-fight-tail, on tools/fight-tail.mjs. Fourteen guards measured this scenario's combat and none could see a long fight, because every one of them averages. The cap is the measurement here, so the tool passes its own (400, not runFight's default 40) and --update refuses to record anything reaching it. Verified by lowering it back to 40: the cut loses 20 fights to the ceiling, the six-a-side 117, 107 of those ending neither won nor wiped, and the recorder stops. Two claims, both read from a fresh measurement rather than the baseline, so --update cannot silence them. "The long end is twice the median" was rejected as a claim because it is true of both encounters and so distinguishes nothing. Instead: a fight past 15 rounds wipes the party materially more often than a short one, and the SIX-A-SIDE fight is the longer one (median 14 vs 10.7) -- R-270 showing up as duration, since a disabled fighter keeps fighting 30 points down. The cut is shorter because it is decisive, not safer. GUARD 16 — check-cited, on tools/check-cited.mjs. check-firstblood and check-attackers catch the game changing; neither reads the document. Re-record after a re-cast and the artifact updates, the guard goes green, and the prose keeps printing the old number under a citation saying where the new one lives. So citations are now machine-readable -- **40**<!-- cite: first-blood cut.swing --> -- and resolved on every build. 25 of them. It failed three times on its first runs, all real: a config keyed "column" that the prose called "line", two figures rounded 32.3 -> 32, and a vacuous pass on zero citations, now fatal in its own right. It also refuses citation of unstable fields. fight-tail.longest may not reach prose: same party, same seeds, same runs, and renaming a config moved it 71 -> 90 rounds, because seedFor derives the stream from the id. Across seven labels -- median spread 0, p95 1, p99 3, longest 21. A sample maximum reads like a bound and is a property of the label. EXPOSURE states p99 instead. Same discipline on the deadlier ratio: 2.42 with seed spread 1.1, so the document gives its direction and declines to quote its size. RULES.MJS — one comment, no rule change. Over resolveLocationHit: its two thresholds are unrelated and usually agree. disabled is a fraction of the pool per location; majorWound is ceil(hp/2) and feeds only dyingLimitFor; neither removes anyone from a fight, which is conditionFor at 2 hit points or a destroyed head. At 10 hp, leg/abdomen/chest capacity is 5 and majorWoundFor is 5, and those locations take 12 of 20 melee results and 15 of 20 ranged -- so two readers reconstructed a rule that does not exist, checked it against the log, and were confirmed by it. The note says to test an arm, the only place the difference shows. Guards verified to bite, not assumed: drift, re-cast, censoring, the longest refusal, the rounding catch and the vacuous-pass catch were each forced and each failed the build with the right guidance, then restored. npm run check: 16 guards, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
74e45b15e7 |
R-270: a rule I invented, and the coincidence that hid it
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> |
||
|
|
765b3518db |
R-269: the fourth quadrant, and a disable does not remove anybody
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> |
||
|
|
0b86c5ffc3 |
R-268: guard the sentence that names a player's character
CLEAN GROUND prints that the four-player cut is three effective attackers and that Ashcroft is not one. Nothing checked it, and a GM reads it aloud to decide who a real player spends four hours being. effective-attackers.mjs measures per attack, not per fight: counted per fight Braithwaite leads on disables, but only because his armour buys him a third more swings -- per attack he is the weakest of the three. Both rates are recorded and only the per-attack one is reasoned from. Asserts exact drift, then the sentence: three clear 5% of attacks disabling, one does not, and that one is Ashcroft. Re-recording does not silence the claim check; verified in a worktree. declared-cast.mjs holds the cast marker reading both scenario guards need, rather than a copy in each. It was briefly named scenario-cast.mjs, which check-scenarios sweeps into the scenario corpus -- its own example marker was read as a real cast. update-readme dropped any guard that exited non-zero, so check-rollable vanished from the README and the count word fell to thirteen while fourteen guards ran. A missing line is now fatal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
60995b28c2 |
R-267 addendum: read the cast, do not copy it
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> |
||
|
|
ce1103ec0d |
R-267: a guarded home for the first disabling blow
Another session verified R-266's measurement, agreed the frame was better than its own, and declined to publish it because the figure had no guarded lineage a doubter could re-derive. It was right, and the fix costs 0.7s of build time -- which is the number I should have checked before calling it a reading tool and not a guard. first-blood.mjs gains --update/--check and is now both the reader and the measurement of record; check-firstblood.mjs is a thin wrapper over it, the same shape as check-bestiary. Compares the recorded figures exactly, then asserts only what a page would claim: first blood lands within 5 points of even in the cut, and its swing exceeds the six-a-side line's by more than both noises. Recording it moved the swing from the scratch run's 43 to 40 against a seed-to-seed spread of 2.1 -- high by more than its own noise, which is the argument in miniature. update-readme's COUNT_WORD could not reach thirteen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
19ed0b5f0f |
R-266: the coin is the first disabling blow, and it lands in round two
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> |
||
|
|
8627fa3536 |
One generator, so a citation names one fight (R-265)
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> |
||
|
|
6e3ea8fa19 |
playthrough: read a scenario's own encounter, against its own cast
The tool could only narrate the guard's fight — the frozen four, at the pack size
focus-baseline records. CLEAN GROUND's EXPOSURE table measures something else entirely: a
declared cast of six at counts of 1, 3, 6, 10 and 15, which is the fight a GM running that
case will actually referee and the one whose numbers we have just re-pinned.
--creature, --count, --party, --seed and --mode now mirror tools/simulate.mjs, so a row
measured there can be read here in words. Positional form still works.
node tools/playthrough.mjs --creature quiet_neighbours_npc --count 6 \
--party ashcroft,bhattacharya,renshaw,braithwaite,pollard,okonkwo --seed 11
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ad5fd57568 |
The harness was fighting a different animal (R-264)
Reading a narrated supporter fight showed it being hit on armR and head. It has wings; it
has never had arms. Three defects, all in the pipeline every published number comes from.
1. It located hits by species, not body plan — simulate.mjs read defender.species where
the game writes spec.bodyPlan ?? spec.species ?? "baseline" into speciesProfile
(build-packs.mjs:294). The barghest, kelpie and church grim were fought on two legs
with arms, and the supporter with no wings, so the fight its tactics call the one the
agents can win could not occur in a measured fight.
2. Armour was one scalar for the whole creature, and the harness's locations had no
armour field. Bare wings, the grounded halving and the vital exemption — R-254 and
R-255 — were invisible to every number. Worn armour was summed the same way, which put
a stab vest on a cleaner's head.
3. Nothing was ever grounded: a wing could be ruined and the creature kept flying.
Fixed in the game's order — locate, then apply what that location carries, every term
imported from rules.mjs. A ruined wing calls groundedPlanFor and the wounds carry across
by severity through remapLocationDamage, the function _preUpdate uses.
A bug of mine no guard would have caught: remapLocationDamage returns { damage, moved,
rescaled } and my first draft passed the whole object where a damage map was expected, so
every wound a creature carried was forgiven the moment it came down. check-lethality would
have passed it — fewer wounds means a longer fight, which reads as a number moving, and
this commit moves numbers. Found by probing a landing by hand.
20 of 47 creatures moved, 18 deadlier and 2 less. The supporter goes 69.3% -> 25.1% wiped,
3.38 -> 2.16 down, second deadliest to fourth: it was being measured as a 30-hit-point
creature in uniform armour 9 that could not be grounded. The small rises elsewhere are the
party's armour no longer covering locations it never protected.
check-focus then caught the page overclaiming, which is what it is for: focus fire still
helps in all 30 but only 28 clear their own noise where 31 of 31 did. The guard was
asserting more than the page needs — the bestiary prints that count from the artifact and
cannot overstate it — so it now checks only what the page asserts outright, and the page
rewrote itself to "in 28 of them".
Both baselines re-recorded. Minor version, not a patch: the published numbers changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5dcaaf519a |
The simulator can be read now, not just totalled (R-263)
Every guard here reports a number per creature, and a number cannot say why a fight went the way it did. That gap is what produced R-258 through R-260: each written from a side harness built to watch a fight, each modelling something slightly different from the game, two of the three wrong in ways no guard could catch because no guard was involved. runFight takes an optional `say` sink. It reports what the simulator already decided — the attack roll and its target, a defence and the penalty it was made at, the landing level after a dodge downgrades it, damage against armour before and after armourAgainst, the location, major wounds, disablement, death. It never touches the generator, so a narrated fight and a silent one are the same fight; check-lethality and check-focus both still match their baselines exactly with the hook in place. tools/playthrough.mjs (npm run play) is its consumer, in the same commit deliberately: a hook with no reader is the exact defect this project keeps finding in its own rules, and adding one to the measurement pipeline with only a scratchpad file calling it would have been committing the thing I have spent the session removing. Pack size comes from focus-baseline.json so the fight you read is the fight check-focus measures; creatures recorded as pinned are played solo. Three redcaps, seed 20260913, same seed both ways. Spread fire: wiped in 10 rounds, 3 dead, and all three redcaps still standing — 39 hit points spread three ways so that none of it finished anything. Focus fire: same opening, diverging at one target choice in round 1, party wins in 20 with two up. The +13.2 points check-focus records, seen once instead of averaged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1d915c55cf |
check-focus: the focus-fire claim is an artifact, not a sentence (R-261)
R-258 put a tactic on a GM-facing page from an unguarded harness, R-259 found it worth
nothing, R-260 found the replacement right for the wrong reason. Three corrections, each
landing as prose with nothing checking it — the arrangement that let
|
||
|
|
555fa761c0 |
check-rollable: guard eleven, because a real skill is not the same as a route
check-scenarios resolves every skill a scenario NAMES against the catalogue, which is a different question from whether anybody present can roll it. CLEAN GROUND shipped four commits with three clues gated on Track (base 10), Navigate (base 10) and Science (Botany) — base 1, so the 1% floor was the whole of it — and every guard passed, because all three are perfectly real skills. A desk playtest found it by auditing the acts against the sheets the scenario casts. That audit is mechanical, so it belongs in the build. Two tiers. Corpus-wide, some roster agent must reach VIABLE for every skill any scenario names. Per scenario, a document that DECLARES its cast is held to that cast instead, and that is the tier that catches this defect class. The cast is declared rather than inferred, and that is the interesting part. The first version scraped pc_ keys out of the prose and swept up the substitutes named in CLEAN GROUND's player-count scaling — a cast of nine instead of six, which put Sandoval and his Track 35 in scope and made the guard pass the very bug it was written for. A guard that guesses the cast is worse than none, because it reports success. CLEAN GROUND now carries a cast comment and the guard reads it off the raw text, since scenarioText strips HTML comments. Verified load-bearing rather than assumed: re-injecting the original Track tag into the real CLEAN_GROUND.md fails the guard, naming the skill, the base chance and the declared cast. Worth recording that the corpus-wide tier would never have caught it — Lindqvist trains Botany at 40, so it is rollable by the roster and simply not by the six who were cast. The first fixture test passed for that reason and misled me; only the declared-cast tier finds this. VIABLE is 25 and is justified, not picked: it is the commonest base chance in the catalogue, what an untrained agent brings to Spot, Listen or Brawl, so it is the level the game itself treats as worth attempting. Below it a clue is not gated, it is buried. check-scenarios exports its tag parser rather than growing a second copy, behind the invokedDirectly pattern simulate.mjs already uses; a duplicated parser is exactly what this repository's one standing law forbids. update-readme then caught me fairly — it cross-checks the advertised guard list against npm run check — so the guard is registered there too and the README advertises eleven in all three places. No REVIEW_LOG entry: the log is clean at R-260 and is being appended to every few minutes by concurrent work, so the end of that file is the likeliest place to collide. Left for whoever next touches it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
48216bae10 |
Focus fire across all 47, and the same confound one turn later (R-260)
Measured in simulate.mjs. For each creature, the pack size whose spread-fire win rate lands nearest 50% — a fight at 0% or 100% cannot show an effect, which is why R-259's first sample was three-quarters useless — then focus versus spread, 2000 fights across three seeds. 31 of 47 creatures had a pack size that could show anything. Focus fire helped 31 of 31, by more than that row's own noise 31 of 31 times, mean +12.8 points. Best is 6x The margin, 40.4% -> 61.1%. The other 16 are unmeasurable rather than unaffected: eleven are trivial six at a time, five are hopeless in pairs. R-259's decomposition was confounded, and I wrote it while correcting a confound. It credited the defence ladder with 4-5 points by comparing dodge <=30% against dodge >=55% — but the low-dodge samples were PAIRS and the high-dodge ones TRIOS. Held at a fixed pack size, dodge is worth 1-2 points. Pack size is the driver: 8.0 points for a pair, 14-15 for three or more, then flat. What focus fire buys is that a dead creature stops attacking and a half-dead one does not attack any less. Two failures, one cause: comparing groups that differ in more than the thing being measured. The control is cheap and I did not run it until the third time. The bestiary advised concentrating fire and gave the ladder as the reason — right advice, wrong reason. It now gives the reason that survives measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
995f3fa94e |
The firing order was worth nothing, measured properly (R-259)
R-258 put a tactic in the bestiary on the strength of a table-side harness — cheapest weapon first, best weapon last, 10.7 points of win rate across 24 orderings. Measured in simulate.mjs, the pipeline every published number comes from, it is worth +0.27 points, with per-seed differences of -1.5 to +1.8 against 2.6 points of noise on a single arm. Zero. The toy lied for two reasons, both mine. Its party had one uniquely valuable weapon (an anti-materiel rifle the borough does not own, so unhalved, worth double anything else) so there was something to sequence around; the frozen party is 4.09, 3.98, 1.43, 1.43 — two near-identical pairs. And the toy fixed the order every round, while the game re-rolls Reaction every round and has no rule for holding an action, which I checked before measuring. The order is not a decision the rules offer, and I had written table advice for it. The page is corrected: the ladder stays, because it is derived and true, and it now says you cannot choose who goes first. What is worth doing, measured the same way: committing a round's attacks to one target, in fights that can actually move (a fight at 0.2% or 100% cannot show an effect and three of my first four samples were pinned there). Against dodge <= 30% focus fire gains 7.6 points, which is just killing attackers sooner; against dodge >= 55% it gains 12.2, so about 4-5 points is the defence ladder and the rest is arithmetic unrelated to dodging. Separating them needed contested fights at both ends of the dodge range — without that control I would have credited all 13.8 points against three redcaps to the ladder, which is R-255's mistake again. runFight gained two hooks, off by default and unused by the baseline: partySequence rearranges the party within the slots initiative already gave them, leaving enemies where they fell so the experiment isolates agent order from who acts before the creature; and partyTargets "focus" concentrates fire. Both roll the same dice, and check-lethality confirms all 47 creatures still fight exactly as recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f932a79121 |
The firing order, in the bestiary, generated (R-258)
DEFENCE_STEP is -30, so a creature's Dodge is not a percentage it has for a round — it is one it has once or twice. The median dodge in the book is 45% and goes 45 -> 15 -> 1 inside a single round. No player-facing document said so, which hid a real decision: what order the party shoots in. The bestiary now has a section on it. Only a landing hit spends a defence, so a miss strips nothing; each landing hit makes the next attack of that round easier; therefore the cheapest reliable weapon fires first and the one you most need to land fires last — against the supporter, the weapon the borough does not own. Everything it states is generated from DEFENCE_STEP, resolveBands and the statblocks: the step, the ladder, the median, the spread (13 of 47 creatures stripped by one landing hit, 25 by two, 9 by three) and the list of the hardest to strip. Negative-tested by setting the step to -15 and -20; the page rewrites itself both times. No win rates are published: the ordering measurements come from a table-side harness that ignores wound penalties, bleeding and dying, so the page states the mechanism and quotes no percentages. My first draft used the dodgiest creature in the book as the worked example — 80%, three landing hits to strip — directly beneath a claim that the first attack of the round eats the dodge. True of a modest dodge, false of a good one, and both were on the same page. tools/bestiary.mjs --check has existed since the document did, with the comment "(for the guards)", and no guard ever ran it: a check written and wired to nothing, the same shape as every rule found computed and never read. That was tolerable while the page restated statblocks and is not now that it derives from rules — change DEFENCE_STEP and the document lies to a GM. check-bestiary.mjs is the tenth guard, negative-tested both ways it can fail: a rule change that staler the page, and a hand-edit of the page. update-readme flagged it as "run by the build, not advertised" before I listed it, and the README's count word went from nine to ten by itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c3a337314f |
One armourAgainst, in the authority, shared with the harness (R-257)
Left open by R-256. Two rules decided how a hit meets armour — armourAgainst (a critical
ignores it, a special halves it, rounded down) and damageAfterArmour (what is left never
goes below zero). Both lived in ringbrp.mjs, which is the game. Neither lived in
rules.mjs, which is the authority, so the guard that forbids redefining a rule had
nothing to forbid. tools/simulate.mjs applied armour from its own inline copies of both,
as it had since it was written.
They agreed, which is the whole point. Nothing published was wrong and no guard could
have said they were two things rather than one. But every number in BESTIARY.md, every
row of the lethality baseline and every measurement quoted from R-251 onward comes out of
that harness: change how a special hit meets armour and the game changes, the numbers
describing the game do not, and all nine guards still pass. Same shape as
|