16 KiB
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_entryemits as the archive's own prose:bodywherebody_kind == "summary",plain_summary.text,body_provenance,uncertainty. Thebody_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
@handleprinted in a prose panel, or a bare-word name that is the last path segment of anx.com/twitter.comURL somewhere inblocks.json(the Q23 residual rule). - Cited handle (the 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 (i,home,search,intent(s),messages,notifications,explore,settings,compose,login,logout,about,privacy,tos,help,hashtag). The reserved-path list exists becausex.com/i/spaces/<id>URLs put the literal segmentiin an unguarded registry. - Verdicts, from a fetched
https://x.com/<handle>withcurlwithout-L:resolve(HTTP 200, live profile),resolve-moved(200, description readsNow at @<other>),suspended(200,<h2>Account suspended</h2>),404(User Profile Not Found). - Anchored already = the entry's current emitted page carries an
hrefwhose last path segment is exactly the handle (measured against this run's scratch build, which is byte-identical to the livesite-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):
<a href="https://x.com/HANDLE">@HANDLE</a>
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: <slug>:<field>: 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/<handle> 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 / sha256281246f7caa03330…/ 2026-10-10T22:13:07Z. In registry. - StackchainQuant (5 mentions across 3 entries): 404 / 37,702 B / sha256
985bf717f99e7173…/ 2026-10-10T22:13:07Z, ``. 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 nox.comanchor 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, themagazine-091wording). The build already anchors the first three; the convention changes nothing for them.
8. Findings
- 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. - 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 (
Sayloron 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. - 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.
{{/}}collide with nothing inblocks.json(0 occurrences of{{,}},[[,]]), so the syntax is safe to introduce today.- 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
- Prototype only.
build_blocks_proto.pylives at/root/crucible/q25/and was never copied into the shared build tree; the shared tree'sbuild_blocks.pyis at97fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672and itssite-ledgerbyte-identical at close (§2). Publishing any build change is Alexander's, as Q13 states. - 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.
- A token in the quote branch prints literally.
expand_proseis wired only to the four prose panels; a{{x:…}}insidebody_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. - 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.
resolve-movedhas 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.