# 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](../workplans/CB-WP-0027-the-commentary-track.md), [ADR-0007](ADR-0007-render-html-not-a-port.md) D5, [ADR-0012](ADR-0012-the-design-instrument.md) D6 (notes), [GameDesign.md](../specs/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 `--record`ed 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 | | `` + 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**: ```rust _ => 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 /command` carries pointer facts. `POST /note` carries text.** > `Note` is a distinct type with **no code path to `GroundCommand`** — > `resolve()` takes a `PointerFact` and 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/-.md`**, exactly as GameDesign §5 already says, with the recording beside it as `trials/-.yaml`. The log is a readable Markdown file with one machine-readable block: ``` | n | round | step | state_hash | comment | ``` **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-game` **only** 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-html` gains a `Note` type and a `/note` route; `PointerFact` is untouched. - `trials/` is created, with `.md` + `.yaml` per trial. - `make trials` reports them; `--self-test` covers 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 |