From 726822b2b9782d96112e714f0f19b140df93abfd Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 21 Sep 2026 21:57:21 -0700 Subject: [PATCH] docs: the information architecture, and a v1 target to gate it against The Booth was built as a file-shuttle on a whim and turned into the fleet's operator-review surface without anyone deciding that. This is the decision, written down, with the measurement that forced it. The defect behind every bolt-on: one lifetime and one shape serving five jobs. Five separate mechanisms exist to put one question next to one artifact. Numbers that drove the model, taken against the live service: - 211 link rows, 145 (69%) pointing at booths that no longer exist - 22 rows that are the same bench re-posted (talk 5x, peedlar 4x) - 13 of 24 booths (54%) on the .forever escape hatch - 270 images rendered as one flat wall, subfolders discarded at render - 5 commits in one day to ship the asks primitive Four nouns replace the accretion: booth (derived lifetime), item (one record), mark (pick/note/flag -- asks, comments and votes were always one thing), and bench (a registry, not an append log). v1 is seven capabilities, each closing a measured defect. Compare mode is the best idea in the set and is parked anyway, because it is the only one that is a new capability rather than a fix. --- ROADMAP.md | 60 ++++++ booth.service | 4 +- docs/design/information-architecture.md | 252 ++++++++++++++++++++++++ 3 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 ROADMAP.md create mode 100644 docs/design/information-architecture.md diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..5af8c51 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,60 @@ +# The Booth — roadmap + +Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md). +Current version: `0.1.15` (the accreted service, 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** — resolved once, carried to gallery, zoom, doc, zip | captions never reach the zoom view (never sent, not lost) | U1 | +| 2 | **Marks** — `pick` / `note` / `flag`, one primitive, one read path | 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** — `.booth.json`, provenance on the index | 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. + +### 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 `` 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. diff --git a/booth.service b/booth.service index 1311910..f1f489e 100644 --- a/booth.service +++ b/booth.service @@ -5,8 +5,8 @@ Wants=network-online.target [Service] Type=simple -WorkingDirectory=/home/lkraven/development/eshpfi-management/services/booth -ExecStart=/home/lkraven/development/eshpfi-management/services/booth/.venv/bin/uvicorn booth.app:app --host 0.0.0.0 --port 8090 +WorkingDirectory=/home/lkraven/development/booth +ExecStart=/home/lkraven/development/booth/.venv/bin/uvicorn booth.app:app --host 0.0.0.0 --port 8090 Environment=BOOTH_DATA_DIR=/home/lkraven/booth-data Environment=BOOTH_TTL_HOURS=24 Environment=BOOTH_HOST_LABEL=nh3-dev 10.100.10.50 diff --git a/docs/design/information-architecture.md b/docs/design/information-architecture.md new file mode 100644 index 0000000..694b964 --- /dev/null +++ b/docs/design/information-architecture.md @@ -0,0 +1,252 @@ +# The Booth — information architecture + +_Written 2026-09-21, after a measurement pass over the live service. Supersedes +the accreted model described in `README.md`; the README is rewritten when this +lands._ + +## What the Booth is, stated once + +**The Booth is the fleet's operator-review surface.** Agents post work; the +operator looks at it, judges it, and the judgment gets back to the agent. + +It was built as a file-shuttle. That was the whim, and the whim was wrong in a +productive way — the thing people actually needed was the review loop, and they +built it out of folders because folders were what was on offer. + +## The measurement that drove this + +Taken 2026-09-21 against the live service and `~/booth-data`: + +| observation | number | +|---|---| +| rows on the standing link board | 211 | +| …pointing at a booth that no longer exists | **145 (69%)** | +| …that are the *same target re-posted* (talk 5×, peedlar 4×) | 22 | +| …that are genuinely durable targets | 40 (19%) | +| live booths | 24 | +| …carrying the `.forever` escape hatch | **13 (54%)** | +| largest booth | 270 images, rendered as one flat wall | +| distinct agent handles posting | 17 | +| commits to ship the asks primitive, in one day | 5 | + +Two of those are load-bearing and neither was obvious before counting: + +- **69% rot is structural, not drift.** `booth link` is an `O_APPEND` write with + no identity and no stated rule, so re-announcing a bench creates a row rather + than updating one, and a booth URL rots the moment its booth is swept. +- **54% kept means the escape hatch is carrying the main load.** A 24h TTL with + a boolean override is not "ephemeral with an exception"; it is two lifetimes + wearing one lifetime's clothes, and the operator is doing the sorting by hand. + +## The defect behind every tacked-on part + +> **One lifetime (24h from last touch) and one shape (a folder), serving five +> jobs with different lifetimes and different shapes.** + +Every bolt-on is a correct local fix to that one global mismatch: + +| bolt-on | the mismatch it patches | +|---|---| +| `.forever` | "this one shouldn't expire" | +| the link board | "this one isn't a folder" | +| `inline.py` (a placeholder DSL) | "this one isn't a gallery" | +| `wrap_verbatim_html` (6 regexes into arbitrary HTML) | "the Booth doesn't own this page" | +| the floating amber asks chip + `/b//asks` | "the operator can't see the question" | + +Five mechanisms exist to get **one question next to one artifact**. That is the +definition of accretion, and the git log shows its signature: every feature +ships, then takes 2–5 patches for cases the single shape did not anticipate. + +## The five jobs + +| # | job | example on the live service | what it needs | +|---|---|---|---| +| 1 | **Review a set** | `sindra20-engines` (197 files), `pancake-v3-full` (270) | grid, sections, zoom, filter, flag-the-winners | +| 2 | **Review a change** | `pancake-v3-full` **vs** `pancake-v4-full` | pairing by name, A/B | +| 3 | **Read a report** | `pewpew-ui-brief`, `run07-decisions` | prose + figures + nav, chrome the Booth owns | +| 4 | **Decide** | asks | the question beside the artifact | +| 5 | **Announce** | *145 dead link rows* | "operator, look at this" | + +Job 5 is the one nobody named. The link board was absorbing it: an agent posts a +booth URL because **a booth has no way to announce itself** — the index card +shows a name, an item count and a countdown, but not who made it or what it is +for. Enforcing the link rule without giving job 5 a home just makes it homeless. + +--- + +# The model + +Four nouns. Everything else is a view over them. + +## 1. Booth — a review surface with a derived lifetime + +A folder, as before. What changes is that **lifetime is derived from state, not +set by a boolean**: + +- Default: swept 24h after last activity. Unchanged, and still the contract that + means nobody cleans up after themselves. +- **An open mark pins its booth.** A booth the operator still owes an answer to + cannot be swept. (Today it can — a real defect, and one of the reasons + `.forever` gets pressed.) +- **Viewing a booth page is activity.** If he is still looking at it, it is still + alive. Deliberate act, so it cannot be triggered by browsing the index. +- `keep` survives as an explicit, reasoned pin — not as the only way to say + "not yet". + +> Expected effect: the 54% kept rate falls to the booths that are genuinely +> durable references. **This is a prediction, and it is measurable** — re-count +> the `.forever` rate a fortnight after this lands. If it does not move, the +> diagnosis was wrong and the boolean was doing something else. + +Each booth carries `.booth.json`: `{handle, title, why, created}` — written by +the CLI from `$ALTHING_HANDLE`. **This is what closes job 5.** The index card +gains provenance and a one-line purpose, and the index becomes the "what landed" +feed the link board was being used as. + +## 2. Item — one record, resolved once, carried everywhere + +Today `build_gallery()` folds a `.txt` sidecar into an item — and +`booth_view_file()` passes only `{name, file, file_url}` to the zoom template. +**The caption is not lost in rendering; it is never sent.** Same for the doc view. + +So: one `booth_items(booth)` resolver returns the full record — +`{rel, kind, caption, marks, flagged, blurred, section, url}` — and the gallery, +the zoom view, the doc view, the compare view and the zip manifest all read from +it. The zoom-loses-annotation bug is then fixed *by construction*, not by +patching one more template and hoping the next one remembers. + +**Sections come from subfolders, which already exist on disk and are currently +thrown away.** `pewpew-ui-brief` has `integration/` and `blueprint/`; +`dfa-concepts` has `source/`. `rglob` flattens them into one wall. Rendering the +structure the poster already created costs nothing and is most of the navigation +fix. + +## 3. Mark — operator judgment attached to a target + +The unification, and the reason "comments, votes and asks" stop being three +tacked-on things: + +> **An ask is the session asking the operator. An annotation is the operator +> telling the session. A vote is the operator pointing at the good ones.** All +> three are *operator judgment attached to an artifact*. One primitive. + +``` +MARK + target : the booth, or one item in it + shape : pick — one of N options the session declared in advance (was: ask) + note — free text the operator volunteered (was: comment) + flag — this one / not this one (was: vote) + writer : the operator, in the browser + reader : the session — `booth marks [--wait]` +``` + +One storage model, one read path, one rendering slot (beside the artifact), one +place where "is anything still open?" is computed. `pick` keeps today's ask +semantics wholesale, including partial answers and the multi-question form — that +behaviour is operator-settled (2026-09-09) and is preserved, not redesigned. + +`flag` is the one that makes a 270-image booth tractable, and it closes a loop +that is currently running through chat: `golden-candidates`, `sindra-finalists` +and the `pancake-*` ladders are all the operator picking winners from a set and +then *telling the session in conversation*. Flags make that readable by the +session that posted the set. + +## 4. Bench — a running thing, registered + +**Not a booth.** The operator's rule, stated by him and now enforced in code: + +> Links are for durable middle-to-long-term testing surfaces — jackdaw's current +> bench, talk's current bench — the things that get promoted to Homepage when +> fully deployed. + +``` +BENCH + id : normalized URL (the identity — re-posting UPDATES, never appends) + name : what it is + owner : the agent handle that registered it + state : live → promoted (to Homepage) → retired + health : last_checked, last_ok (probed; flagged when dead, never auto-deleted) +``` + +- Stored at `~/booth-data/.benches.json` — a dotfile, outside the booth + namespace, so it can neither be swept nor mistaken for a booth. +- `booth bench add ""` upserts on the normalized URL. The 5 `talk` + rows and 4 `peedlar` rows collapse to one each, by construction. +- **`booth link` refuses a `…:8090/b/…` URL** and names the right surface. It + survives as a deprecated alias rather than vanishing — 17 handles have the + muscle memory, and the teaching moment belongs at the point of use. +- Liveness is *flagged*, not enforced. A bench that stops answering gets a + marker and a date; deleting is the operator's call. Nothing here deletes the + operator's data on a timer. + +### Migrating the 211 existing rows + +1. `links.md` is archived verbatim to `~/booth-data/links/links-archive-.md` + **and** committed to this repo. Nothing the operator wrote is destroyed. +2. The ~40 durable rows seed the registry, deduplicated. +3. The 145 dead rows and 26 live booth-links are not carried over — the first + point at nothing, the second are booths, which the index now announces itself. + +--- + +# The frame: who owns the page + +Operator ruling, 2026-09-21: **the page declares itself, the Booth mounts into +it.** + +A report includes one line: + +```html + +``` + +…and the Booth's chrome — nav, marks, annotations, the way home — mounts through +a real DOM API. Marks land at `data-booth-mark=""` anchors, which keeps the +2026-09-09 ruling that *asks render inline beside the artifact, not on a separate +page*. If the line is absent, the Booth injects it at **one** insertion point, so +every existing verbatim booth keeps working untouched. + +**What this deletes**, and this is the whole point of the decision: + +- `booth/inline.py` — 114 lines of placeholder DSL, entirely +- `wrap_verbatim_html` and its six regexes against arbitrary HTML + (`_HEAD_CLOSE_RE`, `_HTML_OPEN_RE`, `_DOCTYPE_RE`, `_BODY_CLOSE_RE`, + `_HTML_CLOSE_RE`, `_ICON_RE`) and the doctype/charset-ordering constraints + they are threading +- `_BACK_CHIP`, `asks_chip` — two floating chips positioned by guessed offsets +- `GET /b//asks` — the standalone page that existed only because a verbatim + booth could not show its own asks + +Regex-injecting into arbitrary author HTML is the single most fragile thing in +the service, and it is load-bearing for the operator's most important workflow. +A declared seam costs the author one line and removes the whole class. + +--- + +# Navigation + +Sized for the real case — 270 items — not the demo case. + +- **Sections** from subfolders (free; the data is already there). +- **A sticky rail**: total, per-section counts, and filters — all / flagged / + annotated / unanswered. Jump-to-section. +- **Keyboard in the grid, not only in the viewer.** Today prev/next exists only + *inside* the zoom view: `←/→` move, `f` flags, `n` opens a note, `Enter` + zooms, `Esc` backs out. A 270-item review is a keyboard task. +- **Zoom keeps the item's annotations** — the same item record, so the caption, + notes and flag state are all present at full size. +- **Compare**, detected rather than configured: when two subfolders share + filenames, offer pair-by-name A/B. This is the `pancake-v3-full` / + `pancake-v4-full` dance — currently four separate booths diffed by eye across + two browser tabs — collapsed into one booth with two subfolders. + +--- + +# Non-goals, kept from the original and still right + +- **No auth.** LAN/mesh-internal, and blur stays explicitly cosmetic. Anything + that must not be seen by whoever can reach `:8090` must not be in a booth. +- **No database.** The filesystem stays the state; `ls ~/booth-data` still tells + you everything. Marks, benches and booth metadata are all flat files. +- **No upload API for booths.** A session makes a folder. That is the whole API, + and it is why every agent family can use this without a client.