CB-WP-0027 T01-T04: the commentary track
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>
This commit is contained in:
parent
5c6e322d5f
commit
4fb506fcd1
11 changed files with 1002 additions and 10 deletions
197
decisions/ADR-0014-the-commentary-track.md
Normal file
197
decisions/ADR-0014-the-commentary-track.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# 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 |
|
||||
| `<!-- 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**:
|
||||
|
||||
```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/<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-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 |
|
||||
Loading…
Add table
Add a link
Reference in a new issue