CB-WP-0045 left "nothing explains what a scope does" open and gave a
FALSE reason: that scoping is our Variant so no printed sentence exists.
The H2 package ships Rules_Text.csv -- twenty-two passages of
player-facing rules -- and nothing in clay-borg had ever read the file.
A wrong reason for an open item is worse than an open item; it retires
the question. Filed as F26 against ground-game: a package that adds a
FILE is invisible where one that adds a column is not.
The scope rule now renders under the table in the edition's own words,
only when a non-global scope is in play, matched by heading rather than
row number, and absent (never paraphrased) if the edition drops it.
The trial log stamps its variant on the begin marker -- a session
property, not an eighth column -- read off state.variant rather than the
--variant flag, because a bare `state.variant = v` leaves H2 inert and a
flag-stamped log would put false provenance on real player words. An
unstamped log reports `unrecorded`, never `ground-darvo-r0`.
Six mutations, six red. The sixth is the finding: every trials.py fixture
built its marker out of BEGIN, so nine checks followed BEGIN away from
what hotseat.rs writes and stayed green while real logs broke. A fixture
built from the constant under test cannot test the constant -- the
control is now a literal, asserted from both sides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
make trials reported "positions unreachable: 4, target 0 — the recording
exists but the position moved". All four were false. A recording holds one
hash, the final state, and reachability asked whether the note's hash was
in that file — so a mid-game note could never match, and a post-game note
from any but the last game could not either. Instance 8 of the ADR-0018
family: vary only WHEN a note was written and the answer flips, with
nothing having moved.
The root cause was not the metric. play again reused state belonging to a
game: it overwrote the previous game's recording (data loss), never
cleared the journal (game 2's log opened with game 1's commands), and so a
note's command index pointed into a recording without those commands.
Fixing reachability alone would have gone green while a session still
destroyed its own evidence.
A note now binds by (game, after) — an index into the recording's own
commands list, which a reader can replay to. The hash keeps a job as the
integrity check at the end of a game, where it can actually fail. Game 1
keeps the path it was given, so GameDesign §5's documented invocation is
unchanged; later games get -2, -3 and nothing is overwritten. Legacy
5-column logs stay readable and are reported as legacy, never as orphans —
an unsubstantiated orphan claim is the defect being fixed.
All three fixes mutation-proven, including at the call site via a real
two-game session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Notes were written to a file and shown back nowhere — the shape
trials.py's own docstring calls this project's signature failure in a new
medium. The log now carries the player's comments where they were made.
Position is the feature: a remark like "why did that do nothing?" is about
the move above it, and collected at the bottom it is a sentence with no
subject. round/step cannot order a note against the log because several
commands share a step, so the server records how many commands had been
played — which it knows exactly — and the page places it there.
A comment must not be readable as something the game did. The log is the
recorder's vocabulary; notes render in their own block, attributed to the
player, quoted. The .note CSS already existed and nothing had ever used it.
No column was added to the trial log. trials.py skipped any row that was
not five cells, silently, so a sixth column would have made `make trials`
report zero notes for every log at once. That latent defect is fixed on
its own terms: a wrong column count now raises, and the walk reports the
real reason rather than blaming a missing block for every failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The meta view beside the table, and a note channel that provably cannot
carry a move.
T01 (ADR-0014). ADR-0007 D5 is SCOPED, NOT AMENDED, and the reason it was
easy is that PointerFact::parse already refuses any unrecognised field --
a comment could not reach the command path even by accident. So /command
carries pointer facts, /note carries text, and Note has no code path to
GroundCommand. Comments live in trials/<date>-<slug>.md, not in
ScenarioFile: a scenario is executed, replayed and hashed, and prose in it
is data the runner must ignore, which is how a format rots. The state hash
binds; round and step are for reading. And the retention question, decided
before any comment was written: RAW NOTES NEVER LEAVE clay-borg. A note
reaches ground-game only by being promoted to a register finding, by a
human, with the wording chosen then -- "the DARVO sequence is infuriating"
is useful signal and a bad way to open a message to the game's designer.
T02. CSS grid, minmax(0,1fr) on both tracks -- load-bearing, because a
grid child defaults to min-content width and without it the SVG table
refuses to shrink and pushes the meta column off-screen, looking correct
on the developer's monitor and broken everywhere else. Single-column
fallback under 64rem. The running tally moved into the panel so it is
visible WHILE PLAYING; it only appeared on the ending page before, and a
score you see once the game is over informs nothing.
T03. A plain <form method="post">, so the box works with the script
disabled; the command channel needs JavaScript because a drag is not a
form submission, a comment is one. 303 See Other so a reload does not
re-post. esc()'s first hostile input: <script>alert(1)</script> renders
escaped AND STILL READABLE -- escaping that eats the player's words is its
own defect. Verified over real HTTP: note posted 303, hostile note stored
as text, empty note refused 400, game did not advance.
T04. tools/trials.py and make trials. THE REPORT'S DESIGN CHANGED BECAUSE
I RAN IT: the first version called any note without a recording an orphan,
so a live session reported every note as broken -- the recording is only
written at game end. A metric that cries wolf is one nobody reads, which
is the exact failure this pass exists to prevent. Now ok / pending /
orphan, and only orphan is a target-0 number. The self-test exercises the
REPORTING path, not just the parser, because design-baseline.py had a
green self-test and an unexercised reporting path and that is where it
rotted.
And a latent Makefile defect surfaced: make trials did nothing, because
trials is also a directory and Make saw an up-to-date file. design,
difficulty and trials -- added by CB-WP-0022, CB-WP-0025 and this pass --
were ALL missing from .PHONY; only the one that collided revealed it.
make all: exit 0. 49 render tests, 26 cb-play, loop-lint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>