`.forever` was the only way to say three different things — "this is durable",
"I have not answered yet", "I am still looking" — and the census said it was
carrying all three: 17 of 24 live booths (70%, up from 54% the day before).
Three of the four booths in the fleet awaiting an answer had been pinned by
hand as well, and 10 of the 17 were younger than the TTL, so the sentinel had
bought them nothing and was pressed pre-emptively.
Only the first meaning is what `keep` means. The other two are facts the
service already held and did not consult.
KEPT `.forever` present never swept (unchanged)
HELD an open pick, or marks we cannot read never swept (new)
EPHEMERAL everything else 24h (unchanged)
Viewing is activity: a deliberately-served response from a booth's own page
route writes `.viewed`, which is a dotfile and not a `.lock` dotfile, so
`_newest_mtime` already counts it. There is no new arithmetic — `booth_age_seconds`,
`is_expired` and `expires_in` are unchanged. Machine reads are excluded on
purpose: an agent must not be able to hold its own booth open by polling for
the answer it is waiting on.
The hold is unbounded, and what makes that safe is visibility plus two exits
that already existed. Every surface whose chrome the Booth owns says
`held until answered` where the countdown was, and `booth rm` / the UI x /
`DELETE /b/<n>` take a held booth exactly as they take a kept one. A hold is
protection from the timer, never from the operator.
Three cross-frontier panels ran and each found a class the others could not:
* the paraphrase panel found that two reads of one file are not one read of
one state — the contract's `is_held(marks_for(c), read_error(c))` could
resolve to `([], None)`, the pair that deletes. `hold_read` is one read.
* the code-review panel found, 4-of-4, that the booth header's board branch
rendered no lifetime at all; and that five of seven invariant tests passed
under the change that defeats them.
* the bug-hunt panel found four more paths where a failed read still
authorized a delete, and a `record_view` that followed a planted symlink.
`is_held` became `hold_reason`, which returns the reason rather than a bool
beside a string that can disagree with it.
Prediction, to re-count on or after 2026-10-06: the `.forever` rate falls to
the booths that are genuinely durable references. Only 4 booths carry marks at
all, so this rests on both halves of the unit; a null result cannot distinguish
a wrong diagnosis from a habit that outlived its need.
406 tests (341 before). Contract: docs/contracts/u4_derived_lifetime.contract.md
127 lines
7.3 KiB
Markdown
127 lines
7.3 KiB
Markdown
# The Booth — roadmap
|
||
|
||
Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md).
|
||
Current version: `0.3.0` (U1, U2, U4 and U5 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**~~ — **landed** | 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, U2, U4 and U5 are landed.** U3 is unblocked and unstarted; U6 remains
|
||
independent and unstarted; U7 waits on the rest.
|
||
|
||
**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) |
|
||
|
||
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), 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.
|