feat(marks): one primitive for operator judgment, so the loop stops running through chat
Five mechanisms existed to get one question next to one artifact. Three of
them were the same thing wearing different clothes, and the third of the three
had no code at all: the operator picked winners out of a 270-image set and
told the session in conversation. `sindra-finalists` is 86 items, every one
captioned, with the selection encoded in the booth's NAME.
A MARK is operator judgment attached to a target — the booth, or one item in
it, addressed by the `rel` U1 established as item identity. Three shapes:
pick — one of N options a session declared in advance (was: an ask)
note — free text the operator volunteered (had nothing)
flag — this one (had nothing)
One file per booth, one read path, one place openness is computed, one slot
beside the artifact. The storage shape is the operator's call (2026-09-21) and
follows from U4: "does this booth still owe an answer?" gets asked per booth
per sweep tick and per card per index render, so it has to be one read and not
a walk of a booth holding 270 files. Marks are also not links.md — that is an
O_APPEND content-hash log because 17 handles write it concurrently, whereas a
booth's marks see one session and one operator, so locking the common path
costs nothing.
The 2026-09-09 pick semantics are preserved by NOT rewriting them: partial
answers legal, a blank question lands in `unanswered`, `complete` false until
every question has a pick, the only refusal a submission carrying nothing.
`write_answer` split into the pure `build_answer` plus the storage that went
away with the sidecar; `normalize_ask` untouched.
Three findings worth naming, because each was caught by a gate rather than by
reading the diff again:
* The seam review found `inline.place` indexes asks by SUBSCRIPT — the only
consumer in the service that does — so a frozen dataclass breaks it, and
`inline.py` had been missing from the contract's scope entirely.
* A retargeted test found a regression in the legacy importer: a malformed
sidecar that renders "broken" today would have silently vanished on
migration. It now imports carrying its reason.
* A partially-answered pick counted as CLOSED on the index while the panel
beside it rendered it "partial" — the two disagreed about one booth. Open
is the reading U4 needs, and it is declared rather than smuggled in.
`GET /b/<n>/marks.json` is new and load-bearing: sessions on other hosts polled
`<stem>.answer.json` over HTTP, so removing the sidecar without it would have
taken that capability away. `/b/<n>/asks` 308s to `/marks`. Legacy sidecars are
imported, never deleted — four are live and unanswered.
Also records the operator's deterministic-order directive as a cross-cutting v1
invariant, in ROADMAP.md with the per-collection rule table and as CLAUDE.md
invariant 6. The Booth's job is comparison; an order that moves between renders
does not crash, it misfiles the judgment.
242 tests. No version bump — a release tier for this is the operator's call.
This commit is contained in:
+64
-9
@@ -26,15 +26,27 @@ _As of 2026-09-21:_
|
||||
`doc_kind` / `read_blurred` / `render_doc` call survives in a route body),
|
||||
the zoom and doc templates render the caption they now receive, the
|
||||
re-exports are asserted by a test. 192 tests green, `0.1.15`.
|
||||
- **U2 (marks) is next**, and its storage shape is settled (see the
|
||||
2026-09-21 decision below). Not yet started: no contract written, no
|
||||
blast-radius pass run.
|
||||
- **Next concrete step:** graphify + grep blast-radius pass over `booth.asks`'s
|
||||
surface and every `ask` / `answer` call-site (including `scripts/booth`),
|
||||
then author `docs/contracts/u2_marks.contract.md`. Full House Code Discipline
|
||||
— U2 is a new primitive with a CLI surface and 17 consuming handles, nowhere
|
||||
near surgical. The **seam review** is the load-bearing gate: U2's whole risk
|
||||
is that `pick` claims to preserve ask semantics and then drifts from them.
|
||||
- **U2 (marks) has landed** — `booth/marks.py`, contract at
|
||||
`docs/contracts/u2_marks.contract.md`, 242 tests green. Not yet deployed.
|
||||
- **Two things are outstanding on U2 and both need the operator:**
|
||||
1. **Deploy + migrate, in that order.** The live service still runs the old
|
||||
code, and four unanswered `*.ask.json` sidecars are live
|
||||
(`dfa-concepts`, `sc-iso-spread`, `sindra-voice-1`, `run07-decisions`).
|
||||
Migrating BEFORE deploying is the hazard: the old code would keep serving
|
||||
the sidecar, an answer written there would land in the sidecar, and
|
||||
`import_legacy_asks` skips a stem it has already imported — so that answer
|
||||
would be lost. Restart the service first, then `booth marks-import <name>`
|
||||
on each of the four.
|
||||
2. **The release tier.** U2 changes the CLI surface for 17 consuming handles
|
||||
(`booth asks` → `booth marks`, new `marks-import`) and is a v1 unit, so it
|
||||
reads minor-worthy — which needs explicit operator approval per the SemVer
|
||||
rule. Nothing is bumped or tagged; the work is committed as SHAs.
|
||||
- **`/heid-contract-review` on the U2 contract is still in flight** (panel mode,
|
||||
posted 2026-09-21, redacted copy at
|
||||
`/tmp/heid-contract-review/booth-20260922-061015/`). Triage it when it lands —
|
||||
the code is written, so findings land as follow-up fixes rather than contract
|
||||
edits. The seam review ran in-session and its nine findings are already folded
|
||||
into the contract and the code.
|
||||
- **Open, operator's call:** whether U6 (benches) runs in parallel with U2 or
|
||||
strictly after it. Nothing blocks on the answer; U6 touches different storage
|
||||
and a different surface, so it cannot be broken by U2.
|
||||
@@ -42,6 +54,49 @@ _As of 2026-09-21:_
|
||||
|
||||
## Recent decisions
|
||||
|
||||
- `[2026-09-21]` **Deterministic order is a cross-cutting v1 invariant** —
|
||||
operator directive, mid-implementation. Every ordered collection the Booth
|
||||
renders must have a *stated* rule producing the same sequence on every render
|
||||
of the same state; the rule can be anything defensible (byte order, time, an
|
||||
explicit number, an arbitrary-but-recorded sequence), but no rule at all is
|
||||
forbidden. It binds harder here than elsewhere because the Booth's job is
|
||||
**comparison** — the operator judges tile 47 against tile 47 and refers to
|
||||
artifacts positionally, so an order that moves between renders misfiles a flag
|
||||
or a note rather than crashing. Recorded as `ROADMAP.md` § "Cross-cutting
|
||||
invariant" (with the per-collection table) and `CLAUDE.md` invariant 6, and
|
||||
tested. Still undecided and must be settled before those units ship: **U7's
|
||||
section ordering and compare pairing**, and **U6's bench listing**.
|
||||
- `[2026-09-21]` **U2 (marks) landed.** One primitive replacing three
|
||||
mechanisms. `pick` / `note` / `flag` in one `.marks.json` per booth, one read
|
||||
path (`marks_for`), one openness predicate (`open_marks`), rendered beside the
|
||||
artifact on the tile, at full size in the zoom, and in the panel. `flag` and
|
||||
`note` had no write path at all before this — the selection loop
|
||||
(`golden-candidates`, `sindra-finalists`, the `pancake-*` ladders) was running
|
||||
through chat. 242 tests. Details worth carrying: `asks.py` kept `normalize_ask`
|
||||
and gained `build_answer` (the 2026-09-09 partial-answer semantics preserved by
|
||||
moving, not rewriting) and LOST its five sidecar-storage functions;
|
||||
`GET /b/<n>/marks.json` was added because remote sessions polled
|
||||
`<stem>.answer.json` over HTTP and the sidecar's removal would have taken that
|
||||
capability with it; `/b/<n>/asks` 308s to `/marks`.
|
||||
- `[2026-09-21]` **A partially-answered pick now counts as OPEN** — declared, not
|
||||
smuggled. The old index badge tested `answer is None`, so a half-answered
|
||||
four-question ask read as closed on the index while the panel beside it
|
||||
rendered `◐ partial`: the two disagreed about the same booth. Open is the
|
||||
reading that makes U4 correct — a lifetime rule that unpinned a booth on the
|
||||
first radio click would sweep a review in flight.
|
||||
- `[2026-09-21]` **The U2 seam review earned its place, and the record should
|
||||
say how.** Nine findings against the real `booth.asks` / `booth.items` /
|
||||
`booth.inline` surfaces, two of which changed scope or behaviour: `inline.py`
|
||||
was missing from `touches` entirely (its `place()` indexes asks by
|
||||
**subscript**, which a frozen dataclass refuses — nothing else in the service
|
||||
does that), and the partial-answer inconsistency above. The cold
|
||||
`/heid-contract-review` pass is artifact-only by design and structurally
|
||||
cannot see a sibling module, so neither it nor a same-model self-review would
|
||||
have found either. Two more surfaced later and are worth the same note: a
|
||||
SECOND subscript in `inline.place` the seam review undercounted, and a
|
||||
regression in my own legacy importer that a retargeted test caught — a
|
||||
malformed sidecar that renders `⚠ broken` today would have silently vanished
|
||||
on migration.
|
||||
- `[2026-09-21]` **Marks are stored as one `.marks.json` per booth**, atomic
|
||||
temp-file + `os.replace`, `fcntl` lock on the read-modify-write — operator
|
||||
decision, this session. Two alternatives were weighed and lost: a sidecar
|
||||
|
||||
Reference in New Issue
Block a user