The diff-scoped bug-hunt panel, four arms, artifact-only. Its strongest finding is one I created two hours earlier while hardening the reader. `stat` reports size 0 for a FIFO and 0 for a symlink to /dev/zero, so both sail under the byte cap added for the RecursionError round — and then `read_text` either blocks in read() with no EOF, so the except never runs, or allocates until the kernel intervenes. `list_booths` reads every booth on every GET / and /healthz, so ONE such file stalls the front page for the whole service, with no error and no recovery short of a restart. Reproduced before believing it (timeout returned 124). S_ISREG is checked BEFORE the size in both modules now; verified against the live service with two FIFOs planted, which answered 200 in 36ms. The shape worth carrying: st_size answers a different question than "can this be read", and a bound that trusts it inherits everything it does not mean. A hardening fix opened a worse hole than the one it closed. THE UPLOAD PATH WROTE ABOVE ITS OWN CLEANUP GUARD (4/4) A failed manifest write orphaned a .uploaded half-booth with no files in it — and because the temp name now carries a random suffix, nothing ever overwrote the leak, and .booth.json.<hex>.tmp is not a .lock, so _newest_mtime counted it and kept that empty booth past every sweep. The uniqueness fix from the previous round is what made the leak permanent. Both writes moved inside the guard; the temp is removed on every exit path. DAMAGED BYTES ARE KEPT, NOT REPLACED (4/4, INV-6) Marks made this explicit in v0.2.1 and this write path contradicted it: a manifest that failed on ONE field lost the others with it, including a why the re-announcer may never have kept anywhere. It diverges from marks in HOW it honours the rule — marks refuse and answer 409 because the operator's judgment is not restatable; a manifest quarantines and proceeds, because refusing would fail `booth add` and lose the files it was copying. ONE OPENNESS PREDICATE, AS U2 SAID (2/4) `booth answer` spelled out `if m.answer is None` while `booth marks` asked `open_marks`, so a partially-answered pick read as done to one verb and open to the other — at the same instant, on the same booth. U2's INV-2 put openness in one function precisely so they could not drift. The mirror case is fixed too: a pick that hydrates broken is refused by the web route, so `answer --wait` polled an hour on a form nothing could ever land. ALSO - now_stamp was whole-second while the importer had moved to microseconds, and '-' sorts before '.', so a later mark came out ahead of an earlier import inside the same second. One format; the previous round's ordering fix had opened this one. - `_broken` was the third of three directory-name fallbacks and the one still handing a raw name into a card's sub-line. - An identical re-announce rewrote the file and reset the TTL. `booth link` does this on every post to the standing board. - The importer's return went through the bare _hydrate, not _hydrate_safe. - A marks document could be written larger than it can be read back, and then read as no marks at all. Refused at the write instead. - `choice` reached the answer builder raw while `notes` beside it did not. AND ONE FINDING DELIBERATELY NOT FULLY CLOSED The mtime-restore race is real. The clean fix — ignore a booth directory's own mtime whenever the booth holds anything — also silently retires the documented rule that releasing a kept board resets its clock, which the CLI header, the README and a deliberately-written test all pin. That is a TTL doctrine change, not a bug fix, and an existing test caught the attempt. The concrete half is fixed (a failing os.utime escaped and 500'd the route); the race is stated in the code where the next reader will meet it. 341 tests. Live service restarted, 24/24 booth pages verified.
121 lines
7.0 KiB
Markdown
121 lines
7.0 KiB
Markdown
# The Booth — roadmap
|
||
|
||
Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md).
|
||
Current version: `0.3.0` (U1, U2 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** — open marks pin; viewing is activity | 54% of booths on the `.forever` escape hatch | 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 and U5 are landed.** U3 and U4 are 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) |
|
||
|
||
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.
|