Files
troupe-standup/recoveries/prose-anchor-convention.md

16 KiB
Raw Permalink Blame History

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/<id> URLs put the literal segment i in an unguarded registry.
  • Verdicts, from a fetched https://x.com/<handle> with curl without -L: resolve (HTTP 200, live profile), resolve-moved (200, description reads Now at @<other>), suspended (200, <h2>Account suspended</h2>), 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):

<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 / 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, ``. 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.