Three defects and a missing test, all surfaced by the cross-frontier contract panel dispatched before implementation and triaged after it (heid, four arms, artifact-only, thread 01M33VSNFER4N1554G0Y0VC9C8). v0.2.0 was already tagged and announced to fifteen handles when they landed, which is the argument for running the gate at all. DATA LOSS. `marks_for` is deliberately lenient — an unparseable `.marks.json` reads as "no marks" so a review page still loads. The write path inherited that leniency through the same reader, so one flag click appended a single entry to an empty list and atomically replaced the file: every mark in the booth gone, silently, from a click. Reproduced first, then fixed. The fix is an asymmetry, not a retreat from leniency. Reads stay lenient; writes go strict through `_read_raw_strict`, which distinguishes bytes-present-but- unreadable from absent and valid-but-empty, and raises `MarksCorrupt`. The damaged bytes are left on disk. Routes answer 409 rather than 500 — the service is fine and the request was well-formed, the state on disk is not — and the body says what to do, because the alternative the operator reaches for otherwise is deleting the file, which is the thing being protected. The CLI says it in one line instead of a traceback. A PICK COULD NOT TARGET AN ITEM. `Mark.target` carried one, `marks_for_target` retrieved by it, and the panel already rendered "on <item>" — but `declare_pick` had no parameter for it, so no session could produce one. A question about one artifact is the whole point of the 2026-09-09 inline-placement ruling; the door was simply missing. THE IMPORTER STRANDED AN ANSWER. A stem already present as a mark was skipped wholesale. If a session had re-declared that stem through marks while the operator's choice sat in the legacy sidecar, that choice was lost permanently — reads are forbidden from looking at sidecars. The declaration is still skipped (idempotence holds) but a legacy answer is now adopted when the existing mark is an unanswered pick, and an answer made through marks is never overwritten. INV-3 NAMED A SURFACE NOTHING TESTED. All four arms converged on it: the rule protects gallery tile, zoom view and doc view; the falsifiable check covered one. The doc view was implemented and untested, so shipping it unmarked would have passed. Three tests now, one per surface. The contract carries the full triage, including two findings accepted and NOT closed: INV-2's and INV-5's checks comply in letter — openness can be re-derived without spelling the grepped pattern, and importlib inside a function defeats the AST walk. Both describe a future careless change, and the honest statement is that these checks raise the cost of drifting rather than making it impossible. Recorded rather than papered over. Also pins the three prose ambiguities the panel found, normatively and once each: what counts as open, the three distinct broken-declaration cases, and INV-6, which had named a helper that does not exist and forbidden the calls that helper must make. 253 tests.
101 lines
5.9 KiB
Markdown
101 lines
5.9 KiB
Markdown
# The Booth — roadmap
|
||
|
||
Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md).
|
||
Current version: `0.2.1` (U1 + U2 landed; 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**~~ — **landed `ce598b3`** | captions never reach the zoom view (never sent, not lost) | U1 |
|
||
| 2 | ~~**Marks**~~ — **landed `c7f9437`, released `v0.2.0`** | 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.
|
||
|
||
**U1 and U2 are landed**, which unblocks U3, U4 and U5 — all three read marks.
|
||
**U5 is next** (operator, 2026-09-21). U6 remains independent and unstarted.
|
||
|
||
### 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.
|