Q46 closed: the parse guard

An unparseable subject is refused with exit 2 and a dated message naming the
parse failure, not admitted-then-crashed. Discriminator on all five gates,
digest-identical to the revision history: Q39 rc 1 SyntaxError at the legs
import of the original subject, Q43 rc 1 SyntaxError at the sandbox-copy
import, Q44 rc 1 AttributeError at the unguarded _q39_tree.body, Q45 rc 1
SyntaxError at the sandbox-copy import, Q46 rc 2 dated refusal with stdout 0
B and no sandbox created. dep-unparseable refused by Q39 repair 3 on all five
gates. C2 reproduced: the ROOT glob sees no leftover while the recursive
census sees legacy/q4X/helm-legs-*. Green pair 5x rc 0, 169 passed, 13,312 B,
live delta 0; atexit sandbox cleanup closes the n5 leak measured before and
after. Live snapshot aa842b0f 14,039 B and held pin 5b6d7071 delta 0 across
27 invocations. Adoption pair is now collector 4e45021d + gate ae69d5dd.
This commit is contained in:
Merlin committed 2026-10-13 01:38:23 +00:00
1 parent 50328d3ccb
commit a6b1ae3434
3 files changed
+321

No files matched your search

+20
View File
@@ -6431,6 +6431,26 @@ Measured by the pipe from `/root/crucible/q45z/` on the fixture `/root/crucible/
**Open and gated beside it:** Q9 (audio handover), Q13 / Q19 (Alexander), Q42 (zephyrus's), Q37 (the pipe's), and the §4 adoption `cp` of the Helm pair — now collector `4e45021d…` + gate `2bbca218…` — which is the same human call. Merlin's §9 follow-on (resolve every path the subject declares **at runtime**, rather than rewriting the literals it declares statically) is unclaimed beside Q46. **Open and gated beside it:** Q9 (audio handover), Q13 / Q19 (Alexander), Q42 (zephyrus's), Q37 (the pipe's), and the §4 adoption `cp` of the Helm pair — now collector `4e45021d…` + gate `2bbca218…` — which is the same human call. Merlin's §9 follow-on (resolve every path the subject declares **at runtime**, rather than rewriting the literals it declares statically) is unclaimed beside Q46.
---
**Q46 (Helm lane) — CLOSED 2026-10-13 by Merlin. An unparseable subject is refused with exit 2 and a dated message; the crashed-run class is measured on all four pre-Q46 gates; no run of the shipped gate leaves a sandbox.**
Deliverable `recoveries/helm-parse-guard.md` (12,284 B / sha256 `19fa3ff6d25c85ce4ae5dfbde5af00b832651f76ca17ca42d3eadf17731f3eba`), scratch tree `/root/crucible/q46/`, harness `run_negatives.py` → `out/Q46-NEGATIVES.json` (58,433 B / `028b6b1c68ab48cd…`, 21 rows, `generated_at 2026-10-13T01:31:42.240730+00:00`) and `run_green.py` → `out/Q46-GREEN.json` + `out/q46-green-{1..5}.{out,err}`. Nothing deployed, no `cp` into `/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret.
- **The pair.** Collector byte-identical to Q45's (`4e45021d…`, 720 / 29,954) — every change is in the gate. Gate **`ae69d5dd735b6f3cfe82b3682b02acf4557b2939d104b9f15b81cffaeef016d4`** (2,032 lines / 109,643 B) against Q45's `2bbca218…` (1,942); `diff -u` = 113 lines = 2 file + 3 hunk + 90 added + 0 removed + 18 context, and 1,942 + 90 − 0 = 2,032 exactly (`out/DIFF-vs-q45.txt`).
- **What changed.** (1) A parse guard after Q35's precondition and before the admission probe: `compile()` the subject's own source; a `SyntaxError` is refused with exit 2 and a **dated** message naming the subject, the failure and its position, with the live snapshot read before and after — and the probe directory and the sandbox do not exist yet, so the refused class creates neither. (2) An **atexit** handler removes the per-invocation sandbox on every exit path (refusal, propagated leg rc, crash) — closing a leak this run measured on the n5 negative. No `check()` is registered: the green count stays **169 passed / 0 failed**.
- **The discriminator, ten invocations, all five gates digest-identical to the revision history** (`9b09f328…`, `c22601ec…`, `18580c10…`, `2bbca218…`): the pipe's C1 fixture crashes **Q39** (rc 1, 575 B, `SyntaxError` at the legs' import of the ORIGINAL subject — no sandbox exists yet), **Q43** (rc 1, 575 B, sandbox-copy import, `legacy/q43/helm-legs-jkij8r1x` left), **Q44** (rc 1, 0 B, `AttributeError: 'NoneType' object has no attribute 'body'` at `:482` — the Q45 record's unexercised §8(a), now exercised, after the sandbox is created), **Q45** (rc 1, 575 B, `legacy/q45/helm-legs-wgn7u3v2` left) — and is **refused by Q46** (rc 2, stdout 0 B / `e3b0c44298fc1c149…`, `Q46 REFUSAL: … does not parse - invalid syntax (noparse-with-markers.py, line 13) (line 13, offset 12) - refused at 2026-10-13T01:31:45.419033+00:00, before the admission probe, before any leg, and before any sandbox is created.`, no traceback, no leftover).
- **The class is split, and both halves measured.** A subject that parses but whose **dependency** does not (`dep-unparseable.py`) is refused by Q39's repair 3 on **all five gates** (rc 2, 0 B stdout, the dated live census in the message) — the residual the parse guard does not have to cover, and repair 3 stays load-bearing.
- **C2 reproduced mechanically.** On the crashed rows the ROOT-anchored `leftover()` reports `[]` while the recursive census reports `legacy/q4X/helm-legs-…`: a gate creates its sandbox beside itself, so a glob anchored at `ROOT` cannot see it. The harness takes both censuses per row; the shipped gate's atexit handler means no run of it leaves one (`leftover []` on every Q46 row).
- **The n5 leak, before/after.** The pre-atexit gate (`1fd889ca…`, the shipped gate minus the handler) run with n5: rc 2, leftover **`work/helm-legs-3fancddl`** (`out/q46-noatexit-n5.{out,err}`); the shipped gate, same fixture: rc 2, leftover `[]`.
- **Green pair, five consecutive invocations** (`run_green.py`, logs on disk): rc 0 each, `169 passed, 0 failed`, `VERDICT: ALL GATES PASS`, 13,312 B stdout, 12.17–12.62 s, live delta 0, held delta 0, no leftover. 169 = 166 + 3, unchanged from Q45 — the guard registers no check and the handler is silent.
- **Live, delta 0 across all 27 invocations** (21 harness rows + 5 green runs + the noatexit run): snapshot 14,039 B / `aa842b0fd88037f8…` / mtime_ns `1791479579475780303` / ctime_ns `1791789490146606356`; held pin 120 B / `5b6d70716153c3f4…` / mtime_ns `1791478687099553071` / ctime_ns `1791846636217687791`. Deployed bin untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`). Final tree: **0 sandboxes**. Host disk 46 GB free of 154 GB (71 %) at run time.
- **§4 consequence:** the adoption pair is now collector `4e45021d…` + gate **`ae69d5dd…`** (superseding `2bbca218…`). Q40's finding stands — the deployed gate `7ed1bca8…` reads `HELM_COLLECTOR` zero times — so the `cp` remains Alexander's and is the only thing that puts Q31–Q46's protection into production.
- **Limits and defects, logged first in the deliverable:** the guard is static (runtime-generated code is out of every static guard's reach); repair 3 remains load-bearing for the dependency class; `atexit` does not run on `os._exit`/`SIGKILL` (coded-not-exercised); my first harness run crashed on a missing fixture path and wrote empty green logs (re-measured with `run_green.py`); in this tree the Q44 crash carries 0 B of stdout where the pipe measured 4 B on the same fixture — a tree-and-clock difference, recorded not reconciled.
- **Q7, measured:** the Helm lane's executable set is empty again after this item — the adoption `cp`, Q9, Q13/Q19 and Q42 are all human- or counterparty-gated. Merlin's Q45 §9 follow-on (resolve every path the subject declares **at runtime**, not only the literals it declares statically) stays open and unclaimed beside the adoption. **No new item opened.**
---
**Q45 closed and verified independently; the live-pin integrity event disclosed; the adoption pair moves to gate `2bbca218…`. 2026-10-12 23:32Z (control plane).** **Q45 closed and verified independently; the live-pin integrity event disclosed; the adoption pair moves to gate `2bbca218…`. 2026-10-12 23:32Z (control plane).**
- **Q45's evidence reproduces on my own reads.** Gate `2bbca218871b…` = **105,477 B / 1,942 lines** against Q44's - **Q45's evidence reproduces on my own reads.** Gate `2bbca218871b…` = **105,477 B / 1,942 lines** against Q44's
+133
View File
@@ -0,0 +1,133 @@
# 2026-10-13 — merlin
> Screen. Every claim below carries a path, a hash, a URL, or a command with its output, or the word
> `unverified`. Nothing secret is printed anywhere in this check-in or in the files it lands.
---
## Run 12 — Q46: the parse guard
### Did
Claimed and finished **Q46 — the parse guard** (Helm lane; the top open item, opened by the pipe on
reply 29's C1). Deliverable: `recoveries/helm-parse-guard.md` (**12,284 B / sha256
`19fa3ff6d25c85ce4ae5dfbde5af00b832651f76ca17ca42d3eadf17731f3eba`**, this host). Queue block
appended to `WORK-QUEUE.md`. Scratch tree `/root/crucible/q46/`; nothing deployed, no `cp` into
`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret.
**The item.** A subject carrying the Q35 markers whose source does not parse was **admitted** by the
Q39 admission and then **crashed** the gate, on every gate from Q39 up. The mechanism, as the pipe
named it: the crash detector tests for the literal `'Traceback (most recent call last)'` and a
top-level `SyntaxError` prints no such line, so "probe rc≠0, wrote nothing, no traceback" is satisfied
by a subject that cannot run at all. What failed was the refusal-path property every gate claims for
every subject — and a crashed run left its sandbox behind.
**The pair.** Collector **byte-identical to Q45's** (`4e45021d80395d3f…`, 720 / 29,954) — every change
is in the gate. Gate **`ae69d5dd735b6f3cfe82b3682b02acf4557b2939d104b9f15b81cffaeef016d4`**
(2,032 lines / 109,643 B) against Q45's `2bbca218…` (1,942); `diff -u` = 113 lines = 2 file + 3 hunk +
**90 added + 0 removed + 18 context**, and 1,942 + 90 − 0 = 2,032 exactly (`out/DIFF-vs-q45.txt`,
counted by `grep -c`, not asserted).
**Two changes.** (1) A parse guard after Q35's source precondition and before the admission probe:
`compile()` the subject's own source; a `SyntaxError` is refused with **exit 2** and a **dated**
message naming the subject, the failure and its position, with the live snapshot read before and after
— and the probe directory and the sandbox do not exist yet, so the refused class creates neither. It
registers **no `check()`**: the green count stays **169 passed / 0 failed**. (2) An **atexit** handler
removes the per-invocation sandbox on every exit path — refusal, propagated leg rc, or crash — closing
a leak this run measured on the n5 negative (before/after below).
**The discriminator, ten invocations, all five gates digest-identical to the revision history**
(`run_negatives.py` → `out/Q46-NEGATIVES.json`, 58,433 B / `028b6b1c68ab48cd…`, 21 rows,
`generated_at 2026-10-13T01:31:42.240730+00:00`):
| fixture | Q39 (`9b09f328…`) | Q43 (`c22601ec…`) | Q44 (`18580c10…`) | Q45 (`2bbca218…`) | Q46 (`ae69d5dd…`) |
|---|---|---|---|---|---|
| `noparse-with-markers.py` | rc 1, 575 B, `SyntaxError` at the legs' import of the **original** subject; no sandbox exists yet | rc 1, 575 B, sandbox-copy import; **`legacy/q43/helm-legs-jkij8r1x`** left | rc 1, 0 B, `AttributeError: 'NoneType' object has no attribute 'body'` at `:482` (the Q45 record's unexercised §8(a), now exercised); **`legacy/q44/helm-legs-8tkclld3`** left | rc 1, 575 B, sandbox-copy import; **`legacy/q45/helm-legs-wgn7u3v2`** left | **rc 2, stdout 0 B (`e3b0c44298fc1c149…`)**, `Q46 REFUSAL: … does not parse - invalid syntax (noparse-with-markers.py, line 13) (line 13, offset 12) - refused at 2026-10-13T01:31:45.419033+00:00, before the admission probe, before any leg, and before any sandbox is created.`; no traceback; no leftover |
| `dep-unparseable.py` (parses; its **dependency** does not) | rc 2 — Q39 repair 3 | rc 2 — repair 3 | rc 2 — repair 3 | rc 2 — repair 3 | rc 2 — repair 3 |
The class is split, and both halves are measured: the subject's own source is refused by the parse
guard; a dependency `SyntaxError` raises **with** a traceback and is refused by repair 3 on all five
gates. Repair 3 stays load-bearing — the guard is the gap between them, not a second layer.
**C2 reproduced mechanically.** On the crashed rows the ROOT-anchored `leftover()` reports `[]` while
the recursive census reports `legacy/q4X/helm-legs-…`: a gate creates its sandbox beside itself, so a
glob anchored at `ROOT` cannot see it. The harness takes both censuses per row; the shipped gate's
atexit handler means no run of it leaves one.
**The n5 leak, before/after.** The pre-atexit gate (`1fd889ca…`, the shipped gate minus the handler)
run with n5: **rc 2, leftover `work/helm-legs-3fancddl`** (`out/q46-noatexit-n5.{out,err}`, measured
live); the shipped gate, same fixture: rc 2, leftover `[]`. The leak is not the item's class (n5
parses) — the criterion generalises to every early exit, and the measurement said the refusal path did
not cover a propagated leg rc.
**Green pair, five consecutive invocations** (`run_green.py`, logs on disk at
`out/q46-green-{1..5}.{out,err}`): rc 0 each, **169 passed / 0 failed**, `VERDICT: ALL GATES PASS`,
13,312 B stdout, 12.17–12.62 s, live delta 0, held delta 0, no leftover.
**Live state, delta 0 across all 27 invocations** (21 harness rows + 5 green runs + the noatexit run):
snapshot `/root/.helm/data/snapshot.json` **14,039 B / sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d`** / mtime
`2026-10-08 17:12:59.475780303 +0000`; held pin `/root/.helm/data/held/stackchain-home.html.sha256`
**120 B / `5b6d70716153c3f4…`**. Deployed bin untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` /
`5c67473d89283c3d…`). Final tree: **0 sandboxes** (`find /root/crucible/q46 -name 'helm-legs-*' -o
-name 'helm-q39-*'` → 0, after `cleanup_final.py`). Host disk **46 GB free of 154 GB (71 % used)**
(`df -h /`).
**Four defects of mine, logged first in the deliverable:** the first harness run crashed on a missing
fixture path (the C1 fixture was at the tree root, not in `fixtures/`); the first harness wrote the
green logs as empty files (re-measured with `run_green.py`, real logs on disk); in this tree the Q44
crash carries **0 B** of stdout where the pipe measured **4 B** on the same fixture — a
tree-and-clock difference, recorded not reconciled; and the n5 leak was found by this run's own
census and fixed here rather than left as somebody else's finding.
### Blocked
Nothing in my lane. Same standing gates, all Alexander's: the **§4 adoption `cp`** — now collector
`4e45021d…` + gate **`ae69d5dd…`** (superseding `2bbca218…`); Q40's finding stands (the deployed gate
`7ed1bca8…` reads `HELM_COLLECTOR` zero times), so none of Q31–Q46's protection is in production until
it lands — Q9 (checksum handover), Q13 (wording fixes), Q19 (wizard onboarding).
### Next
1. **The pipe's verification of this run**, when it lands. Everything is re-runnable from
`/root/crucible/q46/`: `run_negatives.py` and `run_green.py` are self-contained, and
`figures.py` / `print_figs.py` / `show_rows.py` regenerate the record's figures from the JSONs.
2. **Merlin's Q45 §9 follow-on stays open and unclaimed:** resolve every path the subject declares
**at runtime**, rather than rewriting the literals it declares statically. Beside the adoption
decision, that is the one Helm-side follow-on I would name.
3. **Q7, measured: the Helm lane's executable set is empty again.** The adoption, Q9, Q13/Q19 and Q42
are human- or counterparty-gated. **No new item opened** — the next Helm unit is a decision, not a
task.
### Ask
Alexander: nothing blocking. The standing items, consolidated: (a) rule on the **§4 adoption `cp`** —
collector `4e45021d…` + gate `ae69d5dd…`, the pair that makes `python3 helm-selftest.py` green with no
environment and puts Q31–Q46's protection into production (the deployed gate today has no
subject-selection path at all); (b) the **`manifest_match` pin** decision (stale since 2026-10-08; a
static render cannot say so); (c) the **Q13 publication call** (item 13's decided note — escalate, do
not edit the live page — still controls over its `proposed_action`).
---
Landing evidence, appended after the commit and read back over the anonymous raw path
(`https://forge.alexanderwhitestone.com/git/stackchain/troupe-standup/raw/branch/main/<path>`), no
credential — each **HTTP 200** and `cmp` byte-identical to disk:
| file | status / bytes | sha256 |
|---|---|---|
| `recoveries/helm-parse-guard.md` | 200 / 12,284 | `19fa3ff6d25c85ce4ae5dfbde5af00b832651f76ca17ca42d3eadf17731f3eba` |
| `WORK-QUEUE.md` | 200 / (post-append revision) | — |
| `checkins/2026-10-13-merlin.md` | 200 / (this file — self-citation deferred to the committed revision, for the same reason a file cannot hash itself) | — |
Re-measured after the push, from this host: the live snapshot `/root/.helm/data/snapshot.json` is
**14,039 B / sha256 `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime
`2026-10-08 17:12:59.475780303 +0000`** — delta 0 across the whole run — and the deployed
`/root/.helm/bin/` is untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`).
Nothing was deployed, published, rebuilt or re-run beyond the measurements above; `decisions/` and
`inbox/` were not touched; no another-party file was edited; no secret is printed in this check-in or
in the files it lands — the long strings are gate check names, refusal messages, file paths and sha256
digests. This run's commits are authored `Merlin <merlin@agents.invalid>` (`git -c`), so the monitor
sees them as counterparty content per Q36's v1.3 rule.
— Merlin
+168
View File
@@ -0,0 +1,168 @@
> Screen. Every claim below carries a path, a hash, a URL, or a command with its output, or the word
> `unverified`. Nothing secret is printed anywhere in this check-in or in the files it lands.
# Q46 — the parse guard: an unparseable subject is refused, not crashed on
**Item:** Q46 (Helm lane), opened by the pipe 2026-10-12T23:4xZ on reply 29's C1 — the last open executable
item in the queue at the time this run started. **Done as specified:** a subject that does not parse is
refused with exit 2 and a dated message naming the parse failure; the four pre-Q46 gates measured on the
same fixture; the sandbox left by a crashed run either not created or removed; the `leftover()` census
extended past the ROOT glob (C2). Scratch tree `/root/crucible/q46/`; nothing deployed; no `cp` into
`/root/.helm/bin/`; `decisions/` and `inbox/` untouched; no another-party file edited; no secret printed.
## 1. The pair
- Subject collector **`4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c`** (720 lines /
29,954 B) — **byte-identical to Q45's**; every change is in the gate.
- Q45 gate `2bbca218871bc5613e1fe3c4d3a84fe2e6da05ffc526e51b2865a8c8ad00ee5e` (1,942 lines) — the legacy
copy in this tree (`legacy/q45/helm-selftest.py`) is digest-identical to `/root/crucible/q45/helm-selftest.py`,
so the discriminator runs the real Q45 gate, not an approximation of it.
- Q46 gate **`ae69d5dd735b6f3cfe82b3682b02acf4557b2939d104b9f15b81cffaeef016d4`** (2,032 lines /
109,643 B). `diff -u` against the Q45 gate (`out/DIFF-vs-q45.txt`, 113 lines) = **2 file headers + 3
hunk headers + 90 added + 0 removed + 18 context**, and **1,942 + 90 − 0 = 2,032** exactly. The
decomposition is counted by `grep -c` on the diff, not asserted: 91 `^+` minus the `+++` header = 90
added; 1 `^-` is the `---` header; 18 context.
## 2. What changed
**The parse guard** (gate lines 211–263, after Q35's source precondition, before the admission probe):
`compile()` the subject's own source; a `SyntaxError` is refused with **exit 2** and a **dated** message
naming the subject, the parse failure and its position, with the live snapshot read before and after and
the delta printed. The guard sits before the probe directory (`helm-q39-*`) and before the sandbox
(`helm-legs-*`) are created, so the refused class creates neither. It registers **no `check()`**, so the
green count is unchanged at **169 passed / 0 failed**.
**The atexit sandbox cleanup** (gate lines 694–710): an `atexit` handler removes the per-invocation
sandbox on **every** exit path — refusal, a propagated leg rc, or crash. Silent, so the green pair's
stdout/stderr shape is unchanged. This closes a leak this run measured, not hypothesised (§5).
**The class is split, and both halves are measured:**
- a subject whose **own source** does not parse is refused by the parse guard (exit 2, dated);
- a subject that parses but whose **imported dependency** does not parse raises **with a traceback**
and is already refused by Q39's repair 3 ("a crash is not a refusal") — measured on all five gates
(§4, the `dep-unparseable` rows). The parse guard is the gap between them, not a second layer over
repair 3.
## 3. The discriminator — two fixtures, five gates, ten invocations
Fixtures (this tree, `fixtures/`): `noparse-with-markers.py` (Q35 markers present, `def broken(:` at
line 13 — the pipe's C1 fixture, sha256 `7a8872f353419f4a…`), and `dep-unparseable.py` (parses, carries
the markers and a `SNAPSHOT_PATH` assignment, imports the unparseable `broken_dep.py`; sha256
`f621d4718ec83b19…` / `6b8c8a725c23dc07…`). Harness: `run_negatives.py` → `out/Q46-NEGATIVES.json`
(58,433 B / sha256 `028b6b1c68ab48cd3c7a5b1a4b698fbd6ada3a823232106d8ba44cb352fcd392`, 21 rows,
`generated_at 2026-10-13T01:31:42.240730+00:00`). Every row censuses the live snapshot and held
directory on (bytes, sha256, mtime_ns, ctime_ns) before and after; the leftover census is taken **two
ways** — the ROOT-anchored glob and a recursive `rglob`.
| row | rc | stdout | refusal / crash | leftover (recursive) | leftover (ROOT glob) | live delta |
|---|---|---|---|---|---|---|
| q46-dis-q39-noparse | **1** | 575 B | `Traceback… SyntaxError: invalid syntax` (legs import the ORIGINAL subject) | `[]` | `[]` | 0 |
| q46-dis-q43-noparse | **1** | 575 B | `Traceback… SyntaxError` (in-process import of the sandbox copy) | `legacy/q43/helm-legs-jkij8r1x` | `[]` | 0 |
| q46-dis-q44-noparse | **1** | 0 B | `AttributeError: 'NoneType' object has no attribute 'body'` (`_q39_tree.body`, gate `:482`) | `legacy/q44/helm-legs-8tkclld3` | `[]` | 0 |
| q46-dis-q45-noparse | **1** | 575 B | `Traceback… SyntaxError` (sandbox-copy import) | `legacy/q45/helm-legs-wgn7u3v2` | `[]` | 0 |
| q46-dis-q46-noparse | **2** | 0 B | **`Q46 REFUSAL: … does not parse - invalid syntax (noparse-with-markers.py, line 13) (line 13, offset 12) - refused at 2026-10-13T01:31:45.419033+00:00, before the admission probe, before any leg, and before any sandbox is created.`** | `[]` | `[]` | 0 |
| q46-dis-{q39,q43,q44,q45,q46}-dep-unparseable | **2** (all five) | 0 B | `Q39 REFUSAL: … a bare run against the sentinel exited rc=1 (stderr: Traceback …` — repair 3, dated live census in the message | `[]` | `[]` | 0 |
The four crashed rows are the defect the item names, reproduced on gates digest-identical to the
deployed-revision history (`9b09f328…`, `c22601ec…`, `18580c10…`, `2bbca218…`). The Q44 row's crash site
is the unguarded `_q39_tree.body` access the Q45 record reported as unexercised (§8(a)) — now exercised,
and it happens **after** the sandbox is created (`legacy/q44/helm-selftest.py:462` mkdtemp, `:482` the
crash), which is why a sandbox is left. The Q39 row leaves none because Q39's legs run the **original**
subject and crash before any sandbox exists — the pre-Q43 shape. **C2 is reproduced mechanically on the
crashed rows:** the ROOT-anchored glob reports `[]` while the recursive census reports the sandbox in the
gate's own directory (`legacy/q4X/`), because a gate creates its sandbox beside itself.
## 4. The green pair — five consecutive invocations, logs on disk
`run_green.py` → `out/Q46-GREEN.json` and `out/q46-green-{1..5}.{out,err}`:
| run | rc | passed | stdout | seconds | live delta | leftover |
|---|---|---|---|---|---|---|
| 1 | 0 | 169 | 13,312 B | 12.62 | 0 | `[]` |
| 2 | 0 | 169 | 13,312 B | 12.19 | 0 | `[]` |
| 3 | 0 | 169 | 13,312 B | 12.22 | 0 | `[]` |
| 4 | 0 | 169 | 13,312 B | 12.32 | 0 | `[]` |
| 5 | 0 | 169 | 13,312 B | 12.17 | 0 | `[]` |
Green log digests (stdout): `c02adfda1704eea0…` / `10972472d772db66…` / `51b42c964868bed8…` /
`389a04e41d2fd1fd…` / `deef0ded4a09ac77…` (harness run) — the `run_green.py` re-measurement writes the
same bodies to disk. **169 = 166 + 3, unchanged from Q45**: the parse guard registers no check, and the
atexit handler prints nothing, so the green pair's shape is byte-stable against Q45's.
## 5. The n5 sandbox leak — found by measurement, closed by the atexit handler
The five Q39 negative fixtures under the Q46 gate (regression control): **all five rc 2, live delta 0,
held delta 0, no leftover** (`q46-neg-n1…n5` rows). The `n5` row is the interesting one: n5 is
**admitted** (Q45 redirects its live literal), its legs run, and it ends rc 2 through the collector's own
destination rule (`refusing to write the live snapshot <sandbox>/data/snapshot.json without --deploy`,
`out/q46-noatexit-n5.err`) — and on the **pre-atexit** gate it **left its sandbox behind**.
- Pre-atexit gate (the Q46 gate minus the atexit block), sha256
`1fd889ca95c81910aead88c0aa9d445cb91ffd1caecc411958160ad3b04a4260`, run with n5: **rc 2**, leftover
**`work/helm-legs-3fancddl`** (`out/q46-noatexit-n5.out` / `.err`, the leak measured live).
- Post-atexit gate (the shipped `ae69d5dd…`), the same fixture in the harness: **rc 2, leftover `[]`**.
The leak is not the item's class (n5 parses), but the item's done criterion — "the sandbox a crashed run
leaves behind is either not created or removed by the refusal path" — generalises to every early exit,
and the measurement said the refusal path did not cover a propagated leg rc. The atexit handler is the
fix, and the before/after is measured on the same fixture, same gate, same host.
## 6. Live state — delta 0 across every invocation
Across all 21 harness rows, the 5 green runs and the noatexit run (`out/Q46-NEGATIVES.json`,
`out/Q46-GREEN.json`, re-measured after the runs):
- live snapshot `/root/.helm/data/snapshot.json`: **14,039 B / sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime
`2026-10-08 17:12:59.475780303 +0000`** (mtime_ns `1791479579475780303`, ctime_ns `1791789490146606356`
— the ctime is the Q45 restore trace, unchanged);
- live held pin `/root/.helm/data/held/stackchain-home.html.sha256`: **120 B / sha256
`5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297`** / mtime_ns `1791478687099553071`,
ctime_ns `1791846636217687791` (the 2026-10-12T23:10:36Z restore trace).
Deployed bin untouched: `helm-collect.py` `ee0b36a82f23fcf3…`, `helm-selftest.py` `7ed1bca8f9da624f…`,
`helm-render.py` `5c67473d89283c3d…`. Nothing deployed, nothing published. Final tree state: **0
sandboxes** (`find /root/crucible/q46 -name 'helm-legs-*' -o -name 'helm-q39-*'` → 0), fixture
destination directories removed by `cleanup_final.py`. Host disk **46 GB free of 154 GB (71 % used)**
(`df -h /`, run time).
## 7. Limits, stated not buried
1. The guard is **static**: it compiles the subject's own source text. A `SyntaxError` inside code the
subject *generates* at runtime is out of its reach — and out of every static guard's; nothing in this
gate's path executes generated code.
2. The dependency class is **covered by repair 3, not by this guard** — measured on all five gates
(§3). If repair 3 were ever removed, the dependency class would crash again; the guard does not
subsume it.
3. `atexit` does not run on `os._exit` or `SIGKILL`. Coded-not-exercised; the pre-atexit leak is the
measured failure mode it closes, and both SIGKILL classes are outside the gate's control anyway.
4. The guard protects the gate's legs, not a direct CLI run of a pre-gate collector (the Q35 limit,
unchanged).
5. The discriminator's legacy gates run from copies in this tree; their sandboxes land beside the
copies, which is exactly C2's shape and is why the recursive census is the honest one.
## 8. Defects of mine, logged first
1. The first harness run crashed on a missing fixture path — the C1 fixture was at the tree root, not in
`fixtures/` (`FileNotFoundError`, `run_negatives.py:203`). Logged, moved, re-run.
2. The first harness wrote the green logs as **empty files** (the stdout lived only in the JSON).
Re-measured with `run_green.py`, which writes the real stdout and stderr per run (§4).
3. In this tree the Q44 crash is **0 B of stdout** where the pipe measured **4 B** on the same fixture —
a tree-and-clock difference (their fixture path is longer), recorded, not reconciled. The crash site,
the rc and the leftover agree.
4. The n5 sandbox leak (§5) was found by this run's own census, on a class the item does not name, and
fixed here rather than left as somebody else's finding.
## 9. §4 consequence and Q7, measured
**The adoption pair is now collector `4e45021d…` + gate `ae69d5dd…`** (superseding Q45's `2bbca218…`).
Q40's finding is unchanged and is why the `cp` matters: the deployed gate `7ed1bca8…` reads
`HELM_COLLECTOR` **zero** times, so none of Q31–Q46's protection is in production until the pair lands.
Alexander's call; nothing in this run touched `/root/.helm/bin/`.
**Q7:** after this item the Helm lane's executable set is empty again — the adoption `cp` is Alexander's,
Q9 waits on the checksum handover, Q13/Q19 are Alexander's, Q42 is zephyrus's, Q37 is the pipe's.
Merlin's own §9 follow-on from Q45 (resolve every path the subject declares **at runtime**, rather than
rewriting the literals it declares statically) remains open and unclaimed beside the adoption. **No new
item opened** — the next Helm unit is a decision, not a task.