15 KiB
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_entryemits as the archive's own prose and routes throughexpand_prose:bodywherebody_kind == "summary",plain_summary.text,body_provenance,uncertainty. Thebody_kind == "quote"branch rendersbodythroughesc()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 thatTOKEN_RE(\{\{x:([A-Za-z0-9_]{1,15})\}\}) does not match. - Cited handle (validator registry) = every
contributor.handlevalue plus the first path segment of anyhttps://x.com/…orhttps://twitter.com/…URL anywhere inblocks.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:
- A malformed token passes the build and prints raw braces.
expand_prosereturnsesc(text)unless the text contains the literal{{x:, and the validator only ever inspects whatTOKEN_REmatched. So{{x:saylor},{{x:}},{{ x:saylor }},{{y:saylor}}and a 16+ character handle all emittedBuilt 123 pages from 121 entries, exit 0, with the literal on the page. - A token outside the four panels prints literally — the quote branch (Q25's own
limit 3), a
title, anexcerpts[].text, acontributorfield, an indexcoverage.gapsstring. 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:
bodyis a panel only whenbody_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_genesisnow routesbody_provenanceanduncertaintythroughexpand_proseinstead ofesc(). The genesis page previously could not expand a token those panels carried even thoughblock-5's own page did — a third silent class, found while writing the positive control, not in Q25's list.coverage.gapsstrings are checked (the index renders them throughesc()).
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 <scratch>/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
handleslot; the build already anchorscontributor.handle(line 310), and the delta is what admits the handle into the registry. The convention cannot help —display_nameis not a panel, and §5 proves a token there now dies at build time. - magazine-035 (G1): handle-only anchors
@BTC_Freebornand leaves@AnthonyDessauerunanchored, becausecontributorhas exactly onehandleslot while the byline names two accounts — and@AnthonyDessaueris 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 isdisplay_nameas 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
- The first hardening pass missed the quote branch, and the error message now shows
why it had to. A path-based panel set ("
bodyis a panel") admits a token in a quote-kind entry'sbody, whererender_entryrenders it throughesc(). The panel set is per entry and depends onbody_kind; the quote-branch error prints the reduced set, so the rule is visible in the failure. - The genesis page was a third silent class, not in Q25's list.
render_genesisroutedbody_provenanceanduncertaintythroughesc(), so a token those panels carry would expand on the entry's own page and print literally ongenesis/. Routing them throughexpand_proseis byte-neutral with no token present (proved by §3 leg 1) and closes the class. - The scan must run before the output tree is touched. A
die()inside the write loop leaves a half-emittedsite-ledgerbehind, which on a shared tree is exactly the state a reader cannot distinguish from a partial publish. The checks now sit above theOUTcleanup. - 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.
- 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.
{{/}}still collide with nothing inblocks.json(0 occurrences), so the sentinel scan is silent on today's data — the no-token build proves it.
9. Limits
- Prototype only.
build_blocks_hard.pylives at/root/crucible/q26/and was never copied into the shared build tree. Verified at close: the shared tree'sbuild_blocks.pyis still sha25697fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672, itssite-ledgerbyte-identical to this run's scratch build (125 files each,diff -rqsilent), andfind /root/stackchain-legends-build -newermt 2026-10-10 18:00 -type freturns nothing. The live archive is unchanged: indexhttps://alexanderwhitestone.com/stackchain/blocks/200 / 34,488 B / sha2564f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557eat 2026-10-11T00:23:25Z. Publishing a build change is Alexander's, as Q13 states. - 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. display_namewas measured, not built. No fifth panel was implemented; the recommendation is a decision input, not a staged change.- 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.
- 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-*/.