Files
booth/persistent-memory.d/2026-09-22-u6-benches-landed.md
T
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

3.6 KiB

U6 landed — three surfaces, three jobs, one predicate

2026-09-22 · booth

The sixth of seven v1 units. Only U7 is left. 444 → 555 tests, suite green, deployed and verified live: 23/23 booths 200, and the board renders 156 dead of 221 rows — the exact count an independent shell measurement produced before a line of code was written, from two different implementations.

What shipped

  • booth/benches.py (new, stdlib-only AND sibling-free): Bench, normalize_bench_url, lenient read_benches, strict upsert_bench, set_bench_state, remove_bench, order_benches. Registry at ~/booth-data/.benches.json — a dotfile at the DATA ROOT, keyed by id, so two rows with one identity are impossible by construction.
  • links.booth_target — ONE predicate for "is this a booth URL", consumed by three callers (the CLI refusal, the board's dead marker, bench import). Host-agnostic and path-shaped; percent-decodes the name.
  • booth link refuses a booth URL, names booth new --why, and writes nothing — not even the board directory.
  • The board marks dead rows. Removal stays the operator's two clicks through the bulk control that already existed. Nothing in the unit deletes a row.
  • booth bench add|ls|state|rm|import; import writes nothing without --apply and never touches links.md.
  • docs/archive/links-2026-09-22.md — the board archived verbatim into git.

The decision that mattered most, and it was measured

Identity is the FULL normalized URL, not the origin. Collapsing the 43 non-booth rows by origin gives 19 groups; by full URL, 35. The difference is not duplication — it is eight distinct gitea repos merged into one, three unrelated HuggingFace model cards merged into one, and the two LRPG surfaces on 10.100.10.50:8321, which are the IA doc's own example of two real benches, merged into one. Origin identity destroys more than it dedups. Full-URL identity still collapses both cases the doc names (talk 5→1, Peedlar 3→1).

Query is IN the identity (three ShutterChute rows differ only by ?token= and are three real links); fragment is OUT; credentials are REFUSED, not stripped.

The seam review earned it again — three real contract defects

Run in-session against the real .py files, after the cold panel was dispatched:

  • SR-1 — the contract claimed test_stdlib_only already forbids sibling imports. It does not: its failure set is {r for r in roots if r != "booth" and ...}, which exempts booth on purpose. Only test_manifest.py has the strict copy. INV-9 would have shipped untested.
  • SR-2 — the contract named resolve_booth as the dead marker's existence check. That function is a closure inside create_app and raises HTTPException(404) — per row, one swept booth would 404 the whole board page.
  • SR-7 — booth links are emitted through quote(name, safe=""), so a predicate comparing the raw segment marks every encoded-name booth dead forever.

SR-4 and SR-5 were verified rather than assumed: both list_booths and sweep_once skip a child that is not a directory AND one whose name starts with a dot, so the registry is safe from the sweeper by two guards, not one. Had either been absent the design would have eaten its own registry on tick one.

Still open at the time of writing

Both cold gates are IN FLIGHT — contract review 01M35BWCJ806MT75NA630Y4WFH, code review 01M35CK8YKEKMV7T15JXEF6A8N. The bug-hunt has not run. Committed but NOT tagged, per the v0.2.0 lesson: if a gate is outstanding, the tag waits.