/heid-bug-hunt panel 01M35CRRK2RTVWWF1BN09AFQG3, diff-scoped against 91fd8bc.
The most severe of the three rounds, and three of its four convergent findings
were already closed by our own adversarial pass before the reply landed. Three
were not.
- The benches panel was nested inside the booth header's <span class="sub">.
The insertion had matched the first `{% if board %}` in the template rather
than the block-level one. A div inside a span is invalid HTML: the parser
closes the span implicitly and hoists the div out, orphaning the rest of the
sub-line. Nothing 500s, which is precisely why no test in this suite could
see it. Moved to block level, pinned by an offset assertion, and verified
with a real HTML parser.
- _booth_exists used a bare is_dir() while resolve_booth resolves and requires
the parent to BE the data root. They disagreed on a symlink: the marker
called a booth pointing outside the root alive while the page 404s it, so the
row rendered healthy and the link was dead. Same containment now, and
ValueError joins OSError in the guard -- one bad row must never cost the
other 220.
- The board append opened its fd OUTSIDE the lock. `flock LOCK printf ... >>
board` reads as locked and is not: the shell opens the append fd while
parsing, before flock acquires. A concurrent unlink replaces the inode via
os.replace, the old fd still points at the unlinked one, and the append
succeeds, reports success, and vanishes. Pre-existing rather than this
unit's, but it is silent data loss in the file this unit lives in. Proved by
holding the lock and asserting nothing is written.
- The atomic write used a predictable .tmp.<pid> name; a pre-planted symlink
there redirects the write straight through the replace. mkstemp with O_EXCL
in the same directory, and an fsync before the replace -- os.replace orders
the rename, not the data behind it.
Declined and recorded: on a host where booth.links cannot be imported, `booth
link` now refuses every URL rather than only booth ones. True, and kept. A
guard that fails open is not a guard, and that state is a broken install in
which most of the CLI is equally broken.
The sharpest line in the reply is one three arms found independently: this repo
had ALREADY paid for the RecursionError class in marks.py, and the new module
re-introduced the unguarded parse. Reading the new module in isolation would
never have surfaced that.
604 -> 607 tests.
44 KiB
contract_version, module, purpose, depends_on, language, complexity, estimated_loc, confidence, used_by, touches, assumptions, open_questions
| contract_version | module | purpose | depends_on | language | complexity | estimated_loc | confidence | used_by | touches | assumptions | open_questions | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1.0 | booth.benches | A bench is a running thing, registered -- not a booth, and not a bookmark. The standing link board absorbed all three jobs because only one of them had a surface, and it now carries 221 rows of which 178 (80%) are booth announcements and 156 (71% of the whole board) point at booths that were swept. U5 gave the booth announcement a home; this unit gives the RUNNING SERVICE one, and closes the loop by refusing the one shape that now has somewhere better to go. Identity is the normalized URL, so re-announcing a bench UPDATES its row instead of appending a fifth -- `talk` is on the board five times and Peedlar's root three. Nothing on the board is deleted by this unit: the dead rows are MARKED so the operator can see and remove them with the bulk control that already exists. |
|
python | medium | 320 | 0.80 |
|
|
|
|
U6 — benches
The defect, stated precisely
Re-measured 2026-09-22 against the live board, because the numbers in the IA doc are a day old and the board grew:
| IA doc, 2026-09-21 | today | |
|---|---|---|
| rows on the standing board | 211 | 221 |
| rows that are booth URLs | not split out | 178 — 80% of the board |
| …whose booth no longer exists | 145 (69%) | 156 — 71% of the whole board |
| rows that are not booth URLs | ~40 | 43 |
| …distinct after normalization | — | 35 |
The headline number in the IA doc — 69% rot — is two different defects wearing one number, and separating them is what makes this unit the right size:
-
Booth-announcement rot (178 rows). A session posted a booth URL because a booth could not announce itself. U5 closed the cause: a booth now carries
.booth.jsonand the index is the feed. Nothing yet stops the habit, so the board took 11 more of these rows in the day since it was measured. This unit's enforced rule is the stopper, and the dead marker is what lets the operator clear what already landed. -
Bench re-post (8 rows).
booth linkis an append with no identity, so re-announcing a bench creates a row rather than updating one:talkfive times, Peedlar's root three. This unit's registry is the fix, and it is the smaller half — which is worth saying plainly, because the IA doc's single 69% figure implies otherwise.
A third thing the measurement found, which the IA doc does not describe: the
board has a legitimate residual job. Of the 35 distinct non-booth targets,
roughly 14 are running services (benches) and roughly 14 are reference
bookmarks — gitea repositories, HuggingFace model cards, a vLLM recipe, a
Headscale setup page. The IA doc plans for booth link to survive "as a
deprecated alias". That would deprecate the only home a third of its live
content has. booth link is not deprecated by this unit. It loses exactly
one shape — the booth URL — and keeps the rest.
What the measurement changed
Two lines of docs/design/information-architecture.md are wrong and are
corrected in the same commit, rather than left for a reader to trip over:
id : normalized URLstays, but the doc does not say what normalized means, and the obvious reading — the origin — is measurably destructive here (8 gitea repos into one row). The doc gains the rule and the number behind it.- "
booth link… survives as a deprecated alias rather than vanishing" is struck. It survives as itself, minus one refused shape, for the reason above.
The record
@dataclass(frozen=True)
class Bench:
id: str # the normalized URL — the identity, and the dict key on disk
url: str # the URL AS POSTED — what a click goes to
name: str # what it is
owner: str # the althing handle that registered it, or "booth"
state: str # "live" | "promoted" | "retired"
added: str # ISO-8601 with offset, from the FIRST registration
updated: str # ISO-8601 with offset, from the most recent upsert
error: str | None = None # a read-time verdict; never stored
id and url are two fields on purpose. The identity must be normalized so
that re-posting updates; the href must be verbatim so that a URL whose server
cares about a trailing slash, a case-sensitive path or a query still works when
clicked. Collapsing them would make the registry quietly change where a link
goes, which is the kind of bug that surfaces as "the operator clicked a bench
and got a 404" and is never traced back here.
added survives re-registration; updated does not. That is the same shape as
U5's created, and for the same reason: an upsert is the same bench saying
something new about itself, not a new bench.
updated means the last MUTATION of the record, not the last upsert —
set_bench_state bumps it too. Amended after the cold panel read "most recent
upsert" literally and found the code bumping on a state change: the code is
right (a promotion is a change to the record and "last touched" should say so)
and the earlier wording was narrower than what anyone wants the field to mean.
Caps, and what "applied" means for each — stated per field, because it is not the same verb for all of them. The cold paraphrase panel found "applied at the write and again at the read" readable three ways (refuse / clip-for-display / truncate-and-store) with a different build behind each, and 4-of-4 arms flagged it.
| field | cap | at the write | at the read |
|---|---|---|---|
name |
120 | truncated | truncated |
owner |
64 | truncated | truncated |
url |
2048 | refused (normalize_bench_url raises) |
damage — reported, never clipped |
state |
one of three | refused | damage |
id |
2048 | refused, via the url it is derived from | not applied — see below |
name and owner are display budgets: clipping one costs a few characters in
a panel row. url is not a budget and must never be clipped, at either end
— INV-7 promises the click goes to the posted address byte for byte, and a
shortened URL keeps that promise in the type system while breaking it in the
browser. Nothing this code writes can store an over-long one; a hand-edited
registry can, and that is damage.
id is capped at the WRITE ONLY, and that asymmetry is deliberate.
normalize_bench_url refuses an input over URL_MAX, so nothing this code
writes can exceed it. On the read the id is the dict KEY and it is the locator
every control posts back — bench state, bench rm, and the panel's remove
button all address by it. Truncating a hand-edited over-long key on read would
produce a row the operator can see and cannot act on, which is strictly worse
than a long one. Amended after the cold panel found the code and the contract
disagreeing here; the code was right.
Signatures
BENCHES_FILE = ".benches.json" # at the DATA ROOT — not inside a booth
BENCH_LOCK = ".benches.lock"
BENCH_STATES = ("live", "promoted", "retired")
NAME_MAX, OWNER_MAX, URL_MAX = 120, 64, 2048
BENCHES_MAX_BYTES = 256 * 1024
def normalize_bench_url(url: str) -> str:
"""The identity of a bench. Raises ValueError with a reason a human can act
on -- the CLI prints it verbatim.
THE RULE, in full, because it is the identity and a vague identity is worse
than a wrong one:
* surrounding whitespace stripped
* scheme lowercased; anything but http/https is refused
* userinfo (`user:pass@host`) is REFUSED, never stripped
* host lowercased; an empty host is refused
* port dropped when it is the scheme default (80 for http, 443 for https)
* path kept verbatim, except that a bare "/" becomes ""
* query kept verbatim, INCLUDING its parameter order (a query is opaque)
* fragment dropped
"""
def read_benches(root: Path) -> tuple[list[Bench], str | None]:
"""Every registered bench, in the order of `order_benches`, plus a read-time
error or None. NEVER RAISES -- this is on the render path (v0.2.2 lesson)."""
def upsert_bench(root: Path, url: str, name: str, owner: str) -> tuple[Bench, bool]:
"""Register or update by normalized URL. Returns (bench, created).
`added` is preserved on update; `url`, `name`, `owner`, `updated` are
replaced. `state` is preserved on update and is "live" on create."""
def set_bench_state(root: Path, bench_id: str, state: str) -> Bench | None:
"""Move a bench between live / promoted / retired. None if no such bench."""
def remove_bench(root: Path, bench_id: str) -> Bench | None:
"""Drop one bench. Returns the removed record, or None."""
def order_benches(benches: Iterable[Bench]) -> list[Bench]:
"""ORDER: (state rank, name casefolded, id) -- live before promoted before
retired, then alphabetical, with the id as a total tie-break so two benches
sharing a name cannot swap between renders. CLAUDE.md invariant 6."""
And in booth/links.py, the one addition:
def booth_target(url: str) -> str | None:
"""The booth NAME a URL points at, or None when it is not a booth URL.
ONE PREDICATE, THREE CALLERS -- the CLI's refusal, the board's dead marker,
and the import's classifier. They must agree: a rule that refuses a shape
the board then fails to mark as dead (or the reverse) is two readers of one
truth, which is the bug this repo has now paid for three times.
THE NAME SEGMENT IS PERCENT-DECODED. `app.py` emits booth links through
`quote(name, safe="")`, so a booth whose name needs encoding appears on the
board encoded. Comparing the raw segment against a directory name would mark
every such booth dead and would print the encoded form back at the poster in
the refusal message. Seam review SR-7.
Returns the DECODED name. A path of `/b/` with no name, or a decoded name
that is empty, starts with a dot, or contains a separator or `..`, is not a
booth link (None) — the same rules `resolve_booth` enforces, so the two
cannot disagree about what is addressable.
"""
The enforced rule
booth link <url> refuses when booth_target(url) is not None:
$ booth link http://10.100.10.50:8090/b/sindra-bakeoff/ "the bakeoff"
booth link: that is a booth, and a booth announces itself now.
booth new sindra-bakeoff --why "the bakeoff" (or --why on `booth add`)
the index at http://10.100.10.50:8090/ is the feed.
exit 2
Three properties this refusal must have, each of which is an invariant below:
-
It names the alternative. The teaching moment belongs at the point of use; 17 handles have the muscle memory and a bare "refused" would send them to a human.
-
It writes nothing — nothing at all. Not the row, not the board directory, not the
.booth.jsonannouncementbooth linkcreates on first use, not a lock file. The test asserts the data root's entries are unchanged, not merely thatlinks.mdlacks the row.(Amended: this listed two items while INV-3 listed four, so a reader of the prose alone could conclude a lock file was permissible. One list now, and it is the strict one.)
-
It is the ONLY new refusal. A reference bookmark is still a link.
What renders
On the standing board's page, above the rows:
-
The benches panel — each bench as name, URL, owner, state, and the date it was added; ordered by
order_benches. Controls to change state and to remove, both POST, both reversible in one click except remove. -
A board row whose booth is gone is marked dead — visibly, with its checkbox pre-reachable by the existing select-all, so the operator can tick and use the
unlink-manycontrol already on the page. No new delete path.Dead means exactly this, and both halves are load-bearing:
booth_target(row.url)is not None AND the name it returns is not a live directory in the data root. A row that is not a booth link is never dead, no matter what it points at — the Booth cannot know whether a gitea repo still exists and must not guess. A booth link whose booth is alive is not dead. No lifetime arithmetic is involved: a booth one minute from expiry is alive. (Stated after 3-of-4 cold arms read the rule two ways — predicate-driven vs existence-driven — with 221 rows riding on which.)
A registry that cannot be read renders as a panel carrying its error, never as
an absent panel and never as a 500 — the v0.2.2 lesson, which this repo learned
by returning 500 for / and /healthz across all 25 booths.
The panel is gated on PAGE IDENTITY — the booth carries a links.md — and
never on content. A content gate (board or benches) hides the panel AND its
registration form exactly when the board is empty and the registry absent,
which is the state a fresh deployment starts in and the one where "no benches
registered yet" is most worth saying. That is the same defect as a damaged
panel rendering as an absent one, one level up. Amended after the cold panel
found the content gate shipped.
The CLI surface
booth bench add <url> <name> register or update; prints registered/updated
booth bench ls list, in the rendered order, with ids
booth bench state <id|url> <s> live | promoted | retired
booth bench rm <id|url> remove one
booth bench import classify the board's rows; WRITES NOTHING
booth bench import --apply <id>... register ONLY the ids you name
`<id|url>` takes EITHER form because the input is normalized before the lookup,
and normalization is idempotent — an id normalizes to itself. So the id `ls`
prints and the raw URL in the operator's scrollback both address the same row.
Pinned by a test, because it is the property that makes the two-form promise
true rather than merely intended.
import prints three groups — booth rows (skipped; booth_target matched),
candidates (the normalized id beside the raw URL, so a collapse is visible
before it happens), and refused (normalization raised, with the reason).
--apply REQUIRES THE IDS. A bare --apply is refused. This is the
unit's sharpest correction and it came from all four arms of the cold paraphrase
panel independently: the first draft registered every candidate, which made the
write path do the exact thing this document's own rationale calls impossible —
tell a bench from a bookmark by its URL — silently, to roughly 14 of 35 rows
that belong on the board. The dry run prints ids; the operator names the ones
that are benches; an id that is not a candidate is refused and nothing is
written. There was no selection mechanism between the report and the write, and
the report existed precisely because the decision is not mechanizable.
The migration
links.mdis archived verbatim to~/booth-data/links/links-archive-2026-09-22.mdand committed to this repo, before anything else. Nothing the operator wrote is destroyed, and the archive is version-controlled rather than living only on one box.booth bench importproposes; the operator applies by naming ids.- The 156 dead booth rows are marked, and removed by him or not at all.
Scope — the blast-radius pass
graphify explain over remove_link_entry, parse_link_entries,
order_for_display, read_pins and toggle_pin, cross-checked with grep
because graphify cannot see the CLI's python3 -c import (it reports the
app.py importers and the test callers; scripts/booth:353 is invisible to it
— the exact blindness CLAUDE.md names).
No existing function in links.py changes signature or behaviour. The board's
rows keep their content-hash identity, so every pin, every unlink id in the
operator's history, and every concurrent booth link append keep working
untouched.
Out of scope
- Liveness probing. The IA doc's BENCH shape carries
last_checked/last_ok; ROADMAP's v1 row does not — it names registry, identity, enforced rule, migration, and the parking lot already parks the uptime history. This unit ships none of it, deliberately: it is the only part that does network I/O, which is the part that reliably takes 2–5 follow-up patches for cases the first shape did not anticipate — the accretion signature this whole rewrite is undoing. The record is designed so adding it later is purely additive (the read is lenient to unknown keys, so an older Booth reading a newer file does not break). This is a scope reduction against the IA doc and the operator can reverse it; the cost of reversing it is one field pair and one CLI verb. - Pruning the board. Not in v1, by ROADMAP.
- Bench aliases. See open questions.
- A bench page. A bench is a link to somewhere else; giving it a page here would make the Booth a directory service.
- Any change to how booths announce themselves. That was U5 and it landed.
Invariants
INV-1 — one module knows the registry's filename and shape.
booth/benches.py is the only place .benches.json is named, parsed or
written. No route body and no CLI branch constructs the path or reads the JSON.
Falsifiable: a test that fails if the literal .benches.json appears anywhere
outside benches.py — and specifically fails under the change that defeats it,
which is a route reading the file directly to save an import. Asserting only
that the panel renders would pass under exactly that change.
INV-2 — one predicate decides what a booth URL is. links.booth_target is
the only implementation, and the CLI's refusal, the dead marker and the import's
classifier all call it.
Falsifiable: the defeating change is a second implementation — a /b/ check
inlined in the shell for speed, or a regex in app.py. One table of URLs
(trailing slash, no slash, nested path, query, uppercase host, a non-Booth host
with a /b/ path, a /b/ with no name, a percent-encoded name, a decoded ..
and a decoded separator) runs through the predicate, the CLI's refusal AND the
render's dead marker.
AGREEMENT IS THE WEAKER HALF AND IS NOT THE TEST. Three callers of one
wrong predicate agree perfectly, so agreement alone pins nothing — the table's
expected values are the independent check, and the agreement rows exist to
catch a second implementation drifting from the first. Both are asserted; only
one of them would survive booth_target itself being wrong. (Named after a
cold arm pointed out that the falsifier reads as though agreement were
sufficient.) A bare /b/ with no name is NOT a booth link, and the table
pins that.
INV-3 — a refused link writes nothing. No row, no board directory, no
.booth.json, no lock file.
Falsifiable: the defeating change is moving the refusal after the mkdir -p /
announce block in the link branch — which is where it would naturally land
if written without thinking. The test refuses a link into a data root with NO
links booth and asserts the directory still does not exist, not merely that
links.md lacks the row. Asserting the row's absence alone would pass under the
defeating change.
INV-4 — the rendered bench order is total and stated. (state rank, name casefolded, id).
Falsifiable: the defeating change is dropping the id tie-break, which leaves
two benches sharing a name in whatever order the dict yielded. The test
registers two benches with the SAME name in both insertion orders and asserts
the same output sequence from both. A test over distinct names would pass with
no tie-break at all.
INV-5 — the read cannot raise, and cannot cost the caller unboundedly.
read_benches returns ([], "...") for damaged, absent, oversized, or
unreadable; it never propagates. Over BENCHES_MAX_BYTES is refused by size
before it is parsed.
Falsifiable: the defeating change is json.load without the guard. The test
GETs the standing board's page with the registry (a) absent, (b) holding
non-JSON bytes, (c) holding valid JSON of the wrong shape, (d) holding a
well-formed record with a wrong-typed field, (e) over the size cap, and (f)
chmod'd unreadable, asserting 200 for all six AND that (b)–(f) render a visible
error rather than an empty panel. Case (d) is the one that matters: it is the
shape that is currently 500ing the gallery elsewhere in this service.
INV-6 — the identity collapses a re-post and nothing else. Upserting the
same normalized URL updates one row; upserting two URLs that differ in scheme,
host, non-default port, path, or query creates two. That list is
EXHAUSTIVE — the only things normalization discards are a fragment, a
scheme-default port, letter case in the scheme and host, a bare / path, and
surrounding whitespace.
(Amended: this said "path, query or host" with no "only", which reads as
illustrative and left an implementer free to "fix" the rule from the
invariant's wording — and it omitted scheme and port, two of the five. 3-of-4
cold arms flagged it; the falsifier now carries vectors for both.)
Falsifiable: the defeating change is normalizing to the origin. The test
registers the eight gitea URLs measured on the live board and asserts eight
benches, then registers talk's five rows and asserts one — the same
fixture proves both directions. A test that only checked the talk collapse would
pass under origin normalization, which is precisely the wrong rule.
INV-7 — url is what a click goes to; id is never rendered as an href.
Falsifiable: the defeating change is rendering bench.id in the anchor
because it is "the clean one". The test registers a URL whose normalization
differs from its raw form — an uppercase host, an explicit default port, and
a fragment — and asserts the anchor's href is the raw string, byte for byte.
(Amended: this parenthetical used to name "a trailing slash on a non-empty
path" as one of the differences. It is not one — the rule list keeps a
non-empty path verbatim, slash included, and INV-6 makes …/p and …/p/ two
benches. Two passages of this document disagreed about the same character, and
3-of-4 cold arms found the contradiction. The rule list is correct; this
sentence was wrong.)
INV-8 — nothing this unit ships removes a board row. The dead marker is a
render-time stamp; import without --apply writes nothing anywhere; import
with --apply writes only the registry and its lock sidecar (.benches.json,
.benches.lock) and never touches links.md.
(Amended: this said "writes only .benches.json", which contradicted the
unit's own assumption that every read-modify-write is held under an flock on a
sidecar. The cold panel caught the contract arguing with itself. The
load-bearing half — links.md is not touched — is unchanged and is what the
test hashes.)
Falsifiable: the defeating change is import --apply "tidying up" the rows it
consumed. The test snapshots links.md byte for byte, runs the full unit's CLI
surface against it — refusal, import, import --apply, bench add, bench rm — and
asserts the file is unchanged, including its mtime-independent content hash.
INV-9 — stdlib-only, and sibling-free. booth/benches.py imports nothing
outside the standard library and nothing from booth.*.
Falsifiable: the defeating change is from booth.links import booth_target —
which is the natural thing to write, since booth_target is the predicate this
unit's CLI branch also needs.
The existing parametrized test_stdlib_only in tests/test_marks.py DOES
NOT CATCH THAT, and an earlier draft of this contract claimed it did. Its
failure set is {r for r in roots if r != "booth" and r not in sys.stdlib_module_names} — it exempts booth explicitly, so a sibling import
passes it clean. The sibling-free clause exists only in the stricter copy in
tests/test_manifest.py. Adding benches to the parametrized list therefore
buys stdlib-only and NOT sibling-free. So: benches joins that list AND
tests/test_benches.py carries its own stricter copy, mirroring manifest's,
which fails on a booth root. Verified by reading the real test — seam review
SR-1.
Seam review — what the real sibling surfaces said
Run in-session against the actual .py files rather than their contracts,
after the cold panel was dispatched and before any code. Seven checks, five
findings, three of them real defects in this document. /heid-contract-review
is artifact-only by design and structurally cannot run this pass: its arms read
this file and are forbidden the siblings it borrows from.
| # | seam | what the real surface said | disposition |
|---|---|---|---|
| SR-1 | test_stdlib_only (tests/test_marks.py) |
The contract was wrong. It claimed the parametrized test "already carries" the sibling-free clause. It does not — its failure set is {r for r in roots if r != "booth" and ...}, which exempts booth on purpose. Only tests/test_manifest.py:209 has the strict copy. |
Fixed. INV-9 now requires both: the parametrize entry AND a stricter copy in tests/test_benches.py. Without this the unit would have shipped with its own INV-9 untested. |
| SR-2 | resolve_booth (booth/app.py) |
The contract invited an outage. It named resolve_booth as the existence check for the dead marker. That function is a closure inside create_app (not importable) and raises HTTPException(404) — called per row, one swept booth would 404 the entire board page. It also calls .resolve(), a syscall per row, 178 of them on this board. |
Fixed. depends_on now forbids it explicitly and specifies an own non-raising predicate with the same name-safety rules. Cost stated below. |
| SR-7 | quote(name, safe="") (app.py, booth link emission) |
The contract was silent on encoding. Booth links are emitted percent-encoded. A booth_target comparing the raw path segment to a directory name marks every encoded-name booth permanently dead and echoes the encoded form back in the refusal. |
Fixed. booth_target decodes, and applies resolve_booth's own addressability rules so the two cannot disagree. |
| SR-6 | scripts/booth dispatch (flat case "$cmd", 13 single-word verbs) |
Not a defect — a gap. bench add would be the first two-word verb in this script. Nothing about the existing dispatch anticipates one, and booth bench with no sub-verb must not fall through into the generic usage in a way that hides which word was wrong. |
Recorded. A nested case under bench), and a bare bench prints the bench verbs specifically. Named so the implementer does not invent a third pattern. |
| SR-3 | data_dir (booth/app.py) vs DATA (scripts/booth) |
The service resolves and expands its root in create_app; the CLI derives it from $BOOTH_DATA_DIR. Two independent derivations of one path. |
No change. This is already true of links.md, .marks.json and .booth.json — pre-existing and out of this unit's scope. Recorded so it is a known property rather than a discovery. |
| SR-4 | list_booths (booth/app.py) |
Confirmed, not assumed. if not child.is_dir() or child.name.startswith("."): continue — .benches.json fails both guards. The index cannot see the registry. |
Verified. The assumption stands on read code. |
| SR-5 | sweep_once (booth/app.py) |
Confirmed, not assumed — and this was the dangerous one. The sweeper iterates the data root and could in principle delete the registry. It cannot: the same is_dir() + leading-dot pair guards it, and shutil.rmtree is reached only past both. |
Verified. Had either guard been absent this unit would have shipped a design that eats its own registry on the first tick. |
The per-render cost, stated because SR-2 surfaced it. The dead marker runs
once per board row: 221 rows today, 178 of which parse as booth links and cost
one is_dir() each. That is one stat per booth row per render of the standing
board's page — and the page already does a booth_items walk plus a hold_read
per booth on the index, so it is not a new order of magnitude. It is bounded by
the row count, it touches no network, and it is confined to the ONE booth that
carries a links.md. If the board ever grows past a few thousand rows this
becomes worth caching; at 221 it would be premature.
Code review — what the cold panel found
/heid-code-review panel 01M35CK8YKEKMV7T15JXEF6A8N, four arms, verdict
NOT drift-zero. Folded in full. Three findings were independently reported
by all four arms, which is the signature of a contract clause that was
written as prose and never converted into an assertion.
| # | finding | arms | disposition |
|---|---|---|---|
| A | The panel dropped the added date. What renders says "the date it was added"; b.added appeared nowhere in the template and no test asked for it. |
4/4 | Fixed — rendered, and pinned by a test. |
| B | bench ls printed no ids, and the truncated URL it printed was not pasteable into bench state|rm. Worse: the test's own docstring claimed it printed ids while asserting nothing — a claim standing in for evidence, which is how the drift would have survived CI. |
4/4 | Fixed — the id prints whole and last; the test now round-trips what ls prints back through bench state. |
| C | bench import printed the description, not the raw URL, beside each id — hiding the five-rows-of-talk collapse the clause exists to expose. |
4/4 | Fixed — raw URL beside the id, description demoted to a continuation line. |
| D | An IPv6 literal lost its brackets. http://[::1]:8080/a normalized to http://::1:8080/a — not another spelling but a BROKEN identity, so a re-post never matches the row. |
3/4 | Fixed — bracketed literals are re-wrapped; an unbracketed one is refused with a reason rather than guessed at. |
| H | INV-4's tie-break falsifier could not fail. _write_all serializes with sort_keys=True, so both insertion orders came back off disk already id-sorted and removing the tie-break left the test green. |
1/4 | Fixed — the test now calls order_benches directly with records that tie on both prior keys. A vacuous falsifier of exactly the class persistent-memory.d/2026-09-22-vacuous-falsifiers.md names, found by a cold reader and not by us. |
| I | An empty board hid the whole panel, registration form included — the state a fresh deployment starts in. | 1/4 | Fixed — gated on page identity. |
| J | The booth link refusal could fail OPEN on a name bash's $() erases, because it classified by captured-text emptiness. |
1/4 | Fixed — the predicate answers with a B:/N sentinel, so no name can be mistaken for "not a booth". |
| K | A FIFO at the registry path blocked in open(); a deeply-nested JSON RecursionError escaped the except (ValueError, OSError) pair. |
1/4 | The FIFO half was already fixed by our own pass before the reply landed. The RecursionError half was not — 200k open brackets is 200 KB, well inside the byte cap, and it 500'd the page the function exists to protect. Fixed. |
| E | The read does not apply the id cap the contract promised. |
3/4 | Contract amended, code kept. The id is the locator every control posts back; truncating a hand-edited over-long key would make a row visible and unactionable. |
| F, G | INV-5's render test covered 5 of 6 cases and asserted only status 200; INV-2's URL table never ran through the dead-marker render. | 4/4, 3/4 | Both fixed — the render test now covers oversized, unreadable and FIFO and asserts the error is visible; the full table runs through the marker. |
Also folded from the per-invariant vacuity pass (the arms' "what would still
pass" section, which is the single most useful thing the panel produced):
INV-6 had no vector asserting a non-default port is part of the identity, so
"always omit the port" passed every row; INV-3 asserted only that links/ was
absent, so a refusal touching any other sidecar passed; INV-8's hashed sequence
omitted bench ls; INV-9's AST walk is defeated by __import__("booth.links").
All four closed.
Declined: nothing. Amended rather than fixed: E, updated's meaning,
INV-8's file list, the registered/created wording, and every line number in
this document's prose — the panel found two already stale, which is the whole
argument against putting them in prose at all.
Bug hunt — what the cold panel found
/heid-bug-hunt panel 01M35CRRK2RTVWWF1BN09AFQG3, four arms, diff-scoped
against 91fd8bc. The most severe of the three rounds, and three of its four
convergent findings were already closed by our own adversarial pass before the
reply landed — which is the complementarity the skill claims, measured in both
directions on one diff.
| finding | arms | state when the reply landed |
|---|---|---|
A single malformed board row blanks the ENTIRE 221-row board. %00 in a booth name decodes to an embedded NUL; Path.is_dir() raises ValueError, not OSError; _board_rows' blanket handler returns []. Every row vanishes, the page still 200s, nothing says why. |
4/4 | Already fixed (control-character guard). |
RecursionError escapes read_benches and 500s the board page. ~4 KB of nested brackets, well under the byte cap. Three arms independently cited the precedent: this repo already paid for this exact class in marks.py — the new module re-introduced the unguarded parse. |
4/4 | Already fixed. |
| A FIFO still blocks the render path while the code comment claims the hang lesson was applied. | 4/4 | Already fixed — and the comment that lied about it was the thing that made us look. |
| IPv6 bracket loss. Second independent sighting, same root. | 4/4 | Already fixed by the code-review round. |
The benches panel is nested inside <span class="sub">. A <div> in a <span>: the parser closes the span implicitly and hoists the div out, orphaning the rest of the sub-line. Nothing 500s, which is why no test could see it. |
3/4 | OPEN — fixed now. Moved to block level; pinned by an offset assertion and verified with a real HTML parser (0 block-in-span violations). |
_booth_exists and resolve_booth disagree on a symlink. The marker called a booth pointing outside the data root alive while the page 404s it — the row renders healthy and the link is dead. |
3/4 | OPEN — fixed now. Same containment, same rules. |
The board append opens its fd OUTSIDE the lock. flock LOCK printf … >> board reads as locked and is not: the shell opens the append fd while parsing. A concurrent unlink replaces the inode via os.replace, the old fd keeps pointing at the unlinked one, and the append succeeds, reports success, and vanishes. |
solo | OPEN — fixed now. Pre-existing, not this unit's, but it is silent data loss in the file this unit lives in. Proved by holding the lock and asserting nothing is written. |
A pre-planted symlink at the predictable .benches.json.tmp.<pid> defeats the atomic write. The replace is atomic, not safe. |
solo | OPEN — fixed now. mkstemp (O_EXCL, same directory), plus an fsync before the replace, because os.replace orders the rename and not the data behind it. |
| A successful registration can cross the read cap and poison the registry; an empty board hides the panel. | solo | Already fixed by the contract round. |
Declined, with the reasoning recorded. Kimi: the python3 -c guard under
set -e means that on a host where booth.links is not importable, booth link now refuses every URL, not just booth ones — the refusal mechanism
refuses everything, while the sibling announce call degrades gracefully.
True, and kept as-is deliberately. A guard that fails open is not a guard,
and the state it describes (the package unreachable from the script that
computes its path from its own location) is a broken install in which booth new, booth add and booth ask are equally broken. Loud failure with a
message naming what is missing beats silent non-enforcement. Recorded rather
than silently dismissed, because the asymmetry with announce is real.
What the round says about the method. The two lenses were complementary in
both directions on one diff: the cold panel found three live defects the
in-session pass missed (all three invisible to a test — a layout nesting, a
symlink disagreement, a lock-ordering race), and the in-session pass had already
closed three of the panel's four convergent findings. Neither substitutes for
the other. The sharpest single line in the reply is the one noting this repo had
already paid for the RecursionError class in marks.py — a new module
re-introduced a bug the codebase had a test for, which no amount of
reading the new module in isolation would surface.