Files
troupe-standup/recoveries/render-site-channels.md
T

16 KiB

Q28 — The channels the entry walk cannot see

Merlin, 2026-10-11. Scratch tree /root/crucible/q28/; builder build_blocks_hard.py (sha256 c4127c6c9325ed9f8cab03b80b454cccde0f9f1e768289ff216d2fb79e78dbf2, 29,174 B — the Q27 scope-clause patch, copied in byte-identical and verified). blocks.json (63c9cee6103a6824c90c71440fa983d2f5592e380f9c3fb40f7ad366ae41723d, 278,459 B) read-only. Nothing published, nothing staged, no shared-tree edit, no decisions/ or inbox/ action.

The guarantee, in one sentence

Any string containing {{ or }} anywhere inside a blocks.json entry — at any nesting depth, in any field, checked before a single byte is written — kills the build; the only tokens that ever ship are well-formed {{x:HANDLE}} tokens inside the four prose panels of a summary-branch entry naming a handle the ledger cites somewhere.

The three guards that enforce it, printed from build_blocks_hard.py:

:192-225  check_tokens(b, slug)   — walk(b, "") recurses dicts/lists; a string with a
                                     sentinel dies unless its path is in panels_for(b),
                                     then dies again if TOKEN_RE leaves residue.
:228-234  check_gaps(data)        — the one non-entry string channel: coverage.gaps[i].
:151-169  check_uncited(b, slug, cited)
                                  — same TOKEN_RE scan, same message as expand_prose's
                                     die(), run above OUT.mkdir so no partial tree.
:204      walk(v, ...)            — recurses on the VALUE only; keys are never visited.
:292-304  build()                 — check_gaps, then check_tokens and check_uncited over
                                     every entry, all above OUT.mkdir / the rmtree.

Result

20 channels: 13 covered by fixture (19 fixtures), 7 excluded with a measured command. Every negative fixture exits 1 and writes 0 files; the one fixture whose output directory holds a file at failure (protected-clash) holds the file it planted, which the guard refused to clean — the builder itself wrote nothing. The no-token control is byte-identical to the live site-ledger (digest 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8, 125 files, diff -rq silent).

The channel table

Each fixture is a fresh scratch tree: patched builder + one mutated blocks.json. Columns: channel · guard · fixture · exit · files written · first stderr line. Malformed token {{x: unless stated; cited token {{x:arizonanhodl}}; uncited {{x:NotACitedHandle}}.

# channel guard fixture exit files first stderr line
1 coverage.gaps[i] — index Known-gaps list check_gaps gaps-sentinel 1 0 coverage.gaps[0]: token sentinel {{ in a field that is not a prose panel (panels: body, body_provenance, plain_summary.text, uncertainty)
2 slug_for() → canonical href walk (slug) slug-sentinel 1 0 x:slug: token sentinel {{ in a field that is not a prose panel
3 slug_for() → sitemap loc walk (slug) slug-sentinel 1 0 (same fixture — one slug feeds all four slug-derived landings)
4 slug_for() → index li id walk (slug) slug-sentinel 1 0 (same fixture)
5 slug_for() → pager hrefs walk (slug) slug-sentinel 1 0 (same fixture)
6 sortable[:4] → index year heading walk (sort_date) sortdate-sentinel 1 0 magazine-001:sort_date: token sentinel {{ in a field that is not a prose panel
7 esc(prev/nxt["n"]) → pager link text walk (n) pager-n 1 0 magazine-002:n: token sentinel {{ in a field that is not a prose panel
8 safe_url() → body_source_url, non-https shape safe_url url-nonhttps 1 0 block[30]: source URL must be https, got '{{x:'
9 body_source_url, https-with-token shape walk url-https-token 1 0 magazine-001:body_source_url: token sentinel {{ in a field that is not a prose panel
10 receipt arithmetic formatted strings (f'${usd}', f'{price}', f'{calc:,} sats') walk (receipt.usd) receipt-usd 1 0 block-5:receipt.usd: token sentinel {{ in a field that is not a prose panel (panels: body_provenance, plain_summary.text, uncertainty)
11 title — head <title>, JSON-LD headline, h1, note, index label (both branches) walk (title) title-sentinel 1 0 magazine-001:title: token sentinel {{ in a field that is not a prose panel
12 status — badge class + STATUS_LABEL lookup entry validation status-token 1 0 block[30] unknown status '{{x:'
13 ledger — LEDGER_LABEL lookup entry validation ledger-token 1 0 block[30] unknown ledger '{{x:'
14 JSON-LD datePublished + the Date line walk (date) date-sentinel 1 0 magazine-001:date: token sentinel {{ in a field that is not a prose panel
15 JSON-LD author name + byline (display_name) walk (contributor.display_name) jsonld-author-name 1 0 magazine-035:contributor.display_name: token sentinel {{ in a field that is not a prose panel
16 missing row 1 — label() staged branch (pristine :385) walk (title) staged-label-token (research-only + token) 1 0 magazine-001:title: token sentinel {{ in a field that is not a prose panel
17 missing row 2 — ledger-index badges (pristine :399/:400) validation status-token / ledger-token 1 0 (fixtures 12/13)
18 missing row 3 — genesis landing :435, block-5 body on the quote branch walk (body, panel set printed without body) genesis-quote-body 1 0 block-5:body: token sentinel {{ in a field that is not a prose panel (panels: body_provenance, plain_summary.text, uncertainty)
19 genesis landing :448 — uncertainty via expand_prose check_uncited (pre-write) genesis-uncited 1 0 block-5:uncertainty: prose-anchor token names handle @NotACitedHandle which no entry cites
20 static/protected archive files PROTECTED guard protected-clash (planted index.html in OUT) 1 0 (1 planted, refused) refusing to clean …/site-ledger: it holds archive source files ['index.html']

Positive controls (three)

fixture exit files what it proves
staged-label-reachable — magazine-001 flipped to research-only, no token 0 125 the staged branch is data-reachable: the built index carries (staged, not published) exactly once. Q27's table called it unreachable because no entry does today — 121/121 are cleared — which is the one thing an exclusion may not say.
jsonld-close-script — title = </script><b>hi</b> 0 125 the JSON-LD channel's guard is the json.dumps + </-escape, not the sentinel: the page carries <\/script> and no raw </script><b>. A different defect class, watched by a different mechanism — outside the sentinel's declared scope, stated rather than silent.
cited-genesis-positive — cited token in block-5's body_provenance 0 125 the token expands on both landings: exactly 2 files change (block-5/index.html, genesis/index.html), one x.com/arizonanhodl anchor each. Q26's third silent class stays closed under the patched builder.

Control

Unmodified blocks.json through the patched builder: exit 0, 125 files, tree digest 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8 (Q26's baseline, computed with Q26's own digest definition), diff -rq against the shared site-ledger silent.

The structural exclusions — each with the command that measures it

No fixture can place a token in these channels because no blocks.json value reaches them. The reason is a measured command, never a reading.

# channel command output
E1 CSS / FOOT literals grep -n 'CSS|FOOT' build_blocks_hard.py :45 CSS = """…""", :324 write_text(CSS), :379 FOOT = """…""", :512/:570/:607 parts.append(FOOT) — module literals, emitted verbatim; no data is interpolated into them. Reinforced by grep -c '\.format(' build_blocks_hard.py → 0: no emitted literal is ever .format()-ed with a value.
E2 esc(k) — receipt row keys grep -n 'for k, v in' build_blocks_hard.py two sites: :219 walk's obj.items() (values only) and :417 the receipt generator over rows = [("Amount", …), ("Price line", …), ("Amount in sats", …), ("Arithmetic", check)] — hardcoded at :415-416. The only key-rendering esc() in the builder renders hardcoded strings.
E3 dict keys grep -n 'for k, v in obj.items()' build_blocks_hard.py :219 — walk(v, …) recurses on the value only; walk() never visits a key, and no esc(k) exists outside E2. A blocks.json field name cannot reach any emit site.
E4 the OUT-is-site/ guard grep -n 'OUT must not be|refusing to clean|PROTECTED =' build_blocks_hard.py :32 PROTECTED = {…}, :298 the site/ comparison, :302 the clash die(). Source-read, not fixture-tested: OUT is hardcoded to BASE/"site-ledger" (:31), so the site/ comparison cannot fire without editing the builder.
E5 static/protected files — the write set python3: sorted files under the control build's site-ledger 125 files; intersection with PROTECTED = []. The builder's entire write set is enumerated, and none of the five hand-maintained names is in it.
E6 blocks-build.json manifest python3: json.load of the control build's manifest keys ['cleared','files','pages','research_only','schema']; 124 file entries, every value {"sha256","bytes"} — e.g. magazine-001/index.html → {"sha256": "aa550c2b…", "bytes": 4174}. No entry prose reaches the manifest.
E7 coverage keys other than gaps grep -n 'coverage' build_blocks_hard.py four lines, all coverage.gaps (:230, :232, :559, and the :229 docstring). The other keys (block_numbers_held, dollar_receipts_held, documented, timeline_events, …) are never read by the builder — unrendered data, outside the emit surface.

Two corrections, stated in prose (Q27's file is corrected by reference, never by edit)

  1. The pristine builder has 36 esc() call lines / 43 call occurrences, not 28. Measured: python3 over build_blocks.py — lines containing esc( minus the def esc(v): at :87 → 36; sum(l.count("esc(")) minus the definition's own occurrence → 43. Q27's "28 esc() calls" undercounts the emit surface its table claims to cover. (The pipe's correction C3 reproduces exactly; measured here independently.)
  2. Q27's stated fixture breakdown sums to 41 — 42 counting the omitted excl:contributor.handle row — against its own 35-row log. Measured: Q27-RESULTS.json holds 35 rows (5 at exit 0 / 125 files, 30 at exit 1 / 0 files); the record's 12 stated items (4+12+4+10+2+3+1+1+1+1+1+1) sum to 41. The row table itself is right; the sentence is not.

Findings

  1. The walk, not the site census, is the guarantee — and it is stronger than either record says. check_tokens recurses every string at any depth under an entry, so a sentinel in a field no entry has today still dies before OUT.mkdir. Q26 §9 limit 2 ("a sentinel in a field no entry has today cannot be enumerated from today's data") is answered by the walk, not by a table of 32 sites: the 19 fixtures here each name a channel, and the channel dies because its field is an entry string, not because the field is listed anywhere.
  2. slug_for()'s sanitisation is real but irrelevant to the guard. slug_for("{{x:") returns "x" (the [^a-z0-9-]+ run collapses to -, .strip("-") leaves the lone x) — which is why the slug-sentinel die message prints x:slug: rather than a slug at all. Q27's exclusion reason ("slug_for() sanitises {{ out") is falsified as the guard: the walk kills the token one step earlier, and the sanitised value never reaches an emit site. The pipe's C4 holds; re-measured here under the patched builder.
  3. The label-staged branch is the one row where "no entry does" was the stated reason and the measurement disagrees. 121/121 entries are publication_status: "cleared", so the branch is dead today — but flipping one entry to research-only builds it and the index prints (staged, not published). It is data-reachable, and the walk watches its field (title) regardless: fixture 16.
  4. The JSON-LD channel is watched by a different mechanism than the sentinel, and that is worth stating rather than folding in: json.dumps + </-escaping (:362) means a </script> in a walked field ships as <\/script> — measured, exit 0. The sentinel does not see it and does not need to; a future hardening could scan for it, but the item's scope is the token classes, and the answer to "must a token in a URL field die" is unchanged: yes, measured both ways (fixtures 8/9).

Limits

  • 13 fixtures of 16 negative ones each die at the sentinel walk by construction. That is the property being proven, but it means the fixtures demonstrate the channels, not a census of malformed shapes — Q26's 12-fixture set is the shape census and is not re-run.
  • The OUT-is-site/ guard is source-read (E4): OUT cannot be redirected without editing the builder, so the comparison is untested by fixture.
  • Nothing here re-tests Q25's census (46 prose rows / 25 X fetches) — cited, not re-run; no X page was fetched this tick.
  • All digests are dated readings from this host at the run clocks below. The control's byte-identity to the live site-ledger is measured, which is what makes the fixture set anchored to a build matching production.
  • Scratch only. The patched builder stays in /root/crucible/q28/; publishing it is Alexander's, the same gate Q26/Q13/Q27 state.

Scripts (re-runnable, in /root/crucible/q28/)

q28-probe.py (data facts: publication_status, ledger/status counts, sort_date/date/body_kind coverage, gaps) · q28-fixtures.py (19 fixtures + control, emits Q28-RESULTS.json) · q28-exclusions.py (7 exclusions + the 2 corrections, emits Q28-EXCLUSIONS.json) · q28-count-q27.py (the Q27 breakdown recount) · q28-which.py (which entry is n=5). Every fixture tree under fixtures/; the control build in the scratch root.

Run record

  • Builder build_blocks_hard.py sha256 c4127c6c9325ed9f8cab03b80b454cccde0f9f1e768289ff216d2fb79e78dbf2 (copied from /root/crucible/q27/, verified before use).
  • blocks.json sha256 63c9cee6103a6824c90c71440fa983d2f5592e380f9c3fb40f7ad366ae41723d, 278,459 B, read-only.
  • 19 fixtures: 16 negative (all exit 1, 0 builder writes) + 3 positive (all exit 0, 125 files).
  • Control: exit 0, 125 files, digest 91ecf6bf939a2a499983066a2331bb953eab8a4ca85c66a8a19f42cb1b28f7a8, diff -rq vs /root/stackchain-legends-build/site-ledger silent.
  • Shared tree untouched: build_blocks.py still 97fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672.
  • Live ledger index re-fetched: https://alexanderwhitestone.com/stackchain/blocks/ 200 / 34,488 B / sha256 4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e.
  • decisions/ and inbox/ untouched; nothing executed; no other party's file edited.

Defects of my own, logged first

  1. The first harness run died on receipt-usd / the genesis fixtures with SystemExit: not found: block-5 None — the receipt entries carry no slug (slug_for derives block-5 from n), so the slug selector found nothing. Caught on the first run; fixed with an n= selector. Nothing was published on the broken pass.
  2. The first protected-clash fixture ran the build before planting the file, so it reported exit 0 / 125 files and then died on FileExistsError when planting. Restructured so the plant precedes the build; the reported row is the planted one.
  3. slug-sentinel's die message prints x:slug: because slug_for("{{x:") returns "x" — an artifact of the fixture, not of the guard, and recorded above as finding 2 rather than quietly normalised.