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>
326 lines
14 KiB
Markdown
326 lines
14 KiB
Markdown
---
|
|
id: CB-WP-0027
|
|
kind: product
|
|
title: "The commentary track: the meta view beside the table, and what the player says while playing"
|
|
status: active
|
|
state_hub_workstream_id: "e011da8d-de9e-48bf-a3f2-a9e715ef222f"
|
|
---
|
|
|
|
# Purpose
|
|
|
|
```
|
|
structural tier M (touches two canonical interfaces — ADR-0007 D5's
|
|
input contract, which currently admits only pointer
|
|
facts, and the recorded-session format, which is
|
|
`deny_unknown_fields`)
|
|
chaos d8 = 7 → no override
|
|
declared tier M
|
|
```
|
|
|
|
Declaration 10 of chaos window 2. Tier M: survey and decision merged into
|
|
one document; adversarial review optional.
|
|
|
|
## The maintainer's ask
|
|
|
|
> *"Extend the UI to provide the evaluation meta view to the game. Game
|
|
> interface to the left, meta and commentary to the right. As a test
|
|
> player I can provide comments about how I perceive the game or why I am
|
|
> playing a move — signal about unclear situations, frustration etc. —
|
|
> bound to the game state, reviewable for potential optimizations."*
|
|
|
|
## This is `GameDesign.md` §5's trial protocol, made ergonomic
|
|
|
|
**The protocol already exists and nobody would use it as written.** A
|
|
trial game is a `--record`ed session plus a sibling `trials/<date>.md`
|
|
written *afterwards*, from memory. The spec says it "must cost almost
|
|
nothing or it will not be done" — and then asks the player to reconstruct
|
|
their own reasoning after the fact.
|
|
|
|
A comment typed **while the position is on screen** is better in three
|
|
ways that matter to this project:
|
|
|
|
1. **It is contemporaneous.** *"Why am I playing this"* is available for
|
|
about ten seconds and then it is gone.
|
|
2. **It is bound to a position**, so it points at something replayable
|
|
rather than at a memory.
|
|
3. **It captures the class of signal nothing else here can.** Confusion,
|
|
frustration, and *"I don't understand what this does"* are invisible to
|
|
every gate in the project, and they are the observations that produce
|
|
design findings rather than engine findings.
|
|
|
|
## Why this is worth more than a comment box
|
|
|
|
**GameDesign §3.1 makes a note unreportable until it has a reproduction.**
|
|
Three findings are stuck there right now: F12 (GR-A13 wasted SOLVE), F14
|
|
(GR-E03/E04 never played) and F15 (the series question). Each is real and
|
|
none can go to `ground-game`.
|
|
|
|
**A comment bound to a recorded state, plus the recording, is the cheapest
|
|
reproduction a play-derived observation can have.** *"This felt pointless"*
|
|
attached to a replayable position is a different artifact from *"this felt
|
|
pointless"* in a file — the reader can go and look.
|
|
|
|
So the pass is not "add a text box". It is **closing the register's input
|
|
path for findings that come from playing rather than from building.**
|
|
|
|
## The failure this must not reproduce
|
|
|
|
**A pile of prose nobody reads is this project's signature defect.** The
|
|
message that sat unread for four days; the ten rulings that arrived and
|
|
were never collected. A commentary feature that stores comments and
|
|
surfaces them nowhere is that failure in a new medium, and it would be the
|
|
third instance.
|
|
|
|
> **Storage is not the deliverable. Surfacing is.**
|
|
|
|
T04 carries this and no task may be called done while comments are
|
|
write-only.
|
|
|
|
## Task: decide where comments live and what the page may send
|
|
|
|
```task
|
|
id: CB-WP-0027-T01
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "3586f3af-ad45-4bc6-bea2-b6eaedf05fbf"
|
|
```
|
|
|
|
`decisions/ADR-0014-*.md` (tier M merges survey and decision).
|
|
|
|
**Three questions, and the second is the one that bites.**
|
|
|
|
**(a) Where does a comment live?** Candidates: inside the recorded
|
|
`ScenarioFile`; in the `trials/<date>-<slug>.md` sibling `GameDesign.md`
|
|
§5 already specifies; or a third file keyed to the recording. Note
|
|
CB-WP-0026 added four fields to `ScenarioFile` and the schema is
|
|
`deny_unknown_fields` — **a scenario is a game record, and mixing a
|
|
player's prose into it may be the wrong shape.** Say which and why.
|
|
|
|
**(b) What may the page send?** ADR-0007 **Decision 5** currently binds
|
|
the browser to *raw pointer facts* — `down on id`, `up on id` — and
|
|
forbids JavaScript from constructing commands. **A comment is not a
|
|
command**, so D5 is arguably untouched; but the transport widens from two
|
|
ids to arbitrary user text, and that must be *decided* rather than
|
|
assumed. State whether D5 is amended or merely scoped, and what stops the
|
|
new channel from becoming a command channel later.
|
|
|
|
**(c) What binds a comment to the game?** Round and step are readable but
|
|
weak. **The end-state hash is the strong binding** — it is what makes a
|
|
session comparable to its replay (`Summary::end_state_hash`), so a comment
|
|
keyed to a hash points at a position a reader can reach. Decide whether
|
|
the binding is the hash, the command index, or both, and what happens to a
|
|
comment whose position no longer replays.
|
|
|
|
**Also decide the retention question**, because it is a boundary this
|
|
project has not faced: these comments are **the maintainer's own words
|
|
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: done
|
|
priority: high
|
|
state_hub_task_id: "0971185c-b72c-43ec-a356-c221ef6d8165"
|
|
```
|
|
|
|
Two columns: the game as it is today on the left, a meta panel on the
|
|
right. The log moves into the right column — it is commentary on the game,
|
|
not part of the table.
|
|
|
|
**What goes in the meta panel**, beyond the log and the comment box:
|
|
|
|
- the **session tally** (CB-WP-0024 T04), which currently only appears at
|
|
the end;
|
|
- the **register's open notes** for this game, if any — the point of the
|
|
panel is that the player can see what has already been observed;
|
|
- whatever CB-WP-0025's witness produces, **when it is wired** — it is not
|
|
yet, and this task does not wire it.
|
|
|
|
**Controls:**
|
|
- the layout must survive a narrow viewport — a two-column desktop layout
|
|
that unreadably overlaps on a laptop is a regression, and the existing
|
|
page is responsive by accident rather than by design, so this is the
|
|
moment it becomes deliberate;
|
|
- **every existing coverage probe must still pass.** `RENDERED` in
|
|
`lib.rs` asserts each view field appears in the *parsed* document; a
|
|
reflow that drops one fails the build, and that is the gate working;
|
|
- 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: done
|
|
priority: high
|
|
state_hub_task_id: "4a23ba0d-9b23-4381-b6b0-2b8959106270"
|
|
```
|
|
|
|
A comment box in the meta panel, posting to the existing guarded endpoint.
|
|
|
|
**The escaping is not incidental.** `esc()` exists and every interpolated
|
|
value goes through it, with a comment in the source saying *"cannot
|
|
currently carry a `<`" is how injection bugs are written*. This is the
|
|
first user-authored free text the page has ever handled, so it is the
|
|
first time that comment is load-bearing rather than precautionary.
|
|
|
|
**Controls:**
|
|
- **a comment containing `<script>` renders as text**, asserted on the
|
|
emitted document — the first real test of `esc()` against hostile input
|
|
rather than against suit names;
|
|
- a comment is bound to the position per T01's decision, and a test
|
|
asserts the binding **resolves**: a stored comment names a state that
|
|
the recording can reach;
|
|
- the empty comment is not stored — a blank note is noise in the register;
|
|
- **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: done
|
|
priority: high
|
|
state_hub_task_id: "df8afbef-6ff7-4aa2-9ff7-b3fa05f676cc"
|
|
```
|
|
|
|
**The load-bearing task.** Comments must be readable *outside* the
|
|
session that produced them, by a command, without opening a file by hand.
|
|
|
|
- `make design` (or a sibling) reports comments **with their positions**,
|
|
because a comment without its position is prose again;
|
|
- a comment can be **promoted to a register note** — that is the input
|
|
path this pass exists to close, and it should cost one step;
|
|
- **age is reported.** GameDesign §3.1 expires notes at 30 days on the
|
|
existing `PROVISIONAL_WARN_DAYS` machinery; comments should not get a
|
|
private mechanism.
|
|
|
|
**Controls:**
|
|
- a comment written in a session is visible from a command in a *later*
|
|
session — the whole failure mode is signal that only exists while
|
|
someone is looking at it;
|
|
- **the reporting path is exercised by the self-test**, not only the
|
|
parsing. `design-baseline.py` had a self-test and a reporting path with
|
|
no CI, and the reporting path was where it rotted (ADR-0012 D8);
|
|
- promoting a comment to a note produces a register row that
|
|
`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
|
|
id: CB-WP-0027-T05
|
|
status: todo
|
|
priority: medium
|
|
state_hub_task_id: "c0beb2d6-f998-4c36-aafa-c9ebd10c5086"
|
|
```
|
|
|
|
`evidence/CB-EV-0025-*.md`.
|
|
|
|
- **Whether a comment actually got written during play**, by the
|
|
maintainer, unprompted. The protocol's own test is whether it costs
|
|
little enough to use; anything else is a claim about a feature nobody
|
|
exercised.
|
|
- **Whether any stuck note moved.** F12, F14 and F15 are unreportable for
|
|
want of a reproduction; if the commentary path unblocks even one, that
|
|
is the pass's real result.
|
|
- **What the escaping test found**, since it is the first hostile input
|
|
this renderer has seen.
|
|
- **Whether the two-column layout cost any coverage probe**, and what that
|
|
says about how tightly the gate is coupled to the document's shape.
|
|
- **Quote CB-WP-0025's cost by re-running the instrument** — and note that
|
|
CB-EV-0019 §4's unbounded chain is still unbounded.
|