The operator, on the live Desk: "how is this last activity first?" It was not, usefully. The section sorted by `_newest_mtime`, which counts a look (`.viewed`), so opening a booth moved it up. Tonight two post-deploy checks fetched every booth page within half a second, which recorded 22 looks at once and collapsed the section into reverse name order through the (mtime, name) tie-break. Meanwhile each row shows "updated X ago", which is `landed_at`, a different clock from the one the list was sorted by. Operator ruling: "last activity can just be last time the booth was updated, not necessarily operator's last activity." The section now sorts by `(-landed_at, name)`, the date the row shows, labelled "last updated first". Looking, flagging and blurring no longer move a booth. `list_booths` keeps its own order for its other readers, and `_newest_mtime` still feeds lifetime. The r2_flow contract (§3, the ordering table, INV-5) and ROADMAP's ordering row are amended to match. Two tests and two r2_flow.toml rows cover it (25/25).
228 lines
15 KiB
Markdown
228 lines
15 KiB
Markdown
# 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) |
|
|
|
|
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 `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, IN THIS ARC** | 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 `<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. |
|
|
|
|
## 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 `<link>` 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.
|