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.
This commit is contained in:
vh
2026-09-22 13:25:32 -07:00
parent 91fd8bc69d
commit 1c3ce5ddb5
16 changed files with 2277 additions and 26 deletions
+165 -1
View File
@@ -16,9 +16,29 @@
# pick holds its own booth, see below)
# booth unkeep <name> hand it back to the sweeper
# booth link <url> [description] append a link to the standing link board
# REFUSES a booth URL — a booth announces
# itself now; use `booth new --why`
# booth links list the board, numbered, with entry ids
# booth unlink <id|index> remove ONE link from the board
#
# booth bench add <url> <name> register or UPDATE a bench (upsert)
# booth bench ls list benches, live -> promoted -> retired
# booth bench state <id|url> <state> live | promoted | retired
# booth bench rm <id|url> remove one
# booth bench import [--apply] classify the board's rows; writes NOTHING
# without --apply, and never edits links.md
#
# THREE SURFACES, THREE JOBS. Telling them apart is the whole of U6:
# a BOOTH is a review surface you post work to. It announces itself and is
# swept 24h after its last activity. `booth new` / `booth add`.
# a BENCH is a running thing — jackdaw's bench, talk's bench, the things that
# get promoted to Homepage. Durable, and identified BY ITS URL, so posting
# it again updates the row instead of adding a fifth. `booth bench add`.
# a LINK is a reference bookmark — a repo, a model card, a doc page. The
# standing board, unchanged and NOT deprecated. `booth link`.
# The board carried all three because only one of them had a surface: 178 of its
# 221 rows were booth URLs and 156 of those pointed at booths already swept.
#
# booth ask <name> <id> <prompt> <option>... [--no-notes]
# pose a multiple-choice question in a booth
# booth marks <name> [--wait [SECS]] print every mark in a booth as JSON;
@@ -184,8 +204,31 @@ whoami_handle() {
echo "${ALTHING_HANDLE:-${BOOTH_SOURCE:-$(hostname -s 2>/dev/null || echo unknown)}}"
}
# Where booth/*.py lives, for the `python3 -c` calls below. The CLI runs under
# the SYSTEM python3 with no venv, which is why every module it imports is
# stdlib-only (CLAUDE.md invariant 1) and why no AST extractor can see these
# imports — `tests/test_cli.py` runs the real script, and is the only thing that
# catches a third-party import before a fleet host does.
booth_src() {
(cd "$(dirname -- "$(readlink -f -- "$0")")/.." && pwd)
}
# The booth NAME a URL points at, or empty. ONE PREDICATE — this shells out to
# booth.links.booth_target rather than pattern-matching `:8090/b/` here, because
# the board's dead-row marker and `bench import` use that same function and a
# second implementation in the shell would classify the host-agnostic and
# percent-encoded cases differently (INV-2).
booth_target_of() {
BOOTH_SRC="$(booth_src)" BOOTH_Q="$1" python3 -c '
import os, sys
sys.path.insert(0, os.environ["BOOTH_SRC"])
from booth.links import booth_target # stdlib only — no venv needed
sys.stdout.write(booth_target(os.environ["BOOTH_Q"]) or "")
'
}
usage() {
echo "usage: booth {new <name> [--why W] [--title T]|add <name> <file>... [--why W] [--title T]|url <name>|ls|rm <name>|keep <name>|unkeep <name>|blur <name> <file>...|unblur <name> <file>...|link <url> [description]|links|unlink <id|index>|ask <name> <id> <prompt> <option>... [--no-notes]|marks <name> [--wait [SECS]]|asks <name> (deprecated alias for marks)|answer <name> <id> [--wait [SECS]]|marks-import <name>}" >&2
echo "usage: booth {new <name> [--why W] [--title T]|add <name> <file>... [--why W] [--title T]|url <name>|ls|rm <name>|keep <name>|unkeep <name>|blur <name> <file>...|unblur <name> <file>...|link <url> [description]|links|unlink <id|index>|ask <name> <id> <prompt> <option>... [--no-notes]|marks <name> [--wait [SECS]]|asks <name> (deprecated alias for marks)|answer <name> <id> [--wait [SECS]]|marks-import <name>|bench add <url> <name>|bench ls|bench state <id|url> <live|promoted|retired>|bench rm <id|url>|bench import [--apply]}" >&2
exit 2
}
@@ -279,6 +322,21 @@ case "$cmd" in
[ $# -ge 1 ] || usage
link_url="$1"; shift
desc="${*:-}"
# THE REFUSAL COMES FIRST, BEFORE ANY WRITE (INV-3). A booth announces
# itself now (U5), so a booth URL on the board is a row that rots the
# moment the booth is swept — 156 of the board's 221 rows are exactly
# that. Refusing AFTER the mkdir/announce below would leave a new booth
# behind as the side effect of a call that failed.
refused_name="$(booth_target_of "$link_url")"
if [ -n "$refused_name" ]; then
{
echo "booth link: that is a booth, and a booth announces itself now."
echo " booth new $refused_name --why \"${desc:-what the operator is looking at}\""
echo " (or --why on \`booth add\`; re-announcing keeps the original stamp)"
echo " the index at $URL/ is the feed."
} >&2
exit 2
fi
board="$DATA/$LINKS_BOARD"
mkdir -p -- "$board"
: > "$board/$KEEP" # the board is durable by definition
@@ -357,6 +415,112 @@ if removed is None:
print("removed: %s %s" % (removed["desc"], removed["url"]))
' "$board" "$target"
;;
bench)
# SEAM REVIEW SR-6: the first two-word verb in this script. A nested case,
# and a bare `bench` names the bench verbs rather than falling through to
# the generic usage, which would hide which of the two words was wrong.
sub="${1:-}"; shift || true
case "$sub" in
add|ls|state|rm|import) ;;
*)
echo "usage: booth bench {add <url> <name>|ls|state <id|url> <live|promoted|retired>|rm <id|url>|import [--apply]}" >&2
exit 2 ;;
esac
BOOTH_SRC="$(booth_src)" BOOTH_DATA="$DATA" BOOTH_SUB="$sub" \
BOOTH_WHO="$(whoami_handle)" BOOTH_BOARD="$LINKS_BOARD" \
python3 -c '
import os, pathlib, sys
sys.path.insert(0, os.environ["BOOTH_SRC"])
# stdlib only — no venv needed. benches.py imports no sibling either (INV-9).
from booth.benches import (normalize_bench_url, order_benches, read_benches,
remove_bench, set_bench_state, upsert_bench)
from booth.links import booth_target, parse_link_entries
root = pathlib.Path(os.environ["BOOTH_DATA"])
sub, who = os.environ["BOOTH_SUB"], os.environ["BOOTH_WHO"]
argv = sys.argv[1:]
def die(msg, code=2):
print("booth bench: %s" % msg, file=sys.stderr)
raise SystemExit(code)
def row(b):
# ONE LINE PER BENCH, in the rendered order — state first, so a retired
# bench sinks, then name, then id as a total tie-break (INV-4).
return "%-9s %-28s %-52s %s" % (b.state, b.name[:28], b.url[:52], b.owner)
if sub == "add":
if len(argv) < 2: die("bench add <url> <name>")
try:
bench, created = upsert_bench(root, argv[0], " ".join(argv[1:]), who)
except ValueError as exc:
die(exc)
print("%s: %s" % ("registered" if created else "updated", bench.id))
elif sub == "ls":
benches, err = read_benches(root)
if err:
die("the registry could not be read: %s" % err, 3)
if not benches:
print("no benches registered yet")
for b in benches:
print(row(b))
elif sub in ("state", "rm"):
if not argv: die("bench %s <id|url>%s" % (sub, " <state>" if sub == "state" else ""))
try:
bench_id = normalize_bench_url(argv[0])
except ValueError as exc:
die(exc)
if sub == "rm":
gone = remove_bench(root, bench_id)
if gone is None: die("no such bench: %s" % bench_id, 1)
print("removed: %s" % gone.url)
else:
if len(argv) < 2: die("bench state <id|url> <live|promoted|retired>")
try:
moved = set_bench_state(root, bench_id, argv[1])
except ValueError as exc:
die(exc)
if moved is None: die("no such bench: %s" % bench_id, 1)
print("%s is now %s" % (moved.url, moved.state))
elif sub == "import":
apply = "--apply" in argv
board = root / os.environ["BOOTH_BOARD"] / "links.md"
if not board.is_file(): die("no link board at %s" % board, 1)
skipped, candidates, refused = [], [], []
for e in parse_link_entries(board.read_text()):
name = booth_target(e["url"])
if name is not None:
skipped.append((e, name)); continue
try:
candidates.append((normalize_bench_url(e["url"]), e))
except ValueError as exc:
refused.append((e, str(exc)))
print("SKIPPED — booth rows; a booth announces itself now (%d):" % len(skipped))
for e, name in skipped:
print(" %-30s %s" % (name, e["url"]))
print()
print("CANDIDATES — would be registered (%d rows, %d distinct):"
% (len(candidates), len({i for i, _ in candidates})))
for i, e in candidates:
print(" %-52s %s" % (i, e["desc"][:60]))
print()
print("REFUSED — normalization said no (%d):" % len(refused))
for e, why in refused:
print(" %-52s %s" % (e["url"], why))
if not apply:
print()
print("nothing was written. re-run with --apply to register the candidates.")
print("NOTE: a machine cannot tell a bench from a bookmark by its URL —")
print(" roughly 14 of 35 live candidates are repos, model cards and docs,")
print(" for which the board is the right home. Review before applying.")
raise SystemExit(0)
for i, e in candidates:
upsert_bench(root, e["url"], e["desc"], e["who"] or who)
print()
print("applied: %d distinct benches registered. links.md was NOT modified."
% len({i for i, _ in candidates}))
' "$@"
;;
ask)
# booth ask <name> <id> <prompt> <opt>... [--no-notes]
[ $# -ge 5 ] || usage