Files
booth/docs/design/information-architecture.md
T
vh 726822b2b9 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.
2026-09-21 21:57:21 -07:00

253 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<n>/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 <booth> [--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 <url> "<what>"` 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-<date>.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
<script src="/_booth/embed.js" defer></script>
```
…and the Booth's chrome — nav, marks, annotations, the way home — mounts through
a real DOM API. Marks land at `data-booth-mark="<id>"` 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/<name>/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.