Four items owed after S5b, reported by design-dev during the anti-slop run:
- carry() measured a sent-then-changed form against its OLD DEFAULTS. An
answer set back mid-flight to the value the page first showed read as
untouched, and the swap put the just-saved value over it. A form sent and
then changed is now measured against its sent snapshot (sentSet.snapOf).
- The embed's clean-batch reload saw only our own forms. A report's own
inputs lost whatever the operator had typed into them. Unsaved text in
any control we don't own now holds the reload, and the page says so.
- Two r2b.toml rows ("D3 a stored theme...", "D3 forced light...") matched
twice, so they proved only by where the first match fell. Both are
re-anchored, and scripts/mutation_check.py now refuses any anchor that
matches more than once. A new tool control covers that.
- The r2_flow contract's C3 steps 2 and 4 now say what S5b superseded. U3
gains the report-input rule.
Mutation rows: u3_submit_all +1, r2_submit_all +1. Four rows were
re-anchored onto the moved lines.
50 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.app (verbatim serving) + booth/static/embed.js | A booth that ships its own index.html is the operator's most important surface -- his design reviews, his audition reports, his briefs -- and the Booth reaches into it with six regular expressions against arbitrary author HTML plus a placeholder DSL that substitutes rendered markup by pattern. Both work today and both are the single most fragile thing in the service. This unit replaces the whole class with a DECLARED SEAM: the page carries one line (`<script src="/_booth/embed.js" defer></script>`), the Booth mounts its chrome through real DOM APIs, and a page that declares the line is served with ZERO Booth markup added to it. A page that does not declare it gets that one line appended at the end -- the only remaining mutation, and it needs no pattern matching at all. Operator ruling, 2026-09-21: the page declares itself, the Booth mounts into it. |
|
python + javascript | medium | 420 | 0.82 |
|
|
|
|
U3 — the declared embed seam
The defect, stated precisely
A booth that ships its own index.html is served verbatim. That is the whole
promise of the verbatim path, and the Booth breaks it twice on the way out:
wrap_verbatim_htmlsearches arbitrary author HTML with six regular expressions —_ICON_RE,_HEAD_CLOSE_RE,_HTML_OPEN_RE,_DOCTYPE_RE,_BODY_CLOSE_RE,_HTML_CLOSE_RE— to find somewhere to put a favicon and somewhere to put a floating chip, while threading two constraints it cannot verify: never put anything ahead of a leading doctype, and keep the charset meta inside the first 1024 bytes.booth/inline.pymatches a placeholder DSL with four more patterns and substitutes rendered HTML into the author's markup by string replacement.
Ten patterns, applied to documents the Booth did not write, does not parse, and cannot validate. It works. It is also the single most fragile thing in the service, and it is load-bearing for the operator's most important workflow.
The failure this invites is not a crash. It is a report that renders slightly
wrong — and there is a live specimen already. dfa-concepts/index.html writes:
<div class="ask" data-booth-ask="dfa:logo"><h3>The one asset that must survive</h3>
_EL_RE matches the opening <div …> and replaces it. The author's .ask
wrapper class is gone, the <h3> is orphaned, and the </div> further down is
stray. Nobody filed a bug, because a page that is 95% right does not look broken.
The seam
Operator ruling, 2026-09-21. A report carries one line:
<script src="/_booth/embed.js" defer></script>
and the Booth mounts its chrome through real DOM APIs. Three consequences, and the third is the one worth stating out loud:
- A page that declares the line is served with nothing added to it. Not "one small injection" — nothing. The body is what the author wrote.
- A page that does not declare it gets that one line appended at the end. A substring test and a concatenation; no pattern, nothing prepended, no constraint to thread.
- Both of
wrap_verbatim_html's hard constraints stop existing rather than being satisfied more carefully. You cannot displace a doctype you never move, and you cannot push a charset meta out of the detection window by appending.
What crosses the seam
GET /b/{name}/embed.json — server-rendered fragments, and nothing embed.js has
to decide for itself:
{
"booth": "dfa-concepts",
"home": "/",
"favicon": "data:image/svg+xml,…",
"open": ["dfa"],
"marks": [
{
"id": "dfa",
"error": null,
"whole": "<div class=\"bk-ask\" …>",
"submit": "<div class=\"bk-ask\" …>",
"questions": [
{"key": "logo", "html": "<div …>"},
{"key": "display", "html": "<div …>"}
]
}
]
}
Every HTML string comes from the _ask_inline.html macros that render the same
fragments today. open is open_marks(picks) — computed once, server-side, and
never re-derived in JavaScript.
A payload whose .marks.json could not be read has a stated shape, because
an arm asked and the first draft did not say: marks is [], open is [],
booth / home / favicon are present as normal, and top-level error and
detail carry the verdict. Status stays 200, copied from /marks.json — a
pinned status code is a promise to remote clients, and the information goes in
the body instead. The nav mounts; nothing else does. A per-mark error is a
different thing: that is ONE unreadable pick inside a file that read fine.
questions is a LIST, and key may be null. This is not a style choice.
A single-question pick normalizes to questions: [{"key": None, …}]
(asks.normalize_ask), so a JSON object keyed by question key would serialize
that key as the string "null" — inventing a name that collides with a real key
and that JavaScript would have to translate back. A list also carries declaration
order in the format itself rather than leaning on object-key insertion order.
A null key matches no anchor spec, which is correct and is exactly what
place does today: a single-question pick is addressed as a whole or not at all.
Found by the seam review; see SR-2.
How the script learns which booth it is on
The find of the contract-review round, three arms independently, and the one
gap that made the rest unimplementable as first written: the declared line is
byte-identical for every booth, the payload endpoint needs {name}, and the
name arrives inside the response the fetch needs the name to make.
The rule, stated once:
The booth name is the second path segment of the page's own address. A verbatim report is served at
/b/<name>/, soembed.jsreadslocation.pathname, takes segment 2, anddecodeURIComponents it. A page whose address is not/b/<name>/...mounts nothing and returns quietly.Override: a
<script data-booth="...">attribute wins if present. The Booth never writes one — the appended tag is exactly the documented line — but an author embedding a report elsewhere needs a way to say so, and one optional attribute is cheaper than a second endpoint.
This makes the URL grammar part of the public seam, which is the honest reading: an author who writes the line is relying on where the Booth serves them, and that should be written down rather than inferred.
The placement algorithm
The same algorithm inject_asks runs today, expressed against a DOM instead of
a string. It is written out here because it is the part that moves languages,
and a reviewer has to be able to check the two against each other.
placed : Map<markId, Set<key | WHOLE>>
submitted : Set<markId>
1. every element matching
[data-booth-mark], [data-booth-ask] -- in document order
spec -> (id, key?) by splitting on the first ":"
mark unknown -> leave the element ALONE (a typo stays visible)
key absent -> mount whole; placed[id] += WHOLE; submitted += id
key names no question -> leave the element ALONE
key present -> mount question; placed[id] += key
2. every element matching
[data-booth-mark-submit], [data-booth-ask-submit]
mark unknown -> leave alone
otherwise -> mount submit; placed[id] ||= {}; submitted += id
3. tail, appended to <body> in payload order. THE ARROWS ARE EXCLUSIVE, NOT
CUMULATIVE -- first match wins and the mark is done. An arm read them as
four independent tests, under which one unplaced mark would mount its whole
form AND every question AND a submit block; the notation allowed it and the
prose did not forbid it:
if id not in placed: append whole; NEXT MARK
elif mark.error: append nothing; NEXT MARK
else:
if WHOLE not in placed[id]: append every question not in placed[id]
if id not in submitted: append submit (scattered, still submittable)
4. re-associate: for every control carrying form="…", remove and re-set the
attribute, so its form owner is resolved after all fragments are in place.
5. chip: if `open` is non-empty, link it to the FIRST element in document order
whose id is EXACTLY `bk-ask-<open[0]>` or begins `bk-ask-<open[0]>-`.
A bare prefix match would send the chip to `bk-ask-batch2-r1` for the mark
`batch`, or to an author's own element -- flagged by a cold arm, and the
trailing hyphen is what rules it out.
Two more rules the first draft left to the selector rather than stating:
- **An element carrying BOTH `data-booth-mark` and `data-booth-ask` uses the
canonical one.** The alias exists for reports written before the rename, not
to double a mount.
- **A submit anchor's spec is its stem; any `:key` on it is IGNORED.** There is
no per-question submit block — one pick has one `<form>`, which is the whole
reason the `form=` binding exists.
mount is el.insertAdjacentHTML('beforeend', frag) — the anchor element
and its existing contents survive; the fragment lands inside. See the assumption
on anchor-fill for why this is a deliberate change and not an accident.
Step 4 is measured, not assumed. Chromium 151 resolves a control's form owner
correctly even when the control is inserted before its <form>: a probe run
2026-09-22 (N=3 per condition, with a form-first positive control and a
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 the operator fills in whose controls reach no form,
so the button does nothing.
Submitting several asks at once (amended 2026-09-27)
The defect. One pick is one <form> is one POST to /answer, and that
POST 303s back to the page. On a report carrying several picks, a submit sent
exactly ONE of them, and the reload that followed wiped every pick the operator
had made in the others. His report, relayed by infra-ops: "I go through, submit
a question and it only submits the last one and clears out the top ones."
Confirmed against the live auk-audition booth (three single-question picks,
no anchors, so all three in the tail) before any code: the access log shows one
POST at 15:02:23 that saved the LAST pick on the page, the reload, then a 400
four seconds later — the submit of a pick the reload had just blanked — and the
other two re-answered one at a time. The server is not the defect: every
POST did exactly what /answer promises. The page gave him one button per
form and no way to send them together.
The rule. embed.js listens for submit on the forms IT mounted (never an
author's form). A form is dirty when any control it owns — form.elements,
which includes every control bound to it by form= wherever it sits — differs
from its server-rendered default: a radio or checkbox whose checked differs
from defaultChecked, a textarea or text input whose value differs from
defaultValue.
submit on one of our forms F:
a batch is in flight -> preventDefault; nothing else (never the
browser's POST racing the batch)
no OTHER of our forms is dirty -> do nothing; the browser's own POST and 303,
exactly as before this amendment
otherwise -> preventDefault, and send EVERY dirty form of
ours, F included only if F is dirty
send: one POST per form, to that form's own action, carrying that form's own
FormData read AT THE PRESS, with `Accept: application/json` (the
route's 204, r2 C3), one after another, in DOCUMENT ORDER of the <form>
elements. A refused form does not stop the ones after it. A form the
server took (204) gets a new baseline: what it sent. From then on it is
dirty only if it differs from THAT.
then: no refusal AND nothing -> reload the page (a GET), so what shows is
of ours is dirty the server's record
otherwise -> NO reload. The pressed form's submit block
(a refusal, or a change says how many saved and what did not, with
made during the flight) the server's reason, and everything the
operator entered stays on the page.
A refusal blocks the reload ON ITS OWN: a refused form set back to its
first value reads clean, and "nothing dirty" alone reloaded over it.
So does unsaved input in the REPORT'S OWN controls (amended
2026-09-28): any input, textarea or select not owned by one of our
forms that differs from its default. The reload would clear it, and
ourForms cannot see it.
Five consequences, each deliberate:
- A blank pick is skipped, never refused. A pick with nothing entered is not
dirty and is not sent, so pressing its button no longer produces the 400 page
the log shows. Blanks stay legal, as
build_answerhas held since 2026-09-09. - A pick nobody touched is not re-sent — including the one whose button was pressed, and including one this page already saved. Re-sending an answer re-dates it, and a reading session would see a fresh answer that nobody gave. The moved baseline is what keeps a retry after a partial refusal to exactly the picks that did not save; it also means correcting a saved pick back to its first-rendered value counts as a change and is sent.
- One refused pick costs only itself. A pick withdrawn or re-declared while the page was open is refused (404 / 400); the rest are saved regardless. This is the partial-answer ruling's reasoning applied one level up: refusing everything because one was stale throws away the ones that were made.
- The page is reloaded only when nothing would be lost by it. The page stays live while the batch is in flight; a pick made or a note typed in that window is unsaved input, and a reload would clear it — the exact loss this amendment exists to stop. So the reload waits on "every POST succeeded" AND "nothing of ours is dirty" — each on its own. The saved picks' tags stay stale until he reloads, and the message says so. Nothing is ever re-sent without a fresh press; a press after a lost response may re-send (and re-date) a pick that did land, which is the price of never retrying on our own.
- A press during the flight is ignored. Checked before anything else, so a press on a form with no other dirty form beside it cannot fall through to the browser's POST while the batch runs.
What it does not change. /answer, its fields, its 204 and its 303 are
untouched: the server has no batch endpoint and no new request shape. A page
whose only dirty form is the pressed one gets the plain form submission,
byte-identical to before. The status line is server-rendered, empty and
hidden in the submit macro; the script only sets its text, so embed.js
still renders no markup of an ask.
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
fragments already carry ids; document order in a live DOM is directly queryable;
the extra anchor is not needed.
Invariants
Each is falsifiable by a change that a test must catch going red. The
Falsifiable: line names that change — not a test that merely mentions 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 BYTE FOR BYTE.
The response body for a verbatim booth whose index.html contains
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 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
over-cap page is mutated ZERO times and loses its chrome, exactly as it did
before this unit.
Falsifiable: insert the tag before </head> instead of appending, or add the
favicon link back, and test_undeclared_page_gains_only_the_tag fails the
prefix assertion. The exception has its own test,
test_an_oversize_verbatim_page_is_served_raw, which fails if the append starts
firing above the cap.
INV-3 — No regular expression is applied to author HTML.
The verbatim branch of booth_view performs two in tests and one +.
⚠ The first draft of this falsifier was VACUOUS and three arms caught it.
It name-matched the six deleted patterns, so reintroducing the same regex under
a new name — _TAIL_RE, applied in the verbatim branch — left the test green,
on this contract's central promise. Worse, this repo's own vacuity pass missed
it, because the mutation it tried was the named one: a vacuity pass is only as
good as the mutation it picks, and picking the one the contract names is how it
agrees with itself.
Falsifiable: test_no_regex_touches_author_html walks the AST of
booth/app.py and asserts the module performs exactly one regex operation
— ask_form_id's re.sub over a mark id, which is not a page — plus that
booth/inline.py does not exist. Any regex anywhere in the module, under any
name, fails it. Verified by mutation: a renamed _TAIL_RE.sub in
embed_verbatim goes red, and the unmutated control stays green.
INV-4 — The payload is the only source of what a mark says.
embed.js never decides openness, order, or content. open comes from
open_marks; marks order is marks_for order; questions order is
declaration order.
Falsifiable: the claim ranges over three things and so does the check.
Openness: have embed.js derive open marks from a bk-done class and
test_the_chip_count_comes_from_the_server fails on a half-answered pick, which
open_marks calls open and the rendered state does not. Order: reverse the
tail iteration and test_the_tail_follows_payload_order fails. Content: the
fragments are strings the page never authors, which
test_every_piece_the_author_can_place_is_offered pins on the server side.
INV-5 — /_booth/embed.js is read once at startup.
Falsifiable: change the route to read_text() per request and
test_embed_js_does_not_hot_reload_from_disk fails — it mutates the file on
disk after the app is built and asserts the served body is unchanged.
INV-6 — Every ordered collection this unit renders has a stated rule.
Anchors are visited in document order (querySelectorAll). The tail is
appended in payload order, which is (created, id) — the rule marks_for
and hold_read both sort by, stated here as the rule rather than as one
function's name. Questions
within a mark are in declaration order. The chip targets the first element
in document order whose id starts with the open mark's prefix.
Falsifiable: sort the tail by anything else — id, key, insertion — and
test_tail_order_is_payload_order fails against a fixture whose creation order
and id order disagree.
INV-7 — Every question of every READABLE pick reaches the document, on a
page that runs the script. Either placed at an anchor or appended, and every
pick with a placed question has a submit block.
⚠ Two qualifiers, both added because arms read the first wording literally and
were right. Readable: a pick carrying error has no questions to place —
marks._hydrate gives it an empty list — so the tail mounts its broken-ask box
and stops, and an unqualified "every pick" would have demanded placement the
algorithm forbids in exactly the damaged-data case the leniency posture exists
for. Reaches the document, not "is visible": the Booth cannot police an author
who hides their own anchor, and a guarantee that claimed to would be unenforceable
rather than strict.
Falsifiable: drop the "append the questions the author did not place" branch
and test_partially_marked_page_still_shows_every_question fails in the browser
with 2 of 4 radio groups present.
INV-8 — One submit saves every pick on the page the operator changed
(amended 2026-09-27). Per "Submitting several asks at once": every dirty form
of ours is sent, in document order; a clean one never is, nor a saved one
again; a refused one stops nothing; the page reloads only when nothing was
refused and nothing of ours is left unsaved; a press during the flight is ignored; and with no other dirty
form the submit is the browser's own.
Falsifiable, one change per clause, each in tests/mutations/u3_submit_all.toml:
send only the pressed form and
test_one_submit_saves_every_answered_ask_on_the_page fails; send the pressed
form whether or not it is dirty and test_a_blank_ask_is_skipped_never_refused
fails; send every form regardless and test_an_ask_nobody_touched_is_not_re_sent
fails; intercept a lone dirty form and test_one_changed_ask_still_submits_as_a_plain_form
fails; send in reverse and test_the_asks_are_sent_in_document_order fails;
stop at the first refusal, reload on one, or never show the status line, and
test_a_refused_ask_costs_only_itself_and_clears_nothing fails; listen to every
form on the page, or trust our id prefix without the mounted-root check, and
test_an_authors_own_form_is_never_taken_over fails; let a press in flight fall
through and test_a_press_inside_the_flight_never_fires_a_native_post fails;
reload when every POST succeeded regardless of what changed meanwhile and
test_input_made_during_the_flight_is_kept_and_saved_on_the_next_press fails;
leave a saved form's baseline where it was and
test_a_retry_after_a_refusal_sends_only_what_did_not_save fails; drop the
class-level [hidden] rule and test_the_empty_status_line_stays_hidden_under_host_css
fails; let "nothing dirty" alone decide the reload and
test_a_refusal_blocks_the_reload_even_when_nothing_reads_dirty fails; ignore
the report's own inputs and
test_a_clean_batch_does_not_reload_over_text_typed_into_the_report fails.
Out of scope (deferred or never)
Named so a reviewer does not read them as drift.
- The gallery page's chrome. Only a booth's own
index.htmlis served verbatim; every other surface is server-rendered end to end and has no seam problem./_booth/embed.jsis not loaded there and is not meant to be. - Re-rendering an ask in JavaScript. The payload carries server-rendered HTML and embed.js places it. A JS renderer would be a second renderer of one truth — the bug the repo's one-resolver rule exists to stop.
- A no-JavaScript fallback on the verbatim path. The operator's 2026-09-21
ruling accepts the script dependency. The never-invisible guarantee degrades
to surfaces that need no script (the index card's badge,
/b/<name>/marks), and that is the stated cost, not an oversight to be fixed here. - The HTML-comment placeholders
<!-- booth:ask … -->. Zero users across all 21 live booths; dropped rather than ported. A page that used one falls back to the append path, so its ask still renders. _ask_inline.html's deadstandalone=Falsemacro parameter. No caller has passedTruesince U2 turned the standalone asks page into a 308. Deleting it is tidy-up and changes a macro signature for no behavioural gain.base.html's literal duplicate of the favicon data URI. It predates this unit. The payload readsFAVICON_HREF, so this unit adds no third copy; it does not remove the second.WRAP_MAX_BYTESand its raw-serve fallback. Unchanged at 8 MiB. A booth over the cap loses its chrome exactly as it did before — no regression, and the constant keeps its existing test.GET /b/<name>/asks. Already a 308 into/markssince U2. Left alone: the URL is in the operator's history and in landed reports.- Pushing, and the version bump tier. Minor needs the operator's approval.
Slices
| # | slice | red→green on |
|---|---|---|
| 1 | GET /b/{name}/embed.json — payload shape, order, leniency, no view recorded |
payload tests; existing 410 stay green |
| 2 | GET /_booth/embed.js — served from a startup read, ETag, no hot reload |
INV-5 |
| 3 | the verbatim branch rewritten; inject_asks and wrap_verbatim_html deleted |
INV-1, INV-2, INV-3 |
| 4 | booth/static/embed.js — nav, favicon, styles, no marks yet |
browser: chip present, icon set, declaring page untouched |
| 5 | placement: anchors, tail, submit, re-association | browser: INV-4, INV-6, INV-7; the live dfa-concepts and sindra-voice-1 shapes as fixtures |
| 6 | delete inline.py; retire the six tests that test the deleted mechanism; docs |
suite green, IA doc and ROADMAP updated |
Seam review
The sibling-aware pass, run in-session against the real module surfaces rather
than against the sibling contracts' prose. /heid-contract-review is
artifact-only by design and structurally cannot see booth/marks.py, so this is
the only gate that can check what the contract borrows from it.
| # | finding | disposition |
|---|---|---|
| SR-1 | The order invariant named marks_for's ordering. The route actually reads through hold_read — one read answering both "what is here" and "can it be read", per the TOCTOU lesson — and only falls back to marks_for on the error path. Both sort (created, id), so the contract was not wrong, but it named a function where it meant a rule. |
Amended. INV-6 states the rule. The route's reader is named in the payload section. |
| SR-2 | The payload shape was wrong. questions as a JSON object keyed by question key breaks on a single-question pick, whose only question has key: None (asks.normalize_ask, the multi: False branch) — json.dumps writes that key as the string "null". Every one-question ask in the fleet hits it, including the live sindra-voice-1. |
Scope fix. questions is a list of {key, html}; key is nullable; declaration order is carried by the format. booth.asks.normalize_ask added to depends_on. |
| SR-3 | inline.form_id was inside the module the contract deletes entirely, but it is not placement machinery — it builds the shared <form> id the question fragments bind to with form=. Deleting the module as written would delete the fragments' ability to submit. |
Scope miss. form_id moves to booth/app.py; touches says so. |
| SR-4 | A FLAG mark's id is literally flag:<target> (marks.flag_id) — it contains the separator the anchor spec splits on. It never reaches the payload only because the payload filters shape == "pick", and pick ids are valid_stem-checked (no colon). |
No change, stated. The filter is load-bearing, not incidental; a later widening of the payload to all shapes would break the split rule silently. |
| SR-5 | _ask_inline.html's question(a, q, form_id, name_url, standalone=False) has had no caller passing standalone=True since the standalone asks page became a 308 in U2. Dead parameter on a macro this unit edits. |
Out of scope, noted. Deleting it is tidy-up, not this unit's work, and it changes a macro signature for no behavioural gain. |
Contract review — the cold panel
/heid-contract-review, four arms, dispatched 01M351WKV666D681SSRNY7D7X6.
Triaged per the cross-frontier discipline: adopted on merits, not on authority.
| # | finding | arms | disposition |
|---|---|---|---|
| CR-1 | The seam never tells embed.js which booth it is on. The declared line is byte-identical for every booth, the payload endpoint needs {name}, and the name arrives inside the response the fetch needs it to make. Every other section depends on this unstated hop. |
3 of 4, independently | Genuine add, and the round's headline. The code already derived it from location.pathname; the CONTRACT did not say so, which makes a "public API" whose discovery mechanism is unspecified not fully one. New section: How the script learns which booth it is on. No code change. |
| CR-2 | INV-3's falsifier was vacuous — it name-matched the six deleted patterns, so a renamed regex applied to the page body kept it green, on this contract's central promise. | 3 of 4 | Genuine add, and a CODE-side fix. The test now asserts booth/app.py performs exactly one regex operation anywhere in the module. Verified by mutation in both directions. The lesson is sharper than the fix: this repo's own vacuity pass missed it because it tried the mutation the contract named — a pass that picks the named mutation agrees with itself. |
| CR-3 | Declaration by bare substring fails in the dangerous direction. A report that merely mentions /_booth/embed.js — a code sample, a comment — counted as declaring it and was served with no chrome at all, silently. |
3 of 4 | Genuine add, CODE-side. Detection now requires src="…" (either quote style), which fails toward a harmless duplicate tag instead. New test covers prose, comment and ?v=2 spellings. |
| CR-4 | INV-2 and the size cap prescribe different responses for an over-cap non-declaring page, and neither the invariant's wording nor a named falsifier carved the exception. | 2 of 4 | Genuine add. INV-2 now states the cap as an explicit exception and names the test that holds it. Code and test were already right. |
| CR-5 | The tail's four arrows read as independent tests, under which one unplaced mark mounts its whole form AND every question AND a submit block. | 1 | Genuine add. The notation allowed it and the prose did not forbid it. The block is now explicit if/elif/else. Code was already exclusive. |
| CR-6 | INV-7 quantified over picks the algorithm filters (errored picks) and over "visible", which placement cannot guarantee. | 2 of 4 | Genuine add, wording. INV-7 is now scoped to READABLE picks and claims reaches the document, not is visible. |
| CR-7 | The chip's prefix rule can select bk-ask-batch2-r1 for mark batch, or an author's own element. |
1 | Sharpening. The code always matched exactly-or-hyphen; the contract said "starts with". Wording fixed, and test_the_chip_does_not_jump_to_a_mark_that_merely_shares_a_prefix now holds it. |
| CR-8 | Precedence undefined when one element carries both attribute spellings; submit-anchor key handling unstated. | 1 | Sharpening. Both stated; test_the_canonical_attribute_wins_when_both_are_present added. |
| CR-9 | The damaged-.marks.json payload shape was never stated — per-mark error was the only error shown. |
1 | Genuine add, wording. Stated in What crosses the seam. Test already existed. |
| CR-10 | "INV-1" names two different obligations — this contract's untouched-page rule, and the repo's one-resolver rule the assumptions cite. | 1 | Genuine add, wording. The assumptions now name CLAUDE.md invariant 3 explicitly. A real collision: the local falsifier goes red on an added newline and stays green if embed.js becomes a second renderer. |
| CR-11 | INV-4's falsifier covered openness while the invariant claimed openness, order AND content. | 1 | Sharpening. The falsifier now names a test per clause. |
| CR-12 | html.questions keyed by question name vs the top-level questions list — which is authoritative? And INV-4 naming marks_for's order while INV-6 fixed (created, id). |
2 | Settled before the reply landed. The in-session seam review collapsed both (SR-1, SR-2) while the panel was in flight. Independent convergence on the same two spots — worth recording, not re-fixing. |
One arm's finding not adopted, and the reason: that a question mounted into an author-hidden anchor is still invisible. True, and out of reach — the Booth cannot police an author hiding their own markup. Answered by narrowing INV-7's claim rather than by chasing actual visibility (CR-6).
Methodology note the panel raised on its own, relayed by heid: 5 of 8 arms 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. CLOSED 2026-09-22, after U6, at the hydration boundary rather than by a third copy of this guard — so _safe_fragments no longer has a reachable natural trigger and is now a pure backstop, falsified synthetically. Hardening the falsifier found that this guard's own fallback re-rendered through the macro module that had just raised, so it re-raised whenever whole was the broken thing; fixed in the same pass. |
| 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:
- 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.
- Re-run against that fix, it found the fix's own hole (an aliased
import re as _r). - 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.