fix(u6): fold the cold contract panel — the import selection gap, and a document arguing with itself
/heid-contract-review panel 01M35BWCJ806MT75NA630Y4WFH. The headline arrived from all four arms independently and it is a missing feature, not a wording problem. `bench import --apply` registered every candidate, while the same contract says roughly 14 of 35 are reference bookmarks that must stay on the board. There was no selection mechanism between the dry-run report and the write -- so the write path did the exact thing this unit's rationale calls impossible, tell a bench from a bookmark by its URL, silently, to rows that belong where they are. The report existed precisely because the decision is not mechanizable. `--apply` now takes the ids the operator names; a bare `--apply` is refused and an unknown id is refused, both writing nothing. Two solo findings, both real: - A successful registration could push the registry past the size its own reader refuses, so the LAST bench added would make every other bench invisible while reporting success. The writer now respects the reader's cap. - The credential ban covered bench URLs and not `booth link`, the door this unit did not touch -- and the board renders on an unauthenticated LAN surface. A password can no longer reach it through either door. A small deliberate widening, named rather than smuggled. Cap semantics were readable three ways (refuse / clip-for-display / truncate-and-store) with a different build behind each, 4-of-4. Now stated per field: name and owner truncate, url and state are refused at the write and are DAMAGE at the read. url is not a display budget -- INV-7 promises the click goes to the posted address byte for byte, and a clipped URL keeps that promise in the type system while breaking it in the browser. The code had been clipping it; fixed. Two passages disagreed about one character: INV-7's specimen named "a trailing slash on a non-empty path" as something normalization changes, while the rule list keeps it and INV-6 makes the two spellings two benches. The rule list is right; the specimen was wrong. Found by 3-of-4. Also: INV-6's component list was illustrative where it had to be exhaustive and was short scheme and port; "writes nothing" appeared twice with different lists; the dead marker's predicate was readable two ways with 221 rows riding on it; and INV-2's falsifier read as though three callers agreeing pinned something, when three callers of one wrong predicate agree perfectly -- the table's expected values are the real check and now say so. 597 -> 604 tests.
This commit is contained in:
@@ -123,8 +123,26 @@ 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: `name` 120, `owner` 64, `url` 2048, `state` one of three — applied at the
|
||||
write and again at the read. Each is a display budget, not a storage limit.
|
||||
**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
|
||||
@@ -228,9 +246,14 @@ 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.** Not the row, and not the board's `.booth.json`
|
||||
announcement that `booth link` creates on first use — a refused call must not
|
||||
leave a new booth behind as a side effect.
|
||||
- **It writes nothing — nothing at all.** Not the row, not the board
|
||||
directory, not the `.booth.json` announcement `booth link` creates on first
|
||||
use, not a lock file. The test asserts the data root's entries are unchanged,
|
||||
not merely that `links.md` lacks 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
|
||||
@@ -244,6 +267,15 @@ On the standing board's page, above the rows:
|
||||
checkbox pre-reachable by the existing select-all, so the operator can tick
|
||||
and use the `unlink-many` control 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.
|
||||
@@ -263,18 +295,29 @@ 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 [--apply] classify the board's rows; WRITES NOTHING
|
||||
without --apply, and never touches links.md
|
||||
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** (would be registered, with the normalized id beside the raw URL
|
||||
so a collapse is visible before it happens), and **refused** (normalization
|
||||
raised, with the reason). Under `--apply` it upserts the candidates and prints
|
||||
the same three groups plus what it did. It is not automatic and it is not run by
|
||||
this unit: roughly 14 of 35 candidates are reference bookmarks that belong on the
|
||||
board, and a machine cannot tell a bench from a bookmark by its URL. The
|
||||
operator seeds the registry by reviewing that list.
|
||||
**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
|
||||
|
||||
@@ -282,7 +325,7 @@ operator seeds the registry by reviewing that list.
|
||||
**and 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.
|
||||
2. `booth bench import` proposes; the operator applies.
|
||||
2. `booth bench import` proposes; the operator applies **by naming ids**.
|
||||
3. The 156 dead booth rows are marked, and removed by him or not at all.
|
||||
|
||||
## Scope — the blast-radius pass
|
||||
@@ -330,12 +373,20 @@ that the panel renders would pass under exactly that change.
|
||||
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`. The test asserts
|
||||
AGREEMENT rather than behaviour: a 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) is run through the CLI's refusal AND the render's dead marker, and
|
||||
the two must classify every row identically. A test that only checked the
|
||||
refusal would stay green while the marker drifted.
|
||||
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.
|
||||
@@ -367,8 +418,16 @@ 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 path,
|
||||
query or host creates two.
|
||||
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
|
||||
@@ -378,9 +437,15 @@ 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 (a trailing slash on a non-empty path, an uppercase
|
||||
host, a fragment) and asserts the anchor's `href` is the raw string, byte for
|
||||
byte.
|
||||
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`
|
||||
|
||||
Reference in New Issue
Block a user