--- 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 (``), 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 ``. 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 `` 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 `

The one asset that must survive

` loses both the wrapper's class and -- visually -- its framing, leaving the author's heading orphaned and the closing `
` 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. `` and `` 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//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

The one asset that must survive

``` `_EL_RE` matches the opening `
` and replaces it. The author's `.ask` wrapper class is gone, the `

` is orphaned, and the `

` 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 ``` 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": "
", "submit": "
", "questions": [ {"key": "logo", "html": "
"}, {"key": "display", "html": "
"} ] } ] } ``` 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//`, so `embed.js` reads > `location.pathname`, takes segment 2, and `decodeURIComponent`s it. A page > whose address is not `/b//...` mounts nothing and returns quietly. > > **Override:** a `