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:
tegwick 2026-08-06 10:37:46 +02:00
parent 5c6e322d5f
commit 4fb506fcd1
11 changed files with 1002 additions and 10 deletions

View file

@ -2,7 +2,7 @@
id: CB-WP-0027
kind: product
title: "The commentary track: the meta view beside the table, and what the player says while playing"
status: ready
status: active
state_hub_workstream_id: "e011da8d-de9e-48bf-a3f2-a9e715ef222f"
---
@ -80,7 +80,7 @@ write-only.
```task
id: CB-WP-0027-T01
status: todo
status: done
priority: high
state_hub_task_id: "3586f3af-ad45-4bc6-bea2-b6eaedf05fbf"
```
@ -117,11 +117,40 @@ about his own game**, and one of them will eventually be unflattering
about the design or about the engine. State whether they are private to
the repo, whether they travel to `ground-game`, and who decides.
**Done 2026-08-06.**
[ADR-0014](../decisions/ADR-0014-the-commentary-track.md), six decisions.
**(b) turned out to be the easy one, because the control already exists.**
`PointerFact::parse` **refuses any unrecognised field**, so a comment
cannot reach the command path even by accident. **D5 is scoped, not
amended**: `/command` carries pointer facts, `/note` carries text, and
`Note` has **no code path to `GroundCommand`**`resolve()` takes a
`PointerFact` and nothing else. The control is a test that posts a note
whose body is a well-formed pointer fact and asserts the game does not
advance; without it, "separate channels" is a claim about code layout.
**(a) the trial log, not the scenario** — and the reason is not
strictness. A scenario is executed, replayed and hashed; prose in it is
data the runner must ignore, which is how a format rots. The log reuses
`FindingRegister.md`'s idiom: a table between HTML-comment markers, so
`design.py`'s parser is the model.
**(c) the state hash binds; round and step are for reading.** An orphaned
comment — hash no longer in its recording — is **reported, not deleted**,
same reasoning as `gr-e01`'s rewrite and a green reproduction's alarm.
**Retention: raw comments never leave clay-borg.** A comment reaches
`ground-game` only by being promoted to a register finding, by a human, at
which point the wording is chosen deliberately. *"The DARVO sequence is
infuriating"* is useful signal and a bad way to open a message to the
game's designer. **The maintainer decides per comment** — not a rule, not
a default, not the agent.
## Task: the table on the left, the meta on the right
```task
id: CB-WP-0027-T02
status: todo
status: done
priority: high
state_hub_task_id: "0971185c-b72c-43ec-a356-c221ef6d8165"
```
@ -150,11 +179,29 @@ not part of the table.
- the game column must be usable with the meta column collapsed. A player
who does not want to write anything must not be worse off.
**Done 2026-08-06.** CSS grid, `minmax(0,1fr)` on both tracks and a
single-column fallback under 64rem.
**`minmax(0, …)` is the load-bearing part.** A grid child defaults to
min-content width, so without it the SVG table refuses to shrink and
pushes the meta column off-screen instead of narrowing — the layout would
look correct on the developer's monitor and be broken everywhere else.
**The running tally moved into the panel**, so it is visible *while
playing*. It only appeared on the ending page before, and a session score
you see once the game is over informs nothing.
Three tests: the log is in the meta column and the table is not; an empty
panel draws **no furniture** (with the inverse, so it does not pass for a
panel that never renders); and the narrow-viewport rule exists — asserted
on the stylesheet, which is **weaker than laying it out**, and said so
rather than dressed up.
## Task: capture what the player says, bound to where they said it
```task
id: CB-WP-0027-T03
status: todo
status: done
priority: high
state_hub_task_id: "4a23ba0d-9b23-4381-b6b0-2b8959106270"
```
@ -178,11 +225,33 @@ first time that comment is load-bearing rather than precautionary.
- **the game is playable with comments disabled**, and a test says so. The
commentary track must not become a dependency of playing.
**Done 2026-08-06.** `Note` in `input.rs`, a `/note` route, `--trial`, and
`trials/<date>-<slug>.md` written whole on every note.
**The separation is structural, and tested anyway.** A note whose text is
a well-formed pointer fact (`down=action-solve&up=problem-1`) parses as a
note, is stored verbatim, and **the command channel refuses the same body
outright** — with the inverse asserted too, so the test is not vacuous.
**A plain `<form method="post">`**, so the comment box works with the
script disabled. The command channel needs JavaScript because a drag is
not a form submission; a comment is one, and making it depend on the
script would add a failure mode for nothing. The reply is **303 See
Other**, so a reload does not re-post the note.
**Verified over real HTTP**, not only in tests: two columns served, a note
posted (303), a hostile note stored as text (303), an empty note refused
(400), and the game did not advance.
**`esc()`'s first hostile input.** `<script>alert(1)</script>` renders
escaped **and still readable** — the test asserts the player's words
survive, because escaping that eats the text is its own defect.
## Task: surface them, or this pass has failed
```task
id: CB-WP-0027-T04
status: todo
status: done
priority: high
state_hub_task_id: "df8afbef-6ff7-4aa2-9ff7-b3fa05f676cc"
```
@ -209,6 +278,28 @@ session that produced them, by a command, without opening a file by hand.
`tools/design.py` accepts — if the register cannot express it, the shape
is wrong and that is a finding about ADR-0012, not a bug.
**Done 2026-08-06.** `tools/trials.py`, `make trials`, wired into
`make self-tests`.
**The report distinguishes three states, and that distinction came from
running it.** The first version called any note without a recording an
**orphan** — so a live session reported every note as broken, because 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` (no recording yet), `orphan` (the recording exists
and the position is not in it). Only the third is a target-0 number.
**The self-test exercises the reporting path**, not just the parser — it
runs `report` against a fixture and asserts on what it printed.
`design-baseline.py` had a green self-test and an unexercised reporting
path, and that is where it rotted (ADR-0012 D8).
**A latent Makefile defect surfaced.** `make trials` did nothing: `trials`
is also a *directory*, so 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. All three are declared now.
## Task: evidence
```task