Operator report (via infra-ops): on a page with several asks, a submit
saved only the pressed one and the reload wiped the rest. Confirmed on
auk-audition: one POST at 15:02:23 saved the last ask on the page, then a
400 from the submit of an ask the reload had just blanked.
Client-side on both surfaces; /answer is unchanged. A submit on a pick
form, while another pick form on the page holds unsent input, sends every
changed ("dirty") pick form: one POST each, to its own action, with
Accept: application/json, in document order. A refusal stops nothing, and
untouched forms are never re-sent. With no other dirty form, a submit is
exactly what it was.
- embed.js (verbatim reports): reloads only when nothing was refused and
nothing of ours is dirty. Otherwise a server-rendered status line in the
submit block says what did not save, and input stays. A form the server
took gets a new baseline. A press during the flight is ignored.
- base.html (marks page, lightbox, review rail): one refresh in place. A
batch never reloads. Only forms the server took count as sent. In-flight
state and "just sent" are keyed by form identity (formKey) plus the fields
at the press, not the DOM node.
Two heid bug-hunt rounds: a four-arm panel on the first cut, then Hulda
alone on the fold. Ten findings reproduced red in a browser before their
fixes. Contracts: U3 "Submitting several asks at once" + INV-8, R2 C3
steps 2, 3 and 3a. Mutation tables u3_submit_all (15) and r2_submit_all
(11), all proved. Suite 928 -> 951.
578 lines
49 KiB
Markdown
578 lines
49 KiB
Markdown
---
|
|
contract_version: "1.0"
|
|
module: "booth.app (verbatim serving) + booth/static/embed.js"
|
|
purpose: "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."
|
|
depends_on:
|
|
- "booth.marks (marks_for, open_marks, hold_read -- the pick records the payload renders. UNCHANGED by this unit: U3 changes how fragments REACH the page, never what a mark is. Read against booth/marks.py, not against the U2 contract's prose -- see the seam review.)"
|
|
- "booth/templates/_ask_inline.html (the `whole` / `question` / `submit` macros stay the ONE renderer of an ask fragment, called from the embed payload instead of from inject_asks. Its `styles()` macro is DELETED -- inject_asks was its only caller and the CSS moves into embed.js so the chrome is one asset. Macro signatures are otherwise untouched.)"
|
|
- "booth.asks.normalize_ask (TRANSITIVE, through `marks._hydrate`, and named because the payload shape depends on it: a MULTI ask normalizes to questions whose `key` matches `^[A-Za-z0-9][A-Za-z0-9._-]{0,60}$`, and a SINGLE-question ask normalizes to exactly one question whose `key` is `None`. Both facts are load-bearing -- the first makes splitting an anchor spec on the first colon unambiguous, the second is why `questions` is a list. Read against booth/asks.py:138-223.)"
|
|
- "booth.app.FAVICON_HREF (the data-URI icon, carried in the payload rather than copied into embed.js -- a third copy of that string is exactly the multiple-readers-of-one-truth shape the repo's ONE-RESOLVER rule (CLAUDE.md invariant 3 -- NOT this contract's INV-1, which is the untouched-page rule; a cold arm read the two as one label and was right to) exists to stop. base.html's literal copy predates this unit and is out of scope.)"
|
|
language: "python + javascript"
|
|
complexity: "medium"
|
|
estimated_loc: 420
|
|
confidence: 0.82
|
|
used_by:
|
|
- "booth.app.booth_view (the verbatim branch: one read, one substring check, one conditional append -- replacing inject_asks + wrap_verbatim_html entirely)"
|
|
- "the operator's verbatim reports (4 of 21 live booths ship their own index.html; 2 of those 4 use the placement DSL, so the migration is not hypothetical)"
|
|
- "report authors (the declared line is the new public API for a booth that wants Booth chrome where it chooses)"
|
|
touches:
|
|
- "booth/static/embed.js (NEW -- the mount script and the chrome CSS, one asset. Read ONCE at app startup, never per request; see INV-5.)"
|
|
- "booth/app.py (DELETE wrap_verbatim_html, _insert_before, _insert_after, _ICON_RE, _HEAD_CLOSE_RE, _HTML_OPEN_RE, _DOCTYPE_RE, _BODY_CLOSE_RE, _HTML_CLOSE_RE, _BACK_CHIP, asks_chip, inject_asks, FAVICON_LINK and the `from booth.inline import` block. ADD EMBED_SRC/EMBED_SCRIPT_TAG, the startup read of embed.js, GET /_booth/embed.js, GET /b/{name}/embed.json, and the rewritten verbatim branch of booth_view.)"
|
|
- "booth/inline.py (DELETED ENTIRELY -- 119 lines. Nothing else imports it; `scripts/booth` never did, so the stdlib-only CLI surface is untouched. ONE line survives the module: `form_id`, which builds the shared form element id the fragments bind to, moves into booth/app.py beside the route that renders them. It is not placement machinery and dying with the placement engine would take the fragments with it. Seam review, SR-3.)"
|
|
- "booth/templates/_ask_inline.html (DELETE the `styles()` macro and rewrite the header comment: the fragments are now mounted by embed.js, not substituted by regex, and `No JavaScript` stops being true.)"
|
|
- "tests/test_booth.py (DELETE the five test_wrap_* tests and test_verbatim_booth_wrapped_with_back_chip -- they test a mechanism this unit removes; the FAVICON_LINK import goes with them)"
|
|
- "tests/test_asks.py (the inline-placement block, ~L480-590: assertions that server-rendered fragments appear in the page body become assertions about the embed payload. The BEHAVIOUR they encode -- every question reachable, a scattered form still submittable, a typo'd id left alone -- is preserved and re-asserted, half in Python and half in the browser.)"
|
|
- "tests/test_embed.py (NEW -- the payload, the injection rule, the no-hot-reload guard)"
|
|
- "tests/test_embed_browser.py (NEW -- Playwright against a real Chromium: the placement algorithm and the form association, neither of which the Python suite can see. SKIPS, never fails, when playwright or the shared browser is unavailable.)"
|
|
- "pyproject.toml (test extra gains `playwright>=1.60,<1.63` -- the range is the set of releases whose pinned Chromium revision is present in the box-wide /opt/ms-playwright store. Stated explicitly because it is invisible otherwise: 1.63 wants chromium-1243, which is NOT there, and the failure is an opaque `Executable doesn't exist`.)"
|
|
- "docs/design/information-architecture.md (the `What this deletes` list becomes what this DID delete; the standalone /asks bullet is corrected -- U2 already reduced it to a 308)"
|
|
- "ROADMAP.md (U3 row struck through; the ordering table gains the two orders this unit states)"
|
|
assumptions:
|
|
- "THE OPERATOR ALREADY RULED ON THE SEAM (2026-09-21, recorded in the IA doc): the page declares itself and the Booth mounts into it, via `<script src=\"/_booth/embed.js\" defer></script>`. That ruling ACCEPTS a JavaScript dependency on the verbatim path, which today has none. This contract does not re-open it. What the contract DOES do is state the consequence plainly so it is not discovered later -- see the degradation assumption below."
|
|
- "THE ONLY REMAINING SERVER-SIDE MUTATION IS A CONDITIONAL APPEND, AND IT NEEDS NO PATTERN AT ALL. Two substring tests (`src=\"/_booth/embed.js\"` and its single-quoted twin), then a concatenation. ⚠ THE BARE PATH WAS THE FIRST DRAFT AND IT FAILED IN THE DANGEROUS DIRECTION: a report that merely MENTIONS the path -- a code sample, a comment, a sentence about this feature, which the Booth's own design reports are the likeliest pages to contain -- would have counted as declaring it, been served untouched, and shown no chrome at all, silently. Requiring `src=` immediately before the path flips the failure direction: an unusual spelling (`src = \"...\"`, an unquoted attribute, a `?v=2` suffix) reads as NOT declared, so a second tag is appended and embed.js mounts once anyway on its `window.__boothEmbed` guard. A missed declaration costs a duplicate tag; a false one costs the operator his chrome. Three of four cold-panel arms found this independently. This is why all six regexes die rather than collapsing to one: content appended AFTER `</html>` is parsed into the body by every browser, so there is nothing to find. Nothing is ever PREPENDED, which is what retires both of wrap_verbatim_html's hard constraints in one stroke -- no doctype can be displaced into quirks mode and no charset meta can be pushed out of the first 1024 bytes, because nothing moves."
|
|
- "THE FAVICON MOVES FROM A REGEX TO A DOM QUERY. `_ICON_RE` existed to answer `does this page already declare an icon`, against raw text, and three more regexes existed to find a head-ish seam to put one in. embed.js asks `document.querySelector('link[rel~=\"icon\"]')` and appends to `document.head`. That is the same question and the same action, asked of a parsed document instead of a string -- and it is four of the six regexes."
|
|
- "FRAGMENTS ARE STILL RENDERED BY JINJA, ONLY PLACED BY JAVASCRIPT. The payload carries server-rendered HTML from the EXISTING `_ask_inline.html` macros. Re-implementing the ask form in JavaScript would make two renderers of one truth, which is precisely the shape the repo's ONE-RESOLVER rule (CLAUDE.md invariant 3) was written to stop after the zoom view lost its captions. embed.js does DOM placement and nothing else: it never decides what a mark says, whether it is open, or what order marks come in."
|
|
- "PLACEMENT IS AN ANCHOR-FILL, NOT A REPLACEMENT, AND THAT IS A DELIBERATE CHANGE FROM TODAY. `_EL_RE` matches an author's opening tag and SUBSTITUTES it, so `<div class=\"ask\" data-booth-ask=\"dfa:logo\"><h3>The one asset that must survive</h3>` loses both the wrapper's class and -- visually -- its framing, leaving the author's heading orphaned and the closing `</div>` stray. That is live today on `dfa-concepts`. embed.js uses `el.insertAdjacentHTML('beforeend', frag)`: the author's element and its contents survive and the fragment lands inside, under the heading. Strictly closer to what the markup says, and it is the behaviour a DOM API gives for free."
|
|
- "`data-booth-mark` IS CANONICAL; `data-booth-ask` IS A KEPT ALIAS. U2 made an ask one shape of mark and the IA doc names the anchor `data-booth-mark`. But 2 of the 4 live verbatim booths use the `data-booth-ask` spelling, in the operator's own reports, so the selector accepts both -- one extra clause in one selector string. Same for `data-booth-mark-submit` / `data-booth-ask-submit`. Renaming without the alias would break a live report to save nothing."
|
|
- "THE HTML-COMMENT PLACEHOLDERS ARE DROPPED, NOT PORTED. `<!-- booth:ask stem -->` and `<!-- booth:ask-submit stem -->` have ZERO users across all 21 live booths. Walking comment nodes to keep them would be real complexity bought for nobody, in the unit whose entire point is deletion. A page that used one degrades to the append path -- the ask still renders, at the end -- so the never-invisible guarantee holds even for a caller we do not know about."
|
|
- "DEGRADATION WITH JAVASCRIPT OFF IS A REAL LOSS AND IT IS NAMED HERE. Today the verbatim path is zero-JS: an ask renders server-side and submits through a plain form. After this unit, no JS means no chrome on the report -- no ask, no way home, no icon. The guarantee that an ask is NEVER INVISIBLE survives in a weaker and still-true form, through surfaces that need no script: the index card carries the open-mark badge, and `/b/<name>/marks` renders every mark server-side. This is the cost of the operator's ruling, stated once so nobody meets it as a surprise."
|
|
- "EMBED.JS IS READ ONCE AT STARTUP, FOR THE REASON TEMPLATES ARE. Serving it from disk per request would give the service a third staleness rule, and a live asset editable under a running process is exactly what put 19 of 25 booths at 500 on 2026-09-21. One rule in this repo: nothing takes effect until you restart. INV-5 holds the line the same way `test_templates_do_not_hot_reload_from_disk` does."
|
|
- "THE PAYLOAD ENDPOINT DOES NOT RECORD A VIEW. `booth_view` already calls `record_view` above both early returns (U4), and `.viewed` is a deliberate look. A fetch issued by a script on a page that has ALREADY been recorded would double-count activity and reset the TTL on machinery rather than on the operator -- the same distinction the `.lock` exemption draws in `_newest_mtime`."
|
|
- "THE READ IS LENIENT AND THE STATUS STAYS 200, copied deliberately from `/b/{name}/marks.json`. A damaged `.marks.json` must not 500 the operator's report; it returns an `error` in the body and embed.js mounts the nav anyway. This is the v0.2.2 lesson and the posture every read path in this service already takes."
|
|
- "WRAP_MAX_BYTES SURVIVES UNCHANGED, at 8 MiB, with the same raw-FileResponse fallback. The work behind it is now trivial, but the READ is not: the largest live verbatim booth is 280 KB and a pathological one still should not be pulled into memory. A booth over the cap loses its chrome exactly as it does today -- no regression, and the constant keeps its existing test."
|
|
open_questions:
|
|
- "Whether `/_booth/embed.js` should eventually carry the gallery page's chrome too, making one embed for both surfaces. Out of scope: the gallery page is server-rendered end to end and has no seam problem to solve."
|
|
- "Whether a booth should be able to suppress injection entirely (a `.no-embed` dotfile) for a report that wants to be served truly untouched. No live booth wants it; declaring the line and then not using it is already most of the way there. Parked rather than designed."
|
|
---
|
|
|
|
# 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:
|
|
|
|
1. **`wrap_verbatim_html`** searches 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.
|
|
2. **`booth/inline.py`** matches 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:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```json
|
|
{
|
|
"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>/`, so `embed.js` reads
|
|
> `location.pathname`, takes segment 2, and `decodeURIComponent`s 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.
|
|
```
|
|
|
|
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_answer` has 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.
|
|
|
|
## 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.html` is served
|
|
verbatim; every other surface is server-rendered end to end and has no seam
|
|
problem. `/_booth/embed.js` is 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 dead `standalone=False` macro parameter.** No caller
|
|
has passed `True` since 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 reads `FAVICON_HREF`, so this unit adds no third copy; it
|
|
does not remove the second.
|
|
- **`WRAP_MAX_BYTES` and 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 `/marks` since 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:
|
|
|
|
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.
|