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.
98 lines
5.7 KiB
Markdown
98 lines
5.7 KiB
Markdown
# The Booth — roadmap
|
||
|
||
Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md).
|
||
Current version: `0.1.15` (the accreted service, extracted from eshpfi 2026-09-21).
|
||
|
||
## v1 target
|
||
|
||
What must be true to cut 1.0. Seven capabilities, each closing a **measured**
|
||
defect — not a wish. The measurements are in the IA doc.
|
||
|
||
| # | capability | closes | unit |
|
||
|---|---|---|---|
|
||
| 1 | **One item record** — resolved once, carried to gallery, zoom, doc, zip | captions never reach the zoom view (never sent, not lost) | U1 |
|
||
| 2 | **Marks** — `pick` / `note` / `flag`, one primitive, one read path | 5 mechanisms for 1 job; operator→session loop runs through chat | U2 |
|
||
| 3 | **Declared embed seam** — `/_booth/embed.js`, chrome mounts via DOM | 6 regexes injected into arbitrary author HTML, load-bearing for asks | U3 |
|
||
| 4 | **Derived lifetime** — open marks pin; viewing is activity | 54% of booths on the `.forever` escape hatch | U4 |
|
||
| 5 | **Self-announcing booths** — `.booth.json`, provenance on the index | job 5 had no home, so it lived on the link board as 145 dead rows | U5 |
|
||
| 6 | **Benches** — registry, identity, enforced rule, migration | 69% link-board rot; the same bench posted 5× | U6 |
|
||
| 7 | **Navigation at 270 items** — sections, rail, filters, grid keyboard | one flat wall; subfolder structure discarded at render | U7 |
|
||
|
||
Ordering is dependency-driven, not priority-driven: **U1 → U2 → {U3, U4, U5} →
|
||
U7**, with **U6 independent** of all of them (different storage, different
|
||
surface) and therefore the safest thing to land first or in parallel.
|
||
|
||
### Cross-cutting invariant — deterministic order, everywhere
|
||
|
||
**Every ordered collection the Booth renders must have a stated, deterministic
|
||
order.** Not "usually stable", not "whatever the filesystem yields" — a rule
|
||
someone can name, that produces the same sequence on every render of the same
|
||
state. The rule itself is free to be anything defensible: byte order over a
|
||
path, creation time, an explicit number, even an arbitrary-but-recorded
|
||
sequence. What is forbidden is *no rule*.
|
||
|
||
This matters more here than in most services because the Booth's whole job is
|
||
**comparison**. The operator is judging `pancake-v3-full` against
|
||
`pancake-v4-full`, tile 47 against tile 47. If the order shifts between two page
|
||
loads — or differs between the gallery, the zoom ring, the zip manifest and the
|
||
`marks` read — then every positional reference the operator makes ("the third
|
||
one from the left", "the one after the banded one") is silently wrong, and a
|
||
flag or a note lands on the wrong artifact. Non-determinism does not present as
|
||
a bug report; it presents as the operator's judgment being quietly misfiled.
|
||
|
||
Where it already binds, and what the rule is in each case:
|
||
|
||
| collection | rule |
|
||
|---|---|
|
||
| items in a booth | `sorted(rel)` — byte order over the booth-relative path (U1 INV-3) |
|
||
| the zoom prev/next ring | the item order, filtered to images — same sequence, one source |
|
||
| caption sidecar resolution | sorted scan, so two media files sharing a stem resolve the same way every time (a real non-determinism U1 removed) |
|
||
| marks in a booth | `(created, id)` — time, with the id as tie-break so two marks written in the same second cannot swap |
|
||
| legacy ask import | `(mtime, name)`, which is the order `list_asks` gave them |
|
||
| link board rows | pinned first, then newest-first |
|
||
|
||
Where it is still to be decided, and must be before the unit ships: **U7's
|
||
section ordering and its compare pairing** (sections need a stated order among
|
||
themselves, not just within; pairing by filename needs a rule for what happens
|
||
to an unpaired file), and **U6's bench listing**.
|
||
|
||
The test for any new ordered surface: *can you write the rule down in one line?*
|
||
If not, it does not have one yet.
|
||
|
||
### Explicitly NOT in v1
|
||
|
||
- **Backward compatibility with the `ask` CLI verbs.** Pre-1.0, and `ask` /
|
||
`asks` / `answer` become thin aliases over `marks` rather than a second code
|
||
path. The 17 consuming handles get one althing note naming the change — the
|
||
one case where telling peers is real coordination and not a broadcast.
|
||
- **A migration that deletes anything.** `links.md` is archived verbatim and
|
||
committed before the registry is seeded from it.
|
||
|
||
## Parking lot
|
||
|
||
Deferred with a home, per the anti-creep gate. Default is park; these were
|
||
weighed against the v1 path and lost on purpose.
|
||
|
||
| item | why parked |
|
||
|---|---|
|
||
| **Compare mode** — pair-by-name A/B across subfolders | The best idea in the set, and the only one that is a *new capability* rather than a fix for a measured defect. The four-booth `pancake-v3/v4` dance still works. First thing in v1.1. |
|
||
| Virtualized / progressive grid loading | Speculative. 270 `<img loading="lazy">` may be fine. **Measure the real booth before optimising it** — if it renders inside a second, this is invented work. |
|
||
| Bench uptime history + graphs | The v1 need is "is it dead", which one flag answers. A time series is a different product. |
|
||
| Cross-booth search | No evidence of the need in the usage data. |
|
||
| Per-viewer state (who has seen what) | The Booth has one viewer. Revisit if that stops being true. |
|
||
| Auth | Standing non-goal. LAN/mesh-internal. Blur stays cosmetic and says so. |
|
||
|
||
## Post-v1, already committed
|
||
|
||
- **SVOS theme retrofit by `design-dev`.** Runs as a parallel track, not a v1
|
||
gate: we own the information architecture (it is driven by the measurement
|
||
above), design-dev owns the visual and interaction system. The handoff is a
|
||
`/vor-ui` brief written against the landed v1 structure — the same shape
|
||
`hamr-dev` and `pewpew-dev` used.
|
||
|
||
## Gate
|
||
|
||
A proposed feature is on the v1 path or it is parked. **Default: parked.** When
|
||
both are defensible, park it — same asymmetry as the patch-default in SemVer.
|
||
Applies regardless of who proposed it.
|