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>
4.3 KiB
| id | kind | title | status | state_hub_workstream_id |
|---|---|---|---|---|
| CB-WP-0032 | product | The comments in the account | done | 71d744f9-7e40-49c3-994e-f3fb007a1430 |
Purpose
structural tier S (one existing renderer gains a second input; no new
decision, no dependency, no budget moved)
chaos d8 = 6 → no override
declared tier S
Declaration 3 of chaos window 3.
The report
"Commenting after the game is a good practice. We could show the comments in the log. I would think that as useful."
This closes the gap CB-WP-0031
named and did not fix: notes were written to a file and shown back
nowhere. trials.py's own docstring calls that shape "this project's
signature failure in a new medium" (ADR-0014 D6) — and the note channel
had it, in the interface, for as long as it has existed.
The decision: what a comment is positioned against
A note carries round, step and state_hash. None of them can order
it against the log, because the log is a sequence of commands and
several commands share a step. Positioning by round would put a comment
somewhere in the right round — for a remark about a specific move, the
wrong position stated confidently.
So the server records how many commands had been played when the note
was written, which it knows exactly (journal.len()), and the page places
it there. after = 0 is before anyone moved, which is a real thing to
have an opinion at.
The thing that must not happen
A comment must not be readable as something the game did. The log is
the recorder's vocabulary — record::to_step, what the scenario file will
say — so a note styled as a log line would be a sentence the game never
produced, sitting in the account of what the game produced. ADR-0014 D4
turns on that distinction being visible: raw notes stay in this repo, and
only a human promotes one to a finding.
Notes render in their own .note block, attributed "you — round N,
step", with the words in quotes. The .note CSS already existed and was
never used by anything — defined for this, and left unwired.
What was NOT done, and why it is the interesting half
No column was added to the trial log, though after is genuinely the
anchor and persisting it would let a later reader interleave.
trials.py skipped any row that was not five cells — with continue, in
silence. A sixth column would have made make trials report zero notes
for every log at once, which is verbatim the failure the parser's own
docstring says it exists to prevent. after is therefore held in memory
for the session and the file format is untouched.
The latent defect was fixed on its own terms: a wrong column count now raises, and the walk that wraps it reports the real reason instead of blaming a missing block for every failure.
Task: show the comments where they were made
id: CB-WP-0032-T01
status: done
priority: high
state_hub_task_id: "31f60440-e049-45f7-99fa-87c01c00adb7"
Controls:
- placement is asserted, not just presence — mutation-proven: append the notes at the end instead of interleaving and the test goes red;
- a comment cannot be read as a game event — asserted on its own marking and its attribution;
- a comment past the end of the log still appears, because a post-game note is written when every command has been played and a dropped comment is the failure the channel exists to avoid;
- the round trip, not the write — the
cb-playtest posts a note and then GETs the page, which is the path a player takes.
Done 2026-08-07.
Account { lines, notes } replaces the bare &[LogLine] on both pages —
a struct because document_with_log was already at seven arguments, and
because the two are only meaningful together, which is the point.
trials.py hardened as described above, with two new controls: a
wrong-shaped row raises, and the walk names the reason.
Not done here
- Nothing lets a player edit or delete a comment. A note is a record of what they thought at the time and rewriting it would defeat that, but a typo is a real case and there is no answer for it yet.
- The ending page shows the same interleaved account, so a long game puts the post-game comment at the bottom of a long log. Whether that is where a reader looks for it is untested.