# 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. ## How it closed All three cold gates came back and were folded in full, with exactly one finding declined. Released as `v0.6.0` — see [[2026-09-22-u6-benches-released]] and [[2026-09-22-three-cold-panels-on-one-unit]]. The tag waited for the gates, per the v0.2.0 lesson, and that sequencing was right: the panels produced ten code fixes after this entry was first written.