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.
169 lines
12 KiB
Markdown
169 lines
12 KiB
Markdown
> 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.
|