Pre-existing, measured at 42ea67f, so it predates U3. `_hydrate` checked only
that `answer` was a dict and never that `answer["answers"]` was one, so
`marks_for` and `hold_read` both reported the mark healthy with no read error
-- and `_ask_inline.html` then asked a list for `.get`. The v0.2.2 lesson was
half-implemented: that outage was a file that could not be PARSED and the
reader was made lenient, while this one parses perfectly and breaks one layer
further in, at render, where no leniency existed.
Closed at the hydration boundary rather than by a third copy of the guard --
one predicate, one place, every surface inherits it. Only the multi case is
checked, because only the multi case indexes; requiring `answers`
unconditionally would break every single-question pick, and that direction has
its own test. Measured before and after: gallery and marks pages 500 -> 200,
the error visible on the page, the booth's other healthy pick untouched.
The placement was the one open operator question of the session. It was
surfaced three times without a ruling, so it is taken under a stated assumption
and is cheap to move: the whole fix is one condition in one function.
Two things fell out of it worth more than the fix.
`_safe_fragments` no longer has a reachable natural trigger. Probed every wrong
answer shape a .marks.json can carry: `answers` as a list, a string or null all
become hydration errors now, and a wrong-typed value INSIDE `answers` renders
without raising, because Jinja absorbs attribute access on a non-mapping. U3's
guard is a pure backstop, and its test now says so and trips it synthetically
through the shared macro module rather than asserting a path nothing reaches.
A guard tested by an unreachable input is an untested guard.
And that guard's handler could not survive the failure it was handling: it
caught a raising `_pick_fragments` and rebuilt the broken-ask box through the
SAME macro module that had just raised, so whenever `whole` was the broken
thing it re-raised and took the whole report. Found by accident while building
the falsifier. Fixed, with its own test.
Both new falsifiers were verified RED against their defeating change rather
than assumed.
607 -> 611 tests.
472 lines
42 KiB
Markdown
472 lines
42 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.
|
|
|
|
**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.
|
|
|
|
## 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.
|