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>
8.9 KiB
ADR-0014: a second channel that cannot carry a move, and a trial log that a command can read
status: accepted date: 2026-08-06 decided by: agent, under the standing loop authorization tier: M (structural M — touches ADR-0007 D5's input contract and the recorded-session format; chaos d8=7 → no override). Tier M merges survey and decision into one document, which this is. references: CB-WP-0027, ADR-0007 D5, ADR-0012 D6 (notes), GameDesign.md §3.1 and §5
Context
A test player wants to say why they played a move, and what confused them, while the position is still on screen — and have it reviewable later.
GameDesign §5 already specifies the artifact: a trial is a
--recorded session plus a sibling observation log. What it does not
specify is how the log gets written, and as written it asks the player to
reconstruct their reasoning afterwards from memory. That is why no trial
log exists yet.
The survey, such as it is
The candidates are all in this repo, so there is nothing external to benchmark against — which is why tier M's merged form is enough.
| existing mechanism | what it does | why it is not this |
|---|---|---|
provisional: true + owner + date |
marks an undecided rule | about the dataset, not about play |
| the finding register | records findings with reproductions | needs the reproduction this is meant to supply |
<!-- design-register:begin --> + a table |
a machine-readable block inside a human document | the right shape — reused below |
| the game log | what happened | not why, and not how it felt |
.cbreplay bundles |
the position, replayable | the anchor, not the annotation |
The one thing worth stealing is the register's own idiom: a table
between HTML-comment markers inside a readable Markdown file, parsed by a
small tool. It survived contact in FindingRegister.md, design.py
already parses that shape, and it means a trial log is readable by a human
without a tool and by a tool without a parser library.
D1 — a comment is not a command, and the types must make that true
ADR-0007 D5 is scoped, not amended.
D5 says JavaScript may not construct commands — the page reports raw pointer facts, and Rust decides what they mean. That rule is about the command channel and it stays exactly as it is.
The relevant fact is that PointerFact::parse refuses any unrecognised
field:
_ => return Err(format!("unrecognised field in pointer fact: {pair:?}")),
So a comment cannot be smuggled through the existing path even by accident. Rather than widening that parser, the comment gets its own one, and the separation is structural:
POST /commandcarries pointer facts.POST /notecarries text.Noteis a distinct type with no code path toGroundCommand—resolve()takes aPointerFactand nothing else, so a note cannot become a move by any route, including a future careless one.
Control: a test posts a note whose body is a well-formed pointer fact
(down=action-solve&up=problem-1) and asserts no command results and
the game does not advance. Without it, "the channels are separate" is a
claim about code layout rather than about behaviour.
What would falsify D1: if Note ever acquires a field the engine
reads, the separation is gone and ADR-0007 says to revisit the decision
rather than widen the control. The note type carries text, a position, and
a timestamp. Nothing else.
D2 — comments live in the trial log, not in the scenario
Not in ScenarioFile. It is deny_unknown_fields and CB-WP-0026 just
demonstrated why that is right — but the reason to keep prose out is not
strictness, it is that a scenario is a game record and must stay one.
Scenarios are executed by make sim, replayed by replay-test, and
compared by hash. A player's opinion in that file would be data the runner
must ignore, which is how a format rots.
In trials/<date>-<slug>.md, exactly as GameDesign §5 already says,
with the recording beside it as trials/<date>-<slug>.yaml.
The log is a readable Markdown file with one machine-readable block:
<!-- trial-log:begin -->
| n | round | step | state_hash | comment |
<!-- trial-log:end -->
Reusing FindingRegister.md's idiom deliberately — same marker shape,
same table form, so design.py's parser is the model and a reader who has
seen one has seen both.
D3 — the binding is the state hash, with round and step for humans
A comment records state_hash (cb_events::state_hash_hex), plus
round and step.
- The hash is the binding. It is what already makes a session
comparable to its replay (
Summary::end_state_hash), so a comment keyed to it names a position a reader can reach, not merely describe. - Round and step are for reading. "Round 3, Resolve" orients a human instantly; a hash does not.
A comment whose hash no longer appears in its recording is reported as
orphaned, not deleted. The position may have moved because the engine
changed, and that is worth knowing — it is the same reasoning that made
gr-e01 a rewrite rather than a deletion (CB-WP-0021 T03), and the same
signal as a reproduction that has gone green (GameDesign §1.3).
D4 — comments stay here; only findings travel
The retention question, decided before any comment is written.
These are the maintainer's own words about his own game, written in the moment, and some will be unflattering about the design, the engine, or both. That is the point — a commentary track that people self-censor into is worthless.
Raw comments never leave clay-borg. Nothing auto-forwards to
ground-game.A comment reaches
ground-gameonly by being promoted to a register finding and reported through the existing path — which requires a human to promote it, and requires the finding to meet GameDesign §1.
So the trial log is a private notebook, and the register is the published surface. The promotion step is where wording gets chosen deliberately, which is the right place for it: "the DARVO sequence is infuriating" is useful signal and a bad way to open a message to the game's designer.
Who decides: the maintainer, per comment, at promotion time. Not a rule, not a default, and not the agent.
D5 — a comment is a note, and inherits the note tier
A promoted comment enters the register as a note (ADR-0012 D6): kind
by judgment, state note, 30-day expiry on the existing machinery.
It is not automatically a finding, even though it has a position. GameDesign §1 wants a reproduction that shows the claimed thing — a recording proves the position existed, not that anything is wrong with it. "This felt pointless" plus a replayable state is a strong note and still a note.
What promotes it further is the same as for any other note: an artifact demonstrating the defect. The trial log makes that cheap, because the position is already recorded and someone can go and build a scenario from it.
D6 — surfacing is the deliverable
make trials reports every comment with its position and age, across all
trial logs, and flags orphans.
Storage without surfacing would make this the third instance of this project's signature failure — after the four-day unread message and the ten uncollected rulings. The workplan says no task may be called done while comments are write-only, and this ADR agrees: T04 is the pass, T03 is plumbing.
Control: the reporting path is exercised by --self-test, not only
the parser. design-baseline.py had a green self-test and an unexercised
reporting path, and the reporting path is where it rotted (ADR-0012 D8).
Consequences
cb-render-htmlgains aNotetype and a/noteroute;PointerFactis untouched.trials/is created, with.md+.yamlper trial.make trialsreports them;--self-testcovers the reporting path.specs/GameDesign.md§5 gains the log's concrete format — it specified the artifact and not its shape.- Comments are escaped on render (
esc), and this is the first user-authored text this renderer has handled.
What was rejected
| rejected | why |
|---|---|
comments inside ScenarioFile |
a scenario is a game record; prose there is data the runner must ignore |
widening PointerFact to carry text |
D5's parser refusing unknown fields is a control, not an inconvenience |
| free-form prose with no machine-readable block | unreadable by a command, which is the failure mode this exists to avoid |
auto-forwarding comments to ground-game |
invites self-censorship, and the promotion step is where wording belongs |
| treating a positioned comment as a finding | a recording proves the position existed, not that anything is wrong |
| deleting orphaned comments | a moved position is a signal, per CB-WP-0021 T03 |