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)
- The pristine builder has 36
esc()call lines / 43 call occurrences, not 28. Measured:python3overbuild_blocks.py— lines containingesc(minus thedef esc(v):at :87 → 36;sum(l.count("esc("))minus the definition's own occurrence → 43. Q27's "28esc()calls" undercounts the emit surface its table claims to cover. (The pipe's correctionC3reproduces exactly; measured here independently.) - Q27's stated fixture breakdown sums to 41 — 42 counting the omitted
excl:contributor.handlerow — against its own 35-row log. Measured:Q27-RESULTS.jsonholds 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
- The walk, not the site census, is the guarantee — and it is stronger than either record
says.
check_tokensrecurses every string at any depth under an entry, so a sentinel in a field no entry has today still dies beforeOUT.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. 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 lonex) — which is why theslug-sentineldie message printsx: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'sC4holds; re-measured here under the patched builder.- 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 toresearch-onlybuilds it and the index prints(staged, not published). It is data-reachable, and the walk watches its field (title) regardless: fixture 16. - 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):OUTcannot 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-ledgeris 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.pysha256c4127c6c9325ed9f8cab03b80b454cccde0f9f1e768289ff216d2fb79e78dbf2(copied from/root/crucible/q27/, verified before use). blocks.jsonsha25663c9cee6103a6824c90c71440fa983d2f5592e380f9c3fb40f7ad366ae41723d, 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 -rqvs/root/stackchain-legends-build/site-ledgersilent. - Shared tree untouched:
build_blocks.pystill97fedb5dd5e85d2b10071e8e4904b3a9b1573072149e722fbece93eaf0774672. - Live ledger index re-fetched:
https://alexanderwhitestone.com/stackchain/blocks/200 / 34,488 B / sha2564f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e. decisions/andinbox/untouched; nothing executed; no other party's file edited.
Defects of my own, logged first
- The first harness run died on
receipt-usd/ the genesis fixtures withSystemExit: not found: block-5 None— the receipt entries carry noslug(slug_forderivesblock-5fromn), so the slug selector found nothing. Caught on the first run; fixed with ann=selector. Nothing was published on the broken pass. - The first
protected-clashfixture ran the build before planting the file, so it reported exit 0 / 125 files and then died onFileExistsErrorwhen planting. Restructured so the plant precedes the build; the reported row is the planted one. slug-sentinel's die message printsx:slug:becauseslug_for("{{x:")returns"x"— an artifact of the fixture, not of the guard, and recorded above as finding 2 rather than quietly normalised.