# Q26 — Harden the prose-anchor convention: the two silent token classes die loudly **Status: DONE — hardened prototype built, all five Q25 legs re-run green, two negative test sets (7 malformed tokens, 5 outside-panel sentinels) each dying with its own message, the `contributor.display_name` fifth slot measured without building it.** Merlin, 2026-10-11 ~00:20–00:24 UTC. Scratch tree `/root/crucible/q26/`, every artifact re-runnable from the scripts named in §9. **Nothing published, nothing applied to the shared build tree; `blocks.json` was read-only throughout.** --- ## 0. Reading rule (unchanged from Q25, restated so the population is re-derivable) - **Prose panels** = the fields `render_entry` emits as the archive's own prose and routes through `expand_prose`: `body` **where `body_kind == "summary"`**, `plain_summary.text`, `body_provenance`, `uncertainty`. The `body_kind == "quote"` branch renders `body` through `esc()` inside a blockquote — third-party quoted text, **not a panel**. - **Token sentinel** = any occurrence of the literal `{{` or `}}` anywhere in an entry. - **Malformed token** = a `{{x:...}}` in a panel that `TOKEN_RE` (`\{\{x:([A-Za-z0-9_]{1,15})\}\}`) does not match. - **Cited handle (validator registry)** = every `contributor.handle` value plus the first path segment of any `https://x.com/…` or `https://twitter.com/…` URL anywhere in `blocks.json`, minus reserved account paths. ## 1. What was hardened, and why Q25 left two classes failing silently, both reproduced in the control plane's 2026-10-10 verification of Q25: 1. **A malformed token passes the build and prints raw braces.** `expand_prose` returns `esc(text)` unless the text contains the literal `{{x:`, and the validator only ever inspects what `TOKEN_RE` matched. So `{{x:saylor}`, `{{x:}}`, `{{ x:saylor }}`, `{{y:saylor}}` and a 16+ character handle all emitted `Built 123 pages from 121 entries`, exit 0, with the literal on the page. 2. **A token outside the four panels prints literally** — the quote branch (Q25's own limit 3), a `title`, an `excerpts[].text`, a `contributor` field, an index `coverage.gaps` string. Q25's validator could not see any of these. The fix is a **pre-write scan**, `check_tokens(b, slug)` plus `check_gaps(data)`, run for every entry **before a single byte is written** (the scan sits above the `OUT` cleanup in `build()`, so a `die()` can never leave a half-emitted tree — the Q25 prototype's scan point would have): - any `{{` or `}}` in a field the panels do not render → `die`; - any sentinel left in a panel after `TOKEN_RE.sub("", obj)` → `die`; - the panel set is computed **per entry**: `body` is a panel only when `body_kind == "summary"`, so the quote branch is caught by construction rather than by a special case. Two further changes ride along, both byte-neutral with no token present: - `render_genesis` now routes `body_provenance` and `uncertainty` through `expand_prose` instead of `esc()`. The genesis page previously could not expand a token those panels carried even though `block-5`'s own page did — a third silent class, found while writing the positive control, not in Q25's list. - `coverage.gaps` strings are checked (the index renders them through `esc()`). ## 2. The diff `/root/crucible/q26/DIFF.txt` — `diff -u build_blocks_proto.py build_blocks_hard.py`: **+70 / −6 lines, 5,817 B.** `DIFF-PRISTINE.txt` — against the shared tree's `build_blocks.py` (sha256 `97fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672`): **+118 / −11 lines, 9,949 B.** The hardened prototype is `build_blocks_hard.py`, **27,878 B / sha256 `d45cc28f7d69afcdab4eccb16e9e0cc264e4317e059e17f1ba53eb23f792cb60`** (this host, 2026-10-11T00:22Z). The Q25 prototype, copied unchanged into this tree, verified byte-identical first: sha256 `5af80b78961b4f55ca1ac436c19074d275b9a255569de07156a527de39d8de35`. ## 3. The five Q25 legs, re-run on the hardened prototype (`Q26-VERIFY.txt`) **Leg 1 — no-token build is byte-identical.** Unmodified `blocks.json` (278,459 B / sha256 `63c9cee6103a6824c90c71440fa983d2f5592e380f9c3fb40f7ad366ae41723d`, unchanged from Q25's record): ``` $ python3 build_blocks_hard.py Built 123 pages from 121 entries (121 cleared, 0 research-only) tree digest: 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8 baseline : 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8 125 files; BYTE-IDENTICAL to baseline $ diff -rq /site-ledger /root/stackchain-legends-build/site-ledger (no output — live tree identical) ``` **Leg 2 — the registry is unchanged: 8 handles.** `@anthonydessauer @arizonanhodl @guiamadogomes @happyclowntime @rockachopa @satskeeper_ @saylor @yoshishisatoshi` (`REGISTRY.txt`, identical to Q25's). **Leg 3 — the uncited-handle negative test still fails closed**, same message: ``` $ cd negtest && python3 build_blocks_hard.py # {{x:NotACitedHandle}} in uncertainty exit 1 BUILD FAILED: block-25:uncertainty: prose-anchor token names handle @NotACitedHandle which no entry cites ``` **Leg 4 — the token demo is unchanged: 6 deltas (H1–H4 as tokens) → 3 of 125 emitted files.** ``` $ cd tokentest && python3 build_blocks_hard.py exit 0 Built 123 pages from 121 entries (121 cleared, 0 research-only) emitted files: 125 -> 125; changed: 3 event-2022-07-18-the-5-invitation-becomes-a-chain/index.html magazine-002/index.html magazine-005/index.html emitted: event-2022-07-18… https://x.com/AriZonanHODL @AriZonanHODL (x2: body + plain English) emitted: event-2022-07-18… https://x.com/satskeeper_ @satskeeper_ (x2) emitted: magazine-002 https://x.com/AriZonanHODL @AriZonanHODL emitted: magazine-005 https://x.com/AriZonanHODL @AriZonanHODL ``` **Leg 5 — the diff itself** (§2). All five legs: **green**. ## 4. The malformed-token set — 7 fixtures, each in its own tree, each `exit 1` Each fixture appends one token to `block-25`'s `uncertainty` (a panel rendered on every entry). Raw stderr in `MALFORMED.txt`: | fixture | token | exit | first stderr line | |---|---|---|---| | open brace | `{{x:saylor}` | 1 | `BUILD FAILED: block-25:uncertainty: malformed prose-anchor token — sentinel {{ left after token expansion; tokens must read exactly {{x:HANDLE}} with a 1-15 character handle` | | empty handle | `{{x:}}` | 1 | same, sentinel `{{` | | whitespace padded | `{{ x:saylor }}` | 1 | same, sentinel `{{` | | wrong namespace | `{{y:saylor}}` | 1 | same, sentinel `{{` | | handle over 15 chars | `{{x:abcdefghijklmnopq}}` | 1 | same, sentinel `{{` | | lone close brace | `}}` | 1 | same, sentinel `}}` | | close then open | `}}{{x:saylor}}` | 1 | same, sentinel `}}` | All seven now die. Under the Q25 prototype all seven built and shipped the literal. ## 5. The outside-panel sentinel set — 5 fixtures, each `exit 1` Each places a **valid** `{{x:saylor}}` where no panel renders it (`OUTSIDE-PANEL.txt`): | fixture | field | exit | message | |---|---|---|---| | quote branch | `block-6:body` (`body_kind: quote`) | 1 | `token sentinel {{ in a field that is not a prose panel (panels: body_provenance, plain_summary.text, uncertainty)` | | title | `magazine-001:title` | 1 | `… (panels: body, body_provenance, plain_summary.text, uncertainty)` | | excerpt text | `magazine-001:excerpts[0].text` | 1 | same | | contributor display_name | `magazine-035:contributor.display_name` | 1 | same | | index coverage gap | `coverage.gaps[4]` | 1 | same | Note the quote-branch message prints the panel set **without `body`** — the per-entry `body_kind` rule, visible in the error itself. This fixture is the one Q25's limit 3 named; it was the only silent class that survived the first pass of the hardening and the reason the panel set is computed per entry. ## 6. Positive control — a cited token expands on the entry page *and* the genesis page `{{x:AriZonanHODL}}` appended to `block-5`'s `body_provenance` (`positive/`): ``` $ cd positive && python3 build_blocks_hard.py exit 0 Built 123 pages from 121 entries (121 cleared, 0 research-only) block-5 page anchors: https://x.com/AriZonanHODL -> @AriZonanHODL (x2: panel + contributor slot) genesis page anchors: https://x.com/AriZonanHODL -> @AriZonanHODL changed files: 2 [block-5/index.html, genesis/index.html] ``` The genesis-page anchor is the §1 wiring change proven: without it the same token would have expanded on `block-5` and printed raw braces on `genesis/`. A negative test with no positive control proves nothing, so both are recorded. ## 7. The `contributor.display_name` fifth slot, measured without building it Measured from `blocks.json` and from this run's scratch build (byte-identical to the live `site-ledger`), per entry, with Q24's staged delta applied in its own scratch tree (`DISPLAY-NAME-MEASURE.txt`): | entry | Q24 clause | `display_name` today | `handle` today | live page today | after Q24's delta | named accounts in registry | |---|---|---|---|---|---|---| | magazine-035 | G1 | `@BTC_Freeborn and @AnthonyDessauer` | `null` | `@BTC_Freeborn and @AnthonyDessauer` — anchors: NONE | `BTC_Freeborn and Anthony Dessauer (@BTC_Freeborn)` — anchor `x.com/BTC_Freeborn`; sha256 `871638b8…` → `565e6f68…` | `@BTC_Freeborn` no; `@AnthonyDessauer` **yes** | | magazine-056 | G2 | `Red Tail Hawk (@RedTailHawk1923)` | `null` | text only — anchors: NONE | `Red Tail Hawk (@RedTailHawk1923)` — anchor `x.com/RedTailHawk1923`; sha256 `33b55f8f…` → `8094e862…` | `@RedTailHawk1923` no | | magazine-070 | I2 | `@MidyReyes` | `null` | text only — anchors: NONE | `MidyReyes (@MidyReyes)` — anchor `x.com/MidyReyes`; sha256 `6227e199…` → `c994e055…` | `@MidyReyes` no | All three live-page digests reproduce Q24's fix-pack figures to the nibble (`871638b8abb5…`, `33b55f8f61fe…`, `6227e199522f…`), so the live tree is still the tree Q24 measured. **Recommendation, per entry:** - **magazine-056 (G2), magazine-070 (I2): the handle-only fix is the right instrument and it is complete.** One account named, one `handle` slot; the build already anchors `contributor.handle` (line 310), and the delta is what admits the handle into the registry. The convention cannot help — `display_name` is not a panel, and §5 proves a token there now dies at build time. - **magazine-035 (G1): handle-only anchors `@BTC_Freeborn` and leaves `@AnthonyDessauer` unanchored**, because `contributor` has exactly one `handle` slot while the byline names two accounts — and `@AnthonyDessauer` *is* in the registry, so a token for it would be admitted today. A wording-only fix (strip both `@`s, as Q24 stages) loses the link on both. **The only instrument that anchors both is `display_name` as a fifth panel** carrying `{{x:AnthonyDessauer}}` beside the handle slot — a build change, which is Alexander's, not a wizard's. **Which of Q24's deltas the hardened convention can carry as links: H1–H4 only — 4 of the 31.** The hardening changes no clause's carrier; it makes the four token carriers safe to publish. Everything else is a structured field the build already anchors, a note where the handle is uncited or dead, or a wording call. ## 8. Findings 1. **The first hardening pass missed the quote branch, and the error message now shows why it had to.** A path-based panel set ("`body` is a panel") admits a token in a quote-kind entry's `body`, where `render_entry` renders it through `esc()`. The panel set is per entry and depends on `body_kind`; the quote-branch error prints the reduced set, so the rule is visible in the failure. 2. **The genesis page was a third silent class, not in Q25's list.** `render_genesis` routed `body_provenance` and `uncertainty` through `esc()`, so a token those panels carry would expand on the entry's own page and print literally on `genesis/`. Routing them through `expand_prose` is byte-neutral with no token present (proved by §3 leg 1) and closes the class. 3. **The scan must run before the output tree is touched.** A `die()` inside the write loop leaves a half-emitted `site-ledger` behind, which on a shared tree is exactly the state a reader cannot distinguish from a partial publish. The checks now sit above the `OUT` cleanup. 4. **A negative test without a positive control proves nothing** — Q25's own incident note. Leg 6 carries both: a cited token builds, expands, and moves exactly the two files it should. 5. **The registry is the binding constraint, unchanged.** 8 handles of the 23 the census found printed in prose; the hardening widens nothing, it only makes the four carriers safe. 6. **`{{`/`}}` still collide with nothing in `blocks.json`** (0 occurrences), so the sentinel scan is silent on today's data — the no-token build proves it. ## 9. Limits 1. **Prototype only.** `build_blocks_hard.py` lives at `/root/crucible/q26/` and was never copied into the shared build tree. Verified at close: the shared tree's `build_blocks.py` is still sha256 `97fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672`, its `site-ledger` byte-identical to this run's scratch build (125 files each, `diff -rq` silent), and `find /root/stackchain-legends-build -newermt 2026-10-10 18:00 -type f` returns nothing. The live archive is unchanged: index `https://alexanderwhitestone.com/stackchain/blocks/` **200 / 34,488 B / sha256 `4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e`** at 2026-10-11T00:23:25Z. Publishing a build change is Alexander's, as Q13 states. 2. **7 + 5 fixtures are the test set, not a census of typos.** The malformed set is the item's own list plus a lone `}}` and a close-then-open; the outside-panel set is one fixture per field class. A sentinel in a field no entry has today cannot be enumerated from today's data. 3. **`display_name` was measured, not built.** No fifth panel was implemented; the recommendation is a decision input, not a staged change. 4. **Nothing re-tests Q25's census.** The 46-row population, the 25 X fetches and their liveness verdicts are Q25's, cited not re-run. 5. **X byte sizes are dated readings** (per-request CSP nonce); none is load-bearing here — no X page was fetched this run. ## 10. Scripts (all re-runnable, `/root/crucible/q26/`) `verify26.py` (all eight legs) · `display_name.py` (the fifth-slot measurement) · `clean.py` (scratch reset) · `build_blocks_hard.py` (the hardened prototype) · `build_blocks_proto.py`, `build_blocks.py`, `blocks.json` (inputs, unmodified). Artifacts: `DIFF.txt` (+70/−6 vs the Q25 prototype) · `DIFF-PRISTINE.txt` (+118/−11 vs the shared build script) · `NOTOKEN-BUILD.txt` (digest `91ecf6bf…`, 125 files, live-tree diff silent) · `REGISTRY.txt` (8) · `NEGTEST.txt` (exit 1) · `TOKEN-DIFF.txt`, `TOKEN-PAGES.txt` (3 of 125 files, 6 emitted anchors) · `MALFORMED.txt` (7 fixtures) · `OUTSIDE-PANEL.txt` (5 fixtures) · `POSITIVE.txt` (2 changed files) · `DISPLAY-NAME-MEASURE.txt` · `Q26-VERIFY.txt` / `Q26-VERIFY-run.txt` (full run log) · scratch trees `negtest/`, `tokentest/`, `positive/`, `mal-*/`, `out-*/`, `dn-*/`.