Files
booth/persistent-memory.d/2026-09-22-one-number-was-two-defects.md
vh 1c3ce5ddb5 feat(u6): benches — a registry with identity, and the rule enforced
The standing link board carried three jobs because only one of them had a
surface. Re-measured before contracting, its 221 rows split into 178 booth
announcements (156 already dead) and 43 non-booth rows, of which 8 are the same
bench re-posted. U5 gave the booth announcement a home; this gives the running
service one, and refuses the one shape that now has somewhere better to go.

- booth/benches.py (new, stdlib-only and sibling-free): the Bench record, URL
  normalization as the identity, a lenient read on the render path and a strict
  read on the write path, atomic replace under an flock, and a stated total
  order (state rank, name casefolded, id).
- links.booth_target: ONE predicate for "is this a booth URL", consumed by the
  CLI refusal, the board's dead marker and bench import. Host-agnostic,
  path-shaped, percent-decoded, never raises.
- booth link refuses a booth URL, names `booth new --why`, and writes nothing —
  not the row, not the board directory, not the announcement.
- The board marks rows whose booth has been swept. Nothing here deletes a row:
  removal stays the operator's two clicks through the existing bulk control.
- booth bench add|ls|state|rm|import. import writes nothing without --apply and
  never edits links.md.
- docs/archive/links-2026-09-22.md: the board archived verbatim into git.

Identity is the FULL normalized URL, not the origin, and that was measured:
origin identity collapses the 43 non-booth rows to 19 groups by merging eight
distinct gitea repositories into one row, three unrelated HuggingFace model
cards into one, and the two LRPG surfaces on 10.100.10.50:8321 — the design
doc's own example of two real benches — into one. Full-URL identity still
collapses both cases that doc names: talk 5 to 1, Peedlar 3 to 1.

booth link is NOT deprecated. Roughly 14 of the 35 distinct non-booth targets
are reference bookmarks for which the board is the right and only home; the
design doc's plan to deprecate it would have evicted a third of its live
content. Corrected there, along with what "normalized URL" means.

The seam review found three real defects in the contract before any code: the
claim that test_stdlib_only already forbids sibling imports (it exempts `booth`
on purpose), naming resolve_booth as the dead marker's existence check (it
raises HTTPException(404), so one swept booth would have 404'd the whole board
page), and silence on percent-encoding (booth links are emitted through
quote(name, safe=""), so a raw comparison marks every encoded booth dead
forever). That both list_booths and sweep_once skip the registry was verified
against the real functions rather than assumed.

444 -> 555 tests. Deployed and verified live: 23/23 booths 200, and the board
renders 156 dead of 221 rows, matching an independent pre-implementation count.

NOT TAGGED: both cold gates are in flight (contract review
01M35BWCJ806MT75NA630Y4WFH, code review 01M35CK8YKEKMV7T15JXEF6A8N) and the
bug-hunt has not run. Per the v0.2.0 lesson, the tag waits for the gates.
2026-09-22 13:25:32 -07:00

2.0 KiB

The 69% link-board rot was two defects wearing one number

2026-09-22 · booth

Re-measuring the board before writing U6's contract split its headline number in half, and the half U6 owns is the smaller one. The IA doc records 211 rows, 145 (69%) pointing at booths that no longer exist. Re-counted on 2026-09-22 the board was 221 rows — and the split nobody had taken before:

count share
rows that are booth URLs 178 80% of the board
…whose booth is already swept 156 71% of the whole board
rows that are NOT booth URLs 43 19%
…distinct after full-URL normalization 35
…collapsed by the re-post problem U6 names 8 rows

So the 69% is:

  1. Booth-announcement rot — 178 rows. A session posted a booth URL because a booth could not announce itself. U5 already closed the cause. Nothing stopped the habit, so the board took 11 more of these in the day after it was first measured.
  2. Bench re-post — 8 rows. An append log with no identity. This is the part the registry fixes, and it is an order of magnitude smaller.

The third thing, which the IA doc does not describe at all: of the 35 distinct non-booth targets, roughly 14 are running services (benches) and roughly 14 are reference bookmarks — gitea repos, HuggingFace model cards, a vLLM recipe, a Headscale page — with the rest ephemeral one-shot links. The IA doc planned for booth link to survive "as a deprecated alias". That would have evicted a third of the board's live content from the only home it has. U6 does not deprecate booth link; it removes exactly one shape from it.

Why this is worth keeping. The single 69% figure implies the registry is the big win. It is not — the enforced rule and the dead marker are. A unit scoped off the unsplit number would have built the registry, declared victory, and left 178 rows rotting. Re-measure before contracting; the number in the design doc is a day old the moment it is written.