# Q25 — A prose-anchor convention for the ledger build **Status: DONE — designed, prototyped and measured in a scratch tree; nothing published, nothing applied to the shared build tree.** Merlin, 2026-10-10 ~22:0x–22:3x UTC. Working tree `/root/crucible/q25/`, every artifact re-runnable from the scripts named in §8. --- ## 0. Reading rule - **Prose panels** = the fields `render_entry` emits as the archive's own prose: `body` where `body_kind == "summary"`, `plain_summary.text`, `body_provenance`, `uncertainty`. The `body_kind == "quote"` branch is third-party quoted text and is **excluded** (a token there would print literally — see §7, limit 3). - **Handle-shaped token** = an `@handle` printed in a prose panel, or a bare-word name that is the last path segment of an `x.com`/`twitter.com` URL somewhere in `blocks.json` (the Q23 residual rule). - **Cited handle (the 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 (`i`, `home`, `search`, `intent(s)`, `messages`, `notifications`, `explore`, `settings`, `compose`, `login`, `logout`, `about`, `privacy`, `tos`, `help`, `hashtag`). The reserved-path list exists because `x.com/i/spaces/` URLs put the literal segment `i` in an unguarded registry. - **Verdicts**, from a fetched `https://x.com/` with `curl` **without `-L`**: `resolve` (HTTP 200, live profile), `resolve-moved` (200, description reads `Now at @`), `suspended` (200, `

Account suspended

`), `404` (`User Profile Not Found`). - **Anchored already** = the entry's *current* emitted page carries an `href` whose last path segment is exactly the handle (measured against this run's scratch build, which is byte-identical to the live `site-ledger`, §2). ## 1. The convention A prose field may name a handle as: ``` {{x:HANDLE}} ``` `render_entry` expands it, **before** `esc()`, into exactly the anchor the contributor slot already emits today (`build_blocks.py` line 310): ```html @HANDLE ``` The prose around the token is escaped exactly as before; the handle is escaped inside the anchor. The build **fails closed** if a token names a handle no entry cites: ``` BUILD FAILED: :: prose-anchor token names handle @HANDLE which no entry cites ``` The expansion applies to the four prose panels and nowhere else — not the quote branch, not the excerpt anchors, not the JSON-LD author block (which already builds its own `x.com` URL from `contributor.handle`). ## 2. The build-chain proof: a no-token build is unchanged Baseline first: the pristine `build_blocks.py` run in the scratch tree (`/root/crucible/q25/baseline.py`): ``` $ python3 build_blocks.py Built 123 pages from 121 entries (121 cleared, 0 research-only) $ diff -rq /root/crucible/q25/site-ledger /root/stackchain-legends-build/site-ledger (no output — trees identical) tree digest (scratch build) : 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8 tree digest (live site-ledger): 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8 files: 125 each — BYTE-IDENTICAL ``` (Tree digest = sha256 over each file's relative path plus its own sha256, in sorted order — `baseline.py`, `verify.py`.) Then the prototype (`build_blocks_proto.py`, the diff in §3) built against the **unmodified** `blocks.json` — zero tokens present: ``` $ python3 build_blocks_proto.py Built 123 pages from 121 entries (121 cleared, 0 research-only) tree digest: 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8 baseline : 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8 125 files; BYTE-IDENTICAL to baseline ``` The prototype changes **+63 / −6 lines** of `build_blocks.py` and, with no token in the input, changes nothing in the output. **Documented.** The shared build tree is untouched, as the control plane's acceptance note requires: ``` $ sha256sum /root/stackchain-legends-build/build_blocks.py 97fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672 build_blocks.py $ diff -rq /root/stackchain-legends-build/site-ledger /root/crucible/q25/site-ledger LIVE-SITE-LEDGER-UNCHANGED ``` ## 3. The diff `/root/crucible/q25/DIFF.txt` — `diff -u build_blocks.py build_blocks_proto.py`, **+63 / −6 lines, 4,960 B**. The six removed lines are the four `esc(...)` calls on the prose fields plus two unchanged context lines. The added block is `TOKEN_RE`, `URL_HANDLE_RE`, `RESERVED_PATHS`, `collect_cited_handles()`, `expand_prose()`, the `cited` argument threaded through `render_entry(b, entries, cited)`, and the registry computed once in `build()`. ## 4. The validator, negative-tested `negtest/` — a copy of the prototype and a `blocks.json` carrying one token, `{{x:NotACitedHandle}}`, appended to the first entry's `uncertainty` (the panel rendered on every entry, so the field always runs through `expand_prose`): ``` $ cd negtest && python3 build_blocks_proto.py exit 1 stderr: BUILD FAILED: block-25:uncertainty: prose-anchor token names handle @NotACitedHandle which no entry cites ``` The registry at build time (`REGISTRY.txt`) — **8 handles**: ``` @anthonydessauer @arizonanhodl @guiamadogomes @happyclowntime @rockachopa @satskeeper_ @saylor @yoshishisatoshi ``` ## 5. The token demo — what it emits and what it costs `tokentest/` — Q24's clauses **H1–H4** staged *as tokens* instead of as display-form wording changes: `AriZonanHODL posted` → `{{x:AriZonanHODL}} posted`, `and Satskeeper with $7` → `and {{x:satskeeper_}} with $7`, `ArizonanHODL's $5` → `{{x:AriZonanHODL}}'s $5`, `ArizonanHodl's July 18, 2022 tweet` → `{{x:AriZonanHODL}}'s July 18, 2022 tweet` (6 deltas across `body` + `plain_summary.text` on 3 entries). ``` $ cd tokentest && python3 build_blocks_proto.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 ``` Six deltas changed **exactly 3 of 125 emitted files** — the three entries' own pages; the index, sitemap and `ledger.css` are untouched. The emitted anchor is byte-for-byte the contributor form. **Documented.** ## 6. The census — every handle-shaped token in the prose panels Population re-derived from `blocks.json` (121 entries), not copied from any queue figure (`census_tokens2.py` → `census.json`, liveness from `fetch_targets.py` → `TARGETS/`, 25 fetches `https://x.com/` **without `-L`**, 2026-10-10T22:12:28–22:13:07Z, one `code | bytes | sha256 | clock | title` line each in `TARGETS.txt`). **46 prose rows across 23 distinct handles. Verdicts: 20 `resolve`, 1 `resolve-moved`, 2 `404`, 0 `suspended`. Anchored already on the same page: 5 of 46 rows. In the validator registry: 4 of 23 handles.** | handle | verdict | page title | rows | entries | anchored already | in registry | |---|---|---|---|---|---|---| | @_overflow__ | 404 | User Profile Not Found - X | 1 | magazine-020 | 0 | no | | @a_koby | resolve | ko₿u (@a_koby) / X | 1 | magazine-026 | 0 | no | | @AnthonyDessauer | resolve | Anthony vonStackChain⛓16 (@AnthonyDessauer) / X | 2 | magazine-044 | 0 | yes | | @AriZonanHODL | resolve | AriZonan HODL (@AriZonanHODL) / X | 5 | block-5, event-2022-07-18…, magazine-002, magazine-005 | 1 | yes | | @BTCTimescom | resolve | BTC Times (@BTCTimescom) / X | 1 | magazine-064 | 0 | no | | @captainsiddh | resolve-moved | Sidd (@captainsiddh) / X — "Now at @captainsidd" | 1 | magazine-039 | 0 | no | | @caroghero | resolve | Caroghero #1492 (@caroghero) / X | 3 | event-2022-08-28…, magazine-041 | 0 | no | | @DecrimNat | resolve | Psitadel 🍄‍🟫⚡️ @Ple₿SlopArmy 🧡 (@DecrimNat) / X | 1 | magazine-045 | 0 | no | | @DerekMRoss | resolve | Derek Ross (@derekmross) / X | 2 | magazine-018 | 0 | no | | @fiatforassets | resolve | TradingFiatForAssets (@fiatforassets) / X | 1 | magazine-041 | 0 | no | | @HappyClownTime | resolve | Bob (@happyclowntime) / X | 8 | block-6, event-2022-07-18…, magazine-019 | 2 | yes | | @humblebitcoiner | 404 | User Profile Not Found - X | 1 | magazine-051 | 0 | no | | @laurenoflight_ | resolve | Lauren of Light ⚡️ (@laurenoflight_) / X | 1 | magazine-031 | 0 | no | | @LoKoBTC | resolve | Loꓘo 🏒🧄 (@LoKoBTC) / X | 1 | magazine-041 | 0 | no | | @MidyReyes | resolve | Midy ✝️🎙️💪🏽🦉🇩🇴 (@MidyReyes) / X | 1 | magazine-070 | 0 | no | | @parman_the | resolve | Parman - Paranoid Bitcoin Mentor 🔑 (@parman_the) / X | 2 | magazine-032, magazine-048 | 0 | no | | @sathoarder | resolve | Sathoarder #17⚡ (@sathoarder) / X | 2 | event-2022-08-28… | 0 | no | | @Saylor | resolve | Michael Saylor (@saylor) / X | 6 | event-2022-10-07…, magazine-036/038/053/068 | 2 | yes | | @SPACEBULL | resolve | MADEX (@spacebull) / X | 1 | magazine-061 | 0 | no | | @staceski | resolve | Stacey Berisford (@StaceSki) / X | 2 | magazine-003 | 0 | no | | @StackchainMag | resolve | Stackchain ⛓ Magazine (@Stackchainmag) / X | 1 | magazine-001 | 0 | no | | @tim_niemeyer_ | resolve | Tim Niemeyer (@tim_niemeyer_) / X | 1 | magazine-062 | 0 | no | | @waldovision3 | resolve | Waldo Stacker (@WaldoVision3) / X | 1 | magazine-025 | 0 | no | Bare-word names the `@`-rule cannot see, fetched the same way (`TARGETS/x-satskeeper_.meta`, `TARGETS/x-StackchainQuant.meta`): - **Satskeeper** (printed bare, 2 mentions on `event-2022-07-18…`): cited handle `@satskeeper_`, **resolve** — `no₿ody, IQ 277 (@satskeeper_) / X`, 200 / 153,942 B / sha256 `281246f7caa03330…` / 2026-10-10T22:13:07Z. In registry. - **StackchainQuant** (5 mentions across 3 entries): **404 / 37,702 B / sha256 `985bf717f99e7173…`** / 2026-10-10T22:13:07Z, `User Profile Not Found - X | 404 Error `. Not in registry (no entry cites it as a URL or contributor). Per-row detail in `census.json`; the 5 anchored-already rows are `@AriZonanHODL` on `block-5` and `@HappyClownTime` ×2, `@Saylor` ×2 — all already linked via contributor or excerpt anchors. **Excluded from the population, with the reason:** the `body(quote)` branch (2 rows: `@ArizonanHodl`, `@happyclowntime` on `block-6` — third-party testimony text); `@getalby`, which is the domain part of `bitcoinbekka@getalby.com`, not a handle (an email guard is in the token rule: the character before `@` must not be `[A-Za-z0-9._%+-]`); and the two suspended accounts `@BTC_Freeborn` / `@RedTailHawk1923` — those are `contributor.handle` structured fields, which the build already anchors, and they appear in no prose panel. ## 7. Which of Q24's 25 clauses the convention upgrades **Four: H1, H2, H3, H4** — the `prose-handle-form` clauses. Each becomes a link instead of a display-form wording change: | clause | entry | today (staged) | with the convention | |---|---|---|---| | H1 | event-2022-07-18… | `AriZonanHODL posted` → `@AriZonanHODL posted` | `{{x:AriZonanHODL}} posted` — live anchor, 200 / 130,760 B | | H2 | event-2022-07-18… | `and Satskeeper with $7` → `and @satskeeper_ with $7` | `and {{x:satskeeper_}} with $7` — anchor, and the display form is forced to `@satskeeper_` | | H3 | magazine-002 | `ArizonanHODL's $5` → `@AriZonanHODL's $5` | `{{x:AriZonanHODL}}'s $5` — anchor plus a casing normalisation | | H4 | magazine-005 | `ArizonanHodl's July 18, 2022 tweet` → `@AriZonanHODL's …` | `{{x:AriZonanHODL}}'s July 18, 2022 tweet` — same | **The other 21 clauses are not upgraded, and the reasons are the finding:** - **E1–E3 (StackchainQuant notes)** — the convention *cannot* link them: the handle is not in the registry (no entry cites it), so the validator refuses, and it 404s anyway. They stay notes. - **F1, F2 (`@_overflow__`, `@humblebitcoiner`)** — same twice over: uncited by the registry rule **and** 404. Anchoring a dead handle would trade a note for a dead link. - **J1 (`bitcoinbekka@getalby.com`)** — not handle-shaped; the LNURL address resolves at its well-known endpoint, which no `x.com` anchor can express. Stays a note. - **A1–A4, B1–B4, C1, D1, G1–G3, I1, I2** — structured-field or pure text clauses (`excerpts[].url`, `body_source_url`, `contributor.handle`, direction words, the `magazine-091` wording). The build already anchors the first three; the convention changes nothing for them. ## 8. Findings 1. **The registry, not the token, is the binding constraint.** 19 of the 23 prose handles resolve and are not cited anywhere in `blocks.json` — they are names, not sources. The convention as specified (fail on an uncited handle) can link exactly the 4 registry handles printed in prose (plus `@satskeeper_` as a bare word) today. Unlocking the other 19 is content work — adding citations — not build work. 2. **The validator is doing evidentiary work, and that is the right trade.** A looser rule (any handle-shaped name may be tokenised) would let the build link 19 names the ledger never sourced, including the person/org names Q24 already had to exclude (`Saylor` on four magazine pages names a person and a company, not the account). The strict rule keeps the archive's discipline: a prose anchor asserts the ledger cites that handle somewhere. 3. **5 of 46 rows are already anchored** on their own page — the census counts the convention's *marginal* effect, not a re-litigation of Q21's binding work. 4. **`{{`/`}}` collide with nothing** in `blocks.json` (0 occurrences of `{{`, `}}`, `[[`, `]]`), so the syntax is safe to introduce today. 5. **The one display-form casualty is `Satskeeper`:** the token forces `@satskeeper_`, because the cited handle carries the trailing underscore. Q24's H2 already proposed that exact string, so the convention and the staged clause agree. ## 9. Limits 1. **Prototype only.** `build_blocks_proto.py` lives at `/root/crucible/q25/` and was never copied into the shared build tree; the shared tree's `build_blocks.py` is at `97fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672` and its `site-ledger` byte-identical at close (§2). Publishing any build change is Alexander's, as Q13 states. 2. **23 handles is a census of the prose panels, not of the archive.** Contributor handles, excerpt URLs and the two suspended accounts are outside the population by construction. 3. **A token in the quote branch prints literally.** `expand_prose` is wired only to the four prose panels; a `{{x:…}}` inside `body_kind == "quote"` would render as text. That is deliberate (third-party text is not edited), but it means a token placed there fails silently rather than loudly — a future hardening step would extend the validator to *scan* all fields for tokens and fail on any outside the four panels. 4. **X byte sizes and hashes are dated readings.** The pages carry a per-request CSP nonce, so the two 404 pages (37,678 / 37,703 / 37,702 B) are byte-different from each other. The clock (2026-10-10T22:12–22:13Z) is the evidence. 5. **`resolve-moved` has one member** (`@captainsiddh` → `@captainsidd`, "Now at @captainsidd" in the fetched body); the convention links the cited handle, not its successor — updating it is a Q13 wording call, not a build fix. ## 10. Scripts (all re-runnable, `/root/crucible/q25/`) `baseline.py` (pristine build + live-tree diff + digest) · `census_tokens.py`, `census_tokens2.py` (the census and its two fixes) · `probe_names.py` (bare-word contexts + syntax collision check) · `fetch_targets.py` (25 fetches) · `rename_scan.py` · `map_clauses.py`, `dump_clauses.py` (the Q24 mapping) · `show_registry.py` · `build_blocks_proto.py` (the prototype) · `verify.py` (diff, no-token build, registry, negative test, token demo — all five legs) · `census_final.py` (the §6 table) · `emit.py` (this file). Artifacts: `DIFF.txt` (+63/−6) · `BASELINE-BUILD.txt`, `BASELINE-DIGEST.txt` · `NOTOKEN-BUILD.txt` · `REGISTRY.txt` (8 handles) · `NEGTEST.txt` (exit 1) · `TOKEN-BUILD.txt`, `TOKEN-DIFF.txt` (3 of 125 files), `TOKEN-PAGES.txt` · `census.json`, `census-final.json` · `TARGETS/` (25 profiles + `.meta` each) · `Q25-VERIFY.txt`.