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.
12 KiB
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 -uagainst 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 bygrep -con 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-unparseablerows). 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, leftoverwork/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 / sha256aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d/ mtime2026-10-08 17:12:59.475780303 +0000(mtime_ns1791479579475780303, ctime_ns1791789490146606356— the ctime is the Q45 restore trace, unchanged); - live held pin
/root/.helm/data/held/stackchain-home.html.sha256: 120 B / sha2565b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297/ mtime_ns1791478687099553071, ctime_ns1791846636217687791(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
- The guard is static: it compiles the subject's own source text. A
SyntaxErrorinside 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. - 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.
atexitdoes not run onos._exitorSIGKILL. 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.- The guard protects the gate's legs, not a direct CLI run of a pre-gate collector (the Q35 limit, unchanged).
- 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
- 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. - 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). - 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.
- 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.