15 KiB
Run 39 — the pin/hash convention split, aligned on the wire side
Merlin (Helm lane), 2026-10-15 ~16:1x–16:3xZ. Scratch tree /root/crucible/q68/.
Nothing deployed: no cp into /root/.helm/bin/, no write to the live document
root, decisions/ and inbox/ untouched, no another-party file edited, no
secret printed. Instrument set: fix_r39.py, build_r39.py, run_r39.py,
stability_r39.py, report_r39.py; artifacts out/FIX.json,
out/BUILD39.json, out/RUN39.json, out/R39-STABILITY.json,
out/R39-STABILITY-{1,2}.txt (host-local, E13 — git ls-files | grep -c '^out/'
= 0; the raw path 404s for out/, which is not a missing file).
1. The unit, and why it was this one
Run 38 (recoveries/helm-adoption-rehearsal.md) performed the §4 adoption on a
copy and measured the blocker: after helm pin, collect and render produce a
page whose green "match" pill the gate refuses —
pin_stackchain (helm-collect.py:664-680) hashes curl's stdout read as TEXT
(Python universal newlines: 13 CRLF pairs fold, 217,163 wire bytes ->
217,119 characters), so the pin and manifest_match were over the folded body;
G13 row 2 (helm-selftest-q65.py:3344-3376) hashes the RAW bytes of a fresh
fetch and compares them to the held pin.
Run 38's own closing line named the fix — "align the convention on one side" —
and named which side: the wire. The gate's rows compare the pin against the
bytes an outside verifier reproduces with curl -sS <url> | sha256sum; the
collector's comment through 2026-10-15 already admitted the normalised digest
is not independently reproducible, yet the pin was taken through the normalised
path. A drift detector whose reference a stranger cannot recompute is not a
detector. The pin moves to the wire.
2. What changed — two files, the gate untouched
- The collector (
helm-collect-r39.py, from4e45021d…):collect_stackchain()fetches the body once, in bytes mode and computes both digests from the same bytes —live_body_sha256_wire(canonical) andlive_body_sha256(the CRLF-folded digest, kept under its historical field name because the gate's G6 contract requires both fields to exist and to differ).manifest_matchis now pin-vs-wire.pin_stackchain()hashes the wire bytes the same way, in one fetch. The old block fetched the body twice — two curl calls that could return different bodies and pair two digests that never described the same response; that race is gone too.body_hash_normalisationstill carries the substring G6 requires ("CRLF->LF") and now names the pin convention explicitly. - The renderer (
helm-render-r39.py, froma70998c8…): the page's "live body sha256" line publishes the wire digest, because G13 row 1 compares that published hash against the held pin — with the pin on the wire and the page on the folded body, a green pill would fail row 1 instead. The old field stays as a fallback so pre-Run-39 snapshots render rather than blank. - The gate
833d0827…(190,330 B / 3,619 lines / 145 registrations) is byte unchanged. Its contract was right; the producer was not.
Pre-images preserved byte-exact beside the fixed files
(helm-collect-r39-pre.py, helm-render-r39-pre.py), every edit's anchor
asserted to occur exactly once, compile() before each write.
| file | pre-image | fixed | bytes | lines (wc -l) |
|---|---|---|---|---|
| collector | 4e45021d80395d3f… |
5a99364bd4bbca2b5d7f1c4ff13dba5a1488154b6e363f238e2e265a243b9966 |
29,954 -> 31,595 | 720 -> 750 |
| renderer | a70998c8592a03cd… |
117001e839dbbac5fb35c3d1c5f11da461c7e5564049b1aeb5d51a35a7bbfdc6 |
22,930 -> 23,523 | 514 -> 522 |
| gate | 833d08275977054a… |
unchanged | 190,330 | 3,619 |
3. The rehearsal — two arms, one sandbox each, predictions first
Arm A = the fix; arm B = the control (the shipped text-mode pin + the shipped
renderer). Both sandboxes are a copy of /root/.helm/bin with the payload
landed on top (adoption_identical_to_payload: true in both), every live-path
literal the gate names redirected into the sandbox (collector path x2,
renderer path x8, deployed page x8, held pin x2, G11 scratch renderer x3), and
the held pin seeded with the real live pin so the drift is the real drift.
Predictions were written into build_r39.py's docstring and run_r39.py's
PREDICTIONS dict before any gate ran; 13 of 14 met, the miss named in §5.
Arm A — the fix
| leg | result |
|---|---|
| collect #1 | manifest_match: false — wire live c34ae2c245117ca7… vs held f27326e8b13d… (pinned 2026-10-08); snapshot records both digests (live_body_sha256 e40308e573d0…, live_body_sha256_wire c34ae2c24511…), freshness FRESH, observed_at 2026-10-15T16:17:01.942Z |
| render #1 | rc 0, red drift pill, 13,849 B / 3dd6125ff525462a…, as of x2, not re-checked since x0, published hash c34ae2c245117ca7 |
| gate on render #1 | rc 0 / 195 passed / 0 failed |
--pin |
rc 0, pinned c34ae2c245117ca73ba5720b2c22b6fab689840070ff68cda7bffe3e723986d4 -> sandbox held (120 B / 3b543dbe732a3959… / mtime_ns 1792081036093293752) |
| collect #2 | manifest_match: true; held_body_sha256 == live_body_sha256_wire == c34ae2c24511… |
| render #2 | rc 0, green "match" pill, 13,849 B / 6d134efce97782e2…, as of x2, not re-checked since x0, published hash c34ae2c245117ca7 == pin[:16] |
| gate on render #2 | rc 0 / 195 passed / 0 failed — THE LEG Run 38 measured rc 1 / 194 / 1 on |
| gate repeat (byte-stability) | rc 0 / 195 / 0; the two raw stdouts are 17,027 B each and differ on exactly 4 lines, all per-run mkdtemp sandbox paths (/tmp/tmp*, /tmp/q31-g8-deploy-*, sbA/helm-legs-*); a normalisation over those shapes gives residual 0, one normalised digest 49eb85e9a1461769… |
| G11-contract twin | rc 1 / 194 / 1 — see §5, the sole failure is the twin's own artifact |
| drift probe | sandbox pin mutated to 0*64 -> collect #3 manifest_match: false -> render #3 red drift pill -> gate rc 0 / 195 / 0; pin restored, content sha256 asserted equal to the post-pin value |
| outside verifier | curl -sS --max-time 20 https://www.stackchainmagazine.net/ | sha256sum -> c34ae2c245117ca7…, 217,163 B: == snapshot live_body_sha256_wire, == held pin, != live_body_sha256 (e40308e5…) |
Arm B — the control, the bug reproduced on this instrument
| leg | result |
|---|---|
| collect #1 | manifest_match: false (normalised live e40308e5… vs held f27326e8…) |
--pin |
pinned e40308e573d046d7d503acecbfbde82b8ccd10758a345b0440d55b796e2d821e — the CRLF-folded digest, != the wire digest |
| collect #2 | manifest_match: true (both sides folded) |
| render #2 | green "match" pill, published hash e40308e573d046d7 |
| gate on render #2 | rc 1 / 194 passed / 1 failed, sole failure G13 row 2: "an unqualified green pill is confirmed by a fresh fetch" — live_sha256=c34ae2c2… held_pin_digest=e40308e5… pin_matches_live_now=False |
That is Run 38's 194/1, reproduced here with the same gate on the same host, differing only in the collector — the before/after pair the fix needed.
4. The incidental catch
The byte-stability script re-ran the parity gate after the drift probe, when
the sandbox page was still render #3's red pill but the pin had been restored to
the live digest. Both runs returned rc 1 / 194 / 1 with the sole failure
G13 row 3: "a red pill is confirmed by a fresh fetch too" —
unqualified_red_pill=True live_sha256=c34ae2c2… held_pin_digest=c34ae2c2… pin_matches_live_now=True. Row 3 was demonstrated in Run 35 on a constructed
cell; here it fired on a real state transition the sandbox itself produced
(re-pin after a drift render). The failure is the sandbox's page being one
render behind its pin, not a defect in the fix.
5. The one prediction miss, and it is the twin's own artifact
A-P7 (the G11-contract twin at 195/0) measured 194/1. The sole failure
is the G14 discriminator row: "the G14 predicate discriminates: the
adoption payload's own stale output (G11, 306d4bee) v…". The twin redirects
the pre-G14 renderer path (/root/crucible/q59/helm-render-g11.py) to the
sandbox bin renderer — which is the payload renderer now — so the
discriminator's negative leg (the OLD renderer's stale output must lack the
hash-line qualifier) collapses by construction: both legs render with the fixed
renderer and both carry the qualifier. Every G11 contract row itself passes.
Run 38 logged the identical artifact on the identical twin ("The G11 twin's
second failure is Merlin's own artifact: its negative leg renders the same
scratch path the twin redirected"). The prediction was over-optimistic because
it was carried from Run 38's docstring, where it was also not met. A twin that
makes the two renderers identical cannot demonstrate a discriminator between
them; the honest unit is the parity gate (195/0, §3) plus the control arm's
194/1 (§3), which between them isolate the fix.
6. Live, deployed, sandboxes, disk
Live delta 0 across every leg of both arms (bytes + sha256 + mtime_ns,
censused before and after every leg): deployed page 9,865 B /
427b7b54c69b18d2… / mtime_ns 1791709034017837233; snapshot 14,039 B /
aa842b0fd88037f8… / mtime_ns 1791479579475780303; held pin 120 B /
5b6d70716153c3f4… / mtime_ns 1791478687099553071; the five deployed bin
digests unmoved (ee0b36a82f23…, 5c67473d8928…, 7ed1bca8f9da…,
12570786ba70…, 738699986778…). grep -c HELM_COLLECTOR /root/.helm/bin/helm-selftest.py = 0, so Q40 stands and none of Q31–Run 39
is in production. /tmp/q31-g8-deploy-* census 475 before and after — the
q65 gate's Edit B holds. Sandbox dirs created by the runs and not cleaned by
the gate: 0 of 0.
7. What this does to the adoption ask
The ask was: cp the payload files, without a --pin first, acceptance
rc 0 / 195 / 0 on a red drift page — because a green pill could not ship
green while the site serves CRLF. That constraint is gone. Arm A measured
the full sequence with the re-pin: --pin -> collect -> render -> gate
= rc 0 / 195 / 0 on a page carrying a green pill, and the pill's reference
is now reproducible by any stranger with curl. Both adoption variants are
green:
- no
--pin: red drift pill, honest, 195/0; --pinfirst: green pill, 195/0 — the pin accepts the current live body as the drift reference, which is a policy choice, not a technical one.
Adoption sequence, updated (four files, one order): adopt renderer
117001e8… (23,523 B, superseding a70998c8…) -> adopt collector
5a99364b… (31,595 B, superseding 4e45021d…) + gate 833d0827…
(190,330 B, unchanged) -> re-collect -> re-render -> deploy. The file list must
be re-read at adoption time, never copied from an earlier block. The cp
itself remains Alexander's under §4.
Backward compatibility, stated: a held pin written by the old convention
(e.g. today's f27326e8…) simply does not equal the wire digest, so
manifest_match reads False until a re-pin — the same state as today, since
the live pin is stale anyway. Nothing is deleted; live_body_sha256 keeps its
meaning and both digests remain in every snapshot.
8. Limits
- The rehearsal's page is rendered from the sandbox snapshot while the gate's producer legs render from the live snapshot (the live snapshot cannot be written from here); the live fetch inside G13 rows 2 and 3 is a real network read of the snapshot's own URL.
- The gate was run six times in arm A and twice in arm B; the 190 KB gate was not re-run for the parity rows beyond those, and the failing-row text is quoted from the recorded runs.
- The wire convention assumes the served bytes are the bytes to pin. A site
that re-encodes per response (gzip variants, chunk boundaries) would make
even the wire digest unstable; today's body is byte-stable across every
reading this lane has taken (217,163 B /
c34ae2c2…since 2026-10-14). - A subject that reads the held pin and re-writes it through a different fetch path (e.g. a proxy that normalises newlines) would reintroduce a split; the gate's G13 rows are the detector for exactly that, and they now pass only when both sides agree.
9. Correction, Run 40 -- the byte-stability cell paired one run's bytes with
another run's diff
Returned by the control plane at 2026-10-15T17:41Z: the arm-A table's cell
gate repeat (byte-stability) read "rc 0 / 195 / 0; the two raw stdouts are
17,027 B each and differ on exactly 4 lines". The byte count and the rc/tally
belong to one run; the 4-line diff belongs to another. Re-measured from the
artifacts on 2026-10-15 ~18:3xZ, not accepted from the reply:
- The only raw stdout pair on disk is
out/R39-STABILITY-{1,2}.txt: 17,318 B each, both rc 1 / 194 passed / 1 failed -- the section 4 incidental-catch re-run after the drift probe, not the acceptance repeat. Per-run stdout sha256b93549cf1ef860bc...and0dd6922a53a52d20..., both equal to the valuesout/R39-STABILITY.jsonrecords for those two runs; the diff is exactly 4 lines (1, 10, 209, 210), all per-runmkdtemppaths (/tmp/tmp*,/tmp/q31-g8-deploy-*,sbA/helm-legs-*); normalised residual 0, normalised digest49eb85e9a1461769...-- every field re-read from the artifact and the two captures this run. - The 17,027 B / rc 0 / 195 / 0 figures are the five
gate_parity*legs ofout/RUN39.json-- arm Agate_parity_render1(379cfc4e...),gate_parity_render2(799e3e21...),gate_parity_render2_repeat(88fbede4...),gate_parity_render3(f8df74cc...), and arm B'sgate_parity_render1(fc26907c...): every one 17,027 B / rc 0 / 195 / 0 with distinct digests. Four arm-A legs cover three distinct gate states plus render #2's own repeat, and the fifth is arm B's control leg. Their raw stdouts were not the pair that was diffed; the one same-configuration repeat on record (arm Arender2vsrender2_repeat,799e3e21...vs88fbede4...) was not captured to disk, so the 4-line diff cannot be attributed to it from any artifact that exists. - No verdict, digest, row or count in sections 1-8 moves. The 4-line count, the residual 0 and the normalised digest are correct measurements of the 17,318 B pair; only the byte count and the rc/tally beside them belonged to another run. Section 4 already states the stability pair's rc 1 / 194 / 1 correctly -- the arm-A table cell is the sole mispairing.
- Erratum form, per this lane's correction rule: the Run-39 commit message
(
28ca8e5) and the queue's Run-39 block carry the same sentence. A commit message cannot be edited, and the queue is append-only, so both stand and are corrected by this section. Nothing above this append was touched.