All seven v1 capabilities are landed (ROADMAP's v1 target is met), so this is the first release of the 1.x train. Staged as a beta rather than cut final on the operator's call: per the canonical policy `-beta.N` means feature-complete, external testing, no new features, focus is on bugs — which is exactly this state, with a cross-frontier bug-hunt panel outstanding on U7's diff. The repo learned this sequencing the hard way once: v0.2.0 was tagged and announced while a contract panel was in flight, the panel found three defects in the code just released, and v0.2.1 shipped within the hour. A beta is the designed answer to that, not a workaround for it. ALSO FIXES A SECOND COPY OF THE VERSION, found while cutting this one. `booth.__version__` was the literal `0.1.0` and had been wrong through six releases. It is now read from pyproject.toml — deliberately NOT from importlib.metadata, which describes a different artifact: this repo has no build step and no install step (booth.service runs uvicorn with WorkingDirectory set to the tree), and the venv was carrying a vestigial booth-0.3.0.dist-info with no package directory behind it. Installed metadata therefore reported 0.3.0 for a tree at 1.0.0b1 — confidently wrong and varying by environment, which is worse than a literal that at least fails the same way everywhere. booth/__init__.py is also, it turns out, effectively stdlib-only: scripts/booth imports booth.links / booth.marks / booth.manifest under the system python3 with no venv, and every one of those executes the package root first. Nothing asserted it. test_stdlib_only now covers __init__, and the no-venv import path is verified under python3.11 reporting 1.0.0b1. 642 tests green.
11 KiB
The Booth — roadmap
Design: docs/design/information-architecture.md.
Current version: 1.0.0b1 (U1 through U7 landed — every v1 capability is
in; extracted from eshpfi 2026-09-21). The v1 target is MET and staged as a
beta (operator, 2026-09-22): feature-complete, external testing, no new
features — the remaining work is bugs. 1.0.0 final is cut when the beta
survives; per the canonical policy an rc would be cut from the same commit,
but a beta may still take fixes.
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 | ce598b3 |
captions never reach the zoom view (never sent, not lost) | U1 |
| 2 | c7f9437, released v0.2.0 |
5 mechanisms for 1 job; operator→session loop runs through chat | U2 |
| 3 | 87e2c53, released v0.5.0 |
6 regexes injected into arbitrary author HTML, load-bearing for asks | U3 |
| 4 | c3a97c1, released v0.4.0 |
70% of booths on the .forever escape hatch (54% when first counted) |
U4 |
| 5 | c015a91, released v0.3.0 |
job 5 had no home, so it lived on the link board as 145 dead rows | U5 |
| 6 | 1c3ce5d, released v0.6.0 |
69% link-board rot (re-measured: 178 booth rows + 8 bench re-posts) | U6 |
| 7 | 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 newandbooth add, so every existing caller starts announcing without learning anything. - The
whyhas 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) |
| 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) |
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 rode into v1.1 with compare mode itself. 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
askCLI verbs. Pre-1.0, andask/asks/answerbecome thin aliases overmarksrather 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.mdis 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-uibrief written against the landed v1 structure — the same shapehamr-devandpewpew-devused.
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.