# The Booth β roadmap
Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md).
Current version: `1.0.0b1` (**U1 through U7 landed**; extracted from eshpfi
2026-09-21).
β **THE BETA'S PREMISE IS SUPERSEDED AND THE TAG CANNOT BE UNSAID.**
`v1.0.0b1` was cut 2026-09-22 promising "feature-complete, no new features, the
remaining work is bugs." On 2026-09-23 the operator ruled a flow redesign and
compare mode into the arc, which are emphatically new features. **The tag stays
as written** β it is an immutable record of what was believed at the time, not a
claim about now β and no further pre-release is cut until the arc lands.
Dropping back to an alpha is not available: `1.0.0a2` sorts BELOW `1.0.0b1`, and
versions do not go backwards.
π **RULED 2026-09-23: NO `1.0.0` YET.** Verbatim: *"no v1.0 yet."* The tag
stays at `1.0.0b1`, no further pre-release is cut until the arc lands, and the
arc now includes the flow redesign, compare mode and the Desk revisions still in
flight. Do not cut a release because the suite is green and the roadmap looks
complete β it has looked complete twice already.
## 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**~~ β **landed `1c3ce5d`, released `v0.6.0`** | 69% link-board rot (re-measured: 178 booth rows + 8 bench re-posts) | U6 |
| 7 | ~~**Navigation**~~ β ~~sections~~ **filename groups**, rail, filters, grid keyboard β **landed, unreleased** | one flat wall; 0 of 11 galleries have subfolders, so grouping comes from the filename | 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.
**ALL SEVEN UNITS ARE LANDED.** U7 closed last; its only dependency was
`{U3, U4, U5}` and that closed with U3.
β **What U7 actually shipped is not what this row first described, and the
difference is measured.** Sections were dropped for filename-prefix groups
(operator-ratified 2026-09-22) because zero of eleven gallery booths have a
subdirectory. Then the *grouping rule itself* changed at implementation: the
contract's `strip a trailing digit run` yields 24 groups for `sindra-bakeoff`'s
40 images and 27 for `sindra`'s 30 β a rail with a row per tile β because it
keys on the end of the stem, where the instance number lives. The shipped rule
keys on the **first separator-delimited segment**, where the family lives, and
gives 4 and 2. The full re-measurement across all 17 live booths is in
`docs/contracts/u7_navigation.contract.md`.
**The v1 target is met.** What remains is a release decision the operator owns:
cut `1.0`, or take a `0.7.0` staging release first. Nothing in the code is
waiting on it.
**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 review prev/next ring | the item order, **filtered to media** β image, video and audio (`review_chain`, R2). Supersedes `image_chain`, which stays importable and unchanged for its other callers |
| an item's ordinal (`#NN`) | its position in `sorted(rel)` β **counted across ALL items, so `#07` is the same tile under every filter.** This is what makes the operator's "the third one" mean one thing, which the filters had quietly broken (R2) |
| the filmstrip and the tape | the review ring |
| the flag tray | **by ordinal** β the tray reads in the same direction as the grid (R2). The notes list keeps `(created, id)` |
| the Desk's sections | fixed: needs you β new since you looked β everything else (R2) |
| within *needs you* | `(open_since, name)` |
| within *new since you looked* | `(-landed_at, name)` |
| within *everything else* | `(-landed_at, name)`: last updated first, the date each row shows. Was `list_booths`' activity order, which counted a look (operator, 2026-09-23) |
| the Desk's bookmarks column | `order_for_display` β pinned first, then newest |
| 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) |
| **groups among themselves** | **the position of each group's first member in the rendered sequence** β `sorted(rel)` narrowed by the filter, never re-sorted. Walking the rendered list once into an insertion-ordered dict IS the rule, so there is no second sort to drift from it (U7) |
| **items within a group** | not a separate order β a group is a label on a tile, not a container. The grid stays `sorted(rel)` and groups interleave in it freely (U7) |
| 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) |
| the compare filmstrip | **the compare ring**: the review ring (item order, media only) less any rel compare cannot open, so no strip link offers a pair that 404s (r3) |
| compare stepping | along the compare ring, modulo its length; linked moves both sides one place and keeps their distance, unlinked moves the active side only (r3) |
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.
**U7's group ordering is SETTLED and SHIPPED** (operator, 2026-09-22): groups
order by the position of their first member in the rendered sequence, so the
rail reads in the same direction as the grid. Subfolder sections were dropped
in favour of filename-prefix groups on measured grounds β zero of eleven
gallery booths have a subdirectory.
**Nothing in this table is undecided any more.** The two U7 rules that were
(section ordering among themselves, compare pairing) resolved differently:
section ordering is MOOT, because U7 renders no section rail β `Item.section`
still exists and is still derived, it simply has no ordered surface. Compare
pairing was ruled 2026-09-24: pairs are PICKED, never detected from filenames,
and compare landed with r3 (rows above). **U6's bench listing is
settled** β the row above.
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**~~ β **UNPARKED 2026-09-23, LANDED 2026-09-24 (r3)** | Parked as "the only new capability rather than a fix for a measured defect", and **that deferral was ours and the operator overruled it.** design-dev argued it belongs in this arc because the ladders and bakeoffs already need it; the operator ruled `this_arc`. Lands AFTER the Desk and the reel, as a view toggle over the same item record. Recorded so nobody re-parks it by reading an older rule. |
| Virtualized / progressive grid loading | Speculative. 270 `` 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. |
## The design arc β IN SCOPE, not post-v1
β **This section used to be "Post-v1, already committed" and it is not post-v1
any more.** The operator's 2026-09-23 rulings put a flow redesign and compare
mode inside the arc, so the work below is part of what ships, not after it.
- **SVOS theme retrofit by `design-dev` β HANDED OFF AND ACCEPTED 2026-09-22**
(althing thread `01M369321KNBPZ7FYDQGZG7AXP`). 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.
π **OPERATOR RULING 2026-09-23 β THE OWNERSHIP BOUNDARY MOVED, AND IT MOVED
OUR WAY OUT.** The first concept round was ruled **NOT ship-as-shown**:
*"He didn't go far enough, still looks like the booth. I want him to consider
the flow and the requirements β design touches, layout, usability all belong
to him."* **Flow, layout, usability and the REQUIREMENTS are design-dev's.**
The information architecture is no longer fenced off from him: what belongs on
which page, what groups with what, what the rail counts and whether a rail is
the right object at all are his calls to propose and build.
β **The fence was OURS, and it is what produced a reskin.** The handoff said
*"what we are not asking for: layout changes driven by information
architecture"*, and design-dev's *"markup changes are class additions only, no
reordering"* was that constraint honoured. The ruling corrects the brief, not
his round. **That paragraph is void β do not restate it.**
What survives is two tiers, deliberately separated because collapsing them is
how we over-fenced the first time. **Tier 1, correctness not taste:**
deterministic order (an operator directive β the RULE may change, but not into
"whatever the layout yields"), autoescape, blur keeps admitting it is
cosmetic, restart discipline. **Tier 2, engineering defaults WE chose and he
may now argue with:** the gallery working with JavaScript off, virtualization
parked, compare mode deferred. Tier 2 disputes go to the operator, not settled
between agents.
β **The `/vor-ui` brief this row used to require was DECLINED, and rightly.**
The row predates `docs/design/information-architecture.md`; with that doc, the
landed templates and the seven handoff constraints in hand, a `/vor-ui` pass
would have cost the operator a serial Q&A to re-derive IA we had already
measured β the exact operator-load this project exists to reduce. The handoff
message is the brief. If the design system ever wants the IA different,
design-dev raises it and we re-measure rather than either side guessing.
Settled with it: **we merge and restart** (the deployment root stays in one
pair of hands); design-dev works on `svos-retheme` in his own clone against a
COPY of `~/booth-data`, never touching `:8090`; a concept round goes to the
OPERATOR before any fixup. Webfonts arrive by Google Fonts `` with
`display=swap` and a system fallback stack β the CDN-free property was
accreted, not an invariant, and self-hosting is a v1.1 item because
`v1.0.0b1` promises no new features.
## 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.