Files
booth/ROADMAP.md
T
vh 1c3ce5ddb5 feat(u6): benches — a registry with identity, and the rule enforced
The standing link board carried three jobs because only one of them had a
surface. Re-measured before contracting, its 221 rows split into 178 booth
announcements (156 already dead) and 43 non-booth rows, of which 8 are the same
bench re-posted. U5 gave the booth announcement a home; this gives the running
service one, and refuses the one shape that now has somewhere better to go.

- booth/benches.py (new, stdlib-only and sibling-free): the Bench record, URL
  normalization as the identity, a lenient read on the render path and a strict
  read on the write path, atomic replace under an flock, and a stated total
  order (state rank, name casefolded, id).
- links.booth_target: ONE predicate for "is this a booth URL", consumed by the
  CLI refusal, the board's dead marker and bench import. Host-agnostic,
  path-shaped, percent-decoded, never raises.
- booth link refuses a booth URL, names `booth new --why`, and writes nothing —
  not the row, not the board directory, not the announcement.
- The board marks rows whose booth has been swept. Nothing here deletes a row:
  removal stays the operator's two clicks through the existing bulk control.
- booth bench add|ls|state|rm|import. import writes nothing without --apply and
  never edits links.md.
- docs/archive/links-2026-09-22.md: the board archived verbatim into git.

Identity is the FULL normalized URL, not the origin, and that was measured:
origin identity collapses the 43 non-booth rows to 19 groups by merging eight
distinct gitea repositories into one row, three unrelated HuggingFace model
cards into one, and the two LRPG surfaces on 10.100.10.50:8321 — the design
doc's own example of two real benches — into one. Full-URL identity still
collapses both cases that doc names: talk 5 to 1, Peedlar 3 to 1.

booth link is NOT deprecated. Roughly 14 of the 35 distinct non-booth targets
are reference bookmarks for which the board is the right and only home; the
design doc's plan to deprecate it would have evicted a third of its live
content. Corrected there, along with what "normalized URL" means.

The seam review found three real defects in the contract before any code: the
claim that test_stdlib_only already forbids sibling imports (it exempts `booth`
on purpose), naming resolve_booth as the dead marker's existence check (it
raises HTTPException(404), so one swept booth would have 404'd the whole board
page), and silence on percent-encoding (booth links are emitted through
quote(name, safe=""), so a raw comparison marks every encoded booth dead
forever). That both list_booths and sweep_once skip the registry was verified
against the real functions rather than assumed.

444 -> 555 tests. Deployed and verified live: 23/23 booths 200, and the board
renders 156 dead of 221 rows, matching an independent pre-implementation count.

NOT TAGGED: both cold gates are in flight (contract review
01M35BWCJ806MT75NA630Y4WFH, code review 01M35CK8YKEKMV7T15JXEF6A8N) and the
bug-hunt has not run. Per the v0.2.0 lesson, the tag waits for the gates.
2026-09-22 13:25:32 -07:00

150 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The Booth — roadmap
Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md).
Current version: `0.5.0` (U1 through U6 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**~~ — **landed `87e2c53`, released `v0.5.0`** | 6 regexes injected into arbitrary author HTML, load-bearing for asks | U3 |
| 4 | ~~**Derived lifetime**~~ — **landed `c3a97c1`, released `v0.4.0`** | 70% of booths on the `.forever` escape hatch (54% when first counted) | U4 |
| 5 | ~~**Self-announcing booths**~~ — **landed `c015a91`, released `v0.3.0`** | 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 through U6 are landed.** **U7 is the last unit before the 1.0 cut** — its
only dependency was `{U3, U4, U5}` and that closed with U3.
⚠ **Before starting U7, read
`persistent-memory.d/2026-09-21-u7-section-premise-half-wrong.md`, and re-count
the booths first.** Half its premise is already known to be wrong — every booth
that actually needs navigation is FLAT — and the booth set churned again on
2026-09-22: the four large booths U7 was sized against (`pancake-v3-full` and
`pancake-v4-full` at 270 items, `sindra20-engines`, `sindra-finalists`) have all
been swept. The largest live booth is now `miranda-is` at 92 items, flat. Two of
23 booths have subfolders (`pewpew-ui-brief`, `dfa-concepts`) and **both are
reports** — the job where grid navigation matters least. Sections, one of U7's
four named components, buys close to nothing. The rail, the filters and the grid
keyboard are the unit.
**U5's adoption is a measured prediction, not a finished result**, and it is
TWO predictions rather than one. The operator declined a fleetwide announcement
so that adoption could be told apart from design; within fifty minutes of the
deploy a peer that had been told nothing (`comfy-dev`) created a booth and it
announced itself with a handle and an empty `why`. That is the split:
- **The handle rides for free.** It is written by `booth new` and `booth add`,
so every existing caller starts announcing without learning anything.
- **The `why` has to be learned.** It needs someone to know the flag exists.
Both get re-measured on **2026-09-29**:
find ~/booth-data -maxdepth 2 -name .booth.json | wc -l # free
grep -l '"why": "[^"]' ~/booth-data/*/.booth.json | wc -l # learned
A high first count with a near-zero second is the predicted shape of "nobody was
told" — an adoption failure fixed by announcing, which is a different thing from
nobody wanting it. Same instrument as U4's `.forever` prediction below.
### 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 |
| a booth's announcement | not a collection — one flat record per booth, nothing to order (U5) |
| the bench registry | `(state rank, name casefolded, id)` — live before promoted before retired, then alphabetical, with the id as a TOTAL tie-break so two benches sharing a name cannot swap (U6) |
| the link board's dead marker | not an order — a per-row stamp read from the existing `order_for_display` sequence, so marking cannot move a row (U6) |
| embed anchors in a verbatim report | **document order** — what `querySelectorAll` yields, so the author's markup decides (U3) |
| the embed tail (fragments the author did not place) | **payload order**, which is the marks order `(created, id)` — one rule, whether a fragment lands at an anchor or at the end (U3) |
| questions within a pick | declaration order, in the payload's `questions` LIST — carried by the format rather than by object-key insertion order (U3) |
U3's three rows are the first case where the rule binds across a language
boundary: the order is decided in Python and honoured in JavaScript, and a
browser test asserts it rather than a string assertion that could not see it.
U4 added no ordered collection — a booth's lifetime is one state per booth,
not a sequence — so the rule above did not need a new row. The three lifetime
surfaces (index card, booth header, marks page) render through ONE macro
precisely so they cannot disagree, which is the same property stated for
ordering: one rule, one place, every surface reading it.
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). **U6's bench listing is settled** — the row above.
Compare pairing is parked to v1.1 with compare mode itself, so U7 carries one
undecided rule, not two.
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.