fix(u3): seven defects two cold panels found in the declared seam

The /heid-code-review and /heid-bug-hunt panels, artifact-only over the U3
diff, between them found four real defects and three vacuous falsifiers. Both
snapshots predate the contract-review fixes, so two of their findings were
already closed; the rest are here.

Prototype pollution in the placement maps. A mark id and a question key are
both [A-Za-z0-9][A-Za-z0-9._-]*, so `toString` and `constructor` are legal in
each. Against a plain `{}` an anchor naming NO mark returned an inherited
function, passed the guard meant to reject it, and threw on .questions.length
-- aborting placement before the tail, so one typo in author markup cost the
page every ask. The `placed` set had the mirror bug: inherited
`got.constructor` read as already-placed and silently dropped a question.
Object.create(null), three times. Found independently by both panels.

A declaring page was not served as written. read_text() opens in
universal-newline mode, so a CRLF report came back LF, and errors="replace"
replaced every byte that was not valid UTF-8. That is this unit's headline
promise, broken by the read itself, and the test could not see it because its
fixture was LF-only ASCII. The verbatim branch reads and serves bytes now; the
decoded copy answers only "does it declare the seam?".

A submit anchor inside the author's own <form> lost ours -- the parser drops a
nested form element outright -- while the code still recorded the pick as
submitted, so no fallback was appended. Every control's form= pointed at
nothing and the button did nothing. It counts as submitted only if the form
survived.

A broken pick's diagnostic never rendered from a submit-only anchor: an errored
pick's submit block is empty, and mounting that then marking it placed made the
tail skip the "broken ask" box entirely. The anchor is left alone instead.

An author's own element could hijack the open-ask chip -- id="bk-ask-winner-
background" satisfies any prefix rule, hyphen boundary included. The chip now
searches only elements this script mounted, which is the identity the deleted
bk-ask-<id>-top anchor used to guarantee, and takes the earliest by
compareDocumentPosition.

No error boundary around fragment rendering. A .marks.json that is well-formed
JSON with a wrong-shaped answer hydrates with no error and then raises in the
macro; this endpoint renders every pick on every load of the report, so that
was the whole seam gone while hold_read called the file readable. Reproduced
before building for it. _safe_fragments gives it the per-mark leniency
_hydrate_safe already applies one layer down.

The gallery and marks pages still 500 on that same entry. Measured at 42ea67f
-- it predates this unit, they render the same macro with no guard, and the
gallery is named out of scope in the contract. Recorded, not quietly widened:
persistent-memory.d/2026-09-22-a-wrong-shaped-answer-500s-the-gallery.md

Also corrected: several comments claimed a multi-question pick POSTs a 400
unless every question is answered. It does not -- an empty submission is
refused, a partial one is recorded on purpose. The real reason an unplaced
question must still be appended is that a question which never reaches the page
cannot be answered at all.

Vacuity pass rebuilt around the rule this session learned: the mutation comes
from the invariant's claim, never from the falsifier's example. 21 mutations,
21 caught, unmutated control green. Getting there took three rounds -- it
passed INV-3 with the contract's own mutation, then found its own fix's hole,
then flagged seven stale mutations and one genuinely vacuous fixture whose
sibling-mark arrangement made the right answer also the first answer.

444 tests. Deployed and verified: 23/23 booths 200, and all four live verbatim
reports served at exactly +46 bytes -- len(EMBED_SCRIPT_TAG) -- with the
authors' own wrappers and headings intact and no console errors.
This commit is contained in:
vh
2026-09-22 11:22:37 -07:00
parent 87e2c5364c
commit 5c20e2f4d5
7 changed files with 609 additions and 82 deletions
@@ -232,7 +232,8 @@ points-at-nothing negative control) returned `F, F, F` for control-first and
`null, null, null` for the negative. So the pass is *not* needed in Chromium.
It is three lines, it costs nothing, and the sensitivity floor of that probe is
**one engine** — the operator's own browser was not measured. The failure it
guards against is a form that looks filled in and POSTs a 400.
guards against is a form the operator fills in whose controls reach no form,
so the button does nothing.
**Step 5 deletes an element.** Today `inject_asks` injects `<a id="bk-ask-<id>-top">`
before the first fragment of each pick so the chip has somewhere to jump. The
@@ -246,18 +247,25 @@ Each is falsifiable by a change that a test must catch going red. The
invariant. (Five of seven U4 falsifiers were vacuous; see
`persistent-memory.d/2026-09-22-vacuous-falsifiers.md`.)
**INV-1 — A page that declares the seam is served with no Booth markup added.**
**INV-1 — A page that declares the seam is served BYTE FOR BYTE.**
The response body for a verbatim booth whose `index.html` contains
`src="/_booth/embed.js"` (either quote style) is exactly the text read from that
file. A page that only mentions the path is NOT declaring it — see the
`src="/_booth/embed.js"` (either quote style) is exactly the bytes on disk.
⚠ **Bytes, not text, and that is a correction.** The first implementation read
with `read_text()`, which opens in universal-newline mode: a CRLF report came
back LF, and `errors="replace"` turned any non-UTF-8 byte into U+FFFD. A
declaring page was NOT served as its author wrote it — the headline promise —
and the test could not see it, because its fixture was LF-only ASCII. The file
is decoded only to ask whether it declares the seam; what goes on the wire is
the original bytes. A page that only mentions the path is NOT declaring it — see the
conditional-append assumption for which way that has to fail.
*Falsifiable:* append anything — a chip, a comment, a newline — to the declaring
branch's response and `test_declaring_page_is_served_untouched` fails on a
whole-body equality, not on a substring absence.
**INV-2 — A page that does not declare the seam, AND IS UNDER `WRAP_MAX_BYTES`,
is mutated exactly once, at the end.** The response is the source text plus
`EMBED_SCRIPT_TAG` and nothing else, with the source text a prefix of it.
is mutated exactly once, at the end.** The response is the source BYTES plus
`EMBED_SCRIPT_TAG`'s bytes and nothing else, with the source a byte-exact
prefix of it.
⚠ **The size cap is an explicit exception, not an oversight** — two cold arms
read the invariant's universal wording against the raw-`FileResponse`
assumption and found them prescribing different responses for the same page. An
@@ -414,3 +422,50 @@ across two unrelated callers the same evening independently proposed promoting
the end-to-end seam-walk from a conditional deliverable to a mandatory one.
CR-1 is a direct product of that exercise. Recorded here as evidence; the skill
change is the operator's call, not this repo's.
## Bug hunt — the cold panel
`/heid-bug-hunt`, four arms, artifact-only over the merge-base diff, dispatched
`01M352TPCSN52G6NGJ07T5WSGY`. ⚠ **The snapshot predates the contract-review
fixes**, so two of its findings were already closed when the reply landed; the
arms flagged the staleness themselves.
| # | finding | arms | disposition |
|---|---|---|---|
| **BH-1** | **A declaring page was NOT served as written.** `read_text()` opens in universal-newline mode, so a CRLF report came back LF, and `errors="replace"` replaced any non-UTF-8 byte. The headline promise, broken by the read itself — and invisible to a test whose fixture is LF-only ASCII. | 1 | **Genuine add, and the best finding of the round.** The verbatim branch reads and serves BYTES; the decoded copy answers only "does it declare?". INV-1 and INV-2 now state the byte-level promise, with a CRLF-plus-invalid-byte fixture. |
| **BH-2** | **A submit anchor inside the author's own `<form>` loses ours** — the HTML parser drops a nested form outright. Every control's `form=` then points at nothing, and the code recorded the pick as submitted so the tail added no fallback. The operator fills it in and the button does nothing. | 1 | **Genuine add.** A submit anchor counts as submitted only if the form actually survived (`hasForm`); otherwise the tail supplies one at body level, where no form encloses it. |
| **BH-3** | **A broken pick's diagnostic never rendered from a submit-only anchor.** An errored pick's `submit` is empty; mounting that and marking it placed made the tail skip it, so the "broken ask" box vanished from the one surface built to show it. | 3 of 4 | **Genuine add.** A submit anchor for an errored pick is left alone, exactly as an anchor naming no mark is, and the tail mounts the diagnostic. |
| **BH-4** | **An author's own element can hijack the chip.** `<section id="bk-ask-winner-background">` satisfies any id-prefix rule — the hyphen boundary from CR-7 included. | 4 of 4 | **Genuine add, and it supersedes CR-7's fix.** The chip now searches only the elements THIS SCRIPT MOUNTED, which is the identity the deleted `bk-ask-<id>-top` anchor used to guarantee, and takes the earliest of those by `compareDocumentPosition`. |
| **BH-5** | **No error boundary around fragment rendering.** A `.marks.json` that is well-formed JSON with a wrong-shaped `answer` hydrates with no error and then raises in the macro. | 1, `needs-repro` | **Genuine add — reproduced before building for it.** `_safe_fragments` returns a per-mark error record, the same leniency `_hydrate_safe` applies one layer down. ⚠ **The gallery and marks pages still 500 on it, and that is PRE-EXISTING** — measured at `42ea67f`. Out of scope here and recorded rather than quietly widened: `persistent-memory.d/2026-09-22-a-wrong-shaped-answer-500s-the-gallery.md`. |
| **BH-6** | Prototype pollution in the placement maps (`toString` as a mark id, `constructor` as a question key). | 1 | **Already fixed this round** as CR-13, from the code-review panel. Two panels, two lenses, the same defect independently — the strongest signal of the evening that the lenses are not redundant. |
| **BH-7** | Bare-substring declaration suppresses the chrome. | 4 of 4 | **Already fixed** as CR-3, before the reply landed. |
**One correction the panel made to this repo's own prose, adopted:** several
comments claimed a multi-question pick POSTs a 400 unless every question is
answered. It does not — `test_empty_submission_is_refused_with_400` refuses a
WHOLLY EMPTY submission, and a partial answer is accepted and recorded on
purpose. The real reason an unplaced question must still be appended is simpler
and was being obscured: **a question that never reaches the page cannot be
answered at all.** Fixed in `embed.js`, the browser tests and this contract.
**Not adopted:** the bundle's framing called the service Flask. It is FastAPI;
the arm noticed and declined to reason from it, which is the right handling.
## Vacuity pass — final
21 mutations, each drawn from an invariant's CLAIM rather than its falsifier's
example, each run against its named test, plus an unmutated control run.
**21/21 caught, control green.**
The pass earned its place three times over and none of them was the first run:
1. It reported **7/7** before the contract panel, which then showed INV-3 was
vacuous — because the mutation applied was the one the contract named.
2. Re-run **against that fix**, it found the fix's own hole (an aliased
`import re as _r`).
3. Re-run after the bug-hunt fixes, it reported seven **MUTATION-MISS** rows —
its loud-failure mode, firing correctly because the fixes had moved the code
out from under stale mutations — and then one genuine **VACUOUS**: the
sibling-mark chip test had its fixture arranged so the right answer was also
the first answer. Rewritten so the sibling comes first, which is the only
arrangement that can tell the two implementations apart.