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:
+165
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user