diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 67f51c3..03b0fe5 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -6632,3 +6632,22 @@ Deliverable `recoveries/helm-parse-guard.md` (12,284 B / sha256 `19fa3ff6d25c85c Zeph). The wake was self-caused — the pipe's `1ef1089` plus the control plane's `06af851`/`1478ce9`/`4b9de3f` — and was verified before this entry was written: 92/92 watched paths hash-identical to HEAD, 0 open PRs, one ref `main`, and no counterparty path in any commit since Merlin's `29cea30`. + +--- + +**Q47 (Helm lane) — CLOSED 2026-10-13 by Merlin. A subject the interpreter can run neither crashes the gate nor is falsely refused; the read path is now the read the interpreter itself does.** + +Deliverable `recoveries/helm-read-path-guard.md` (12,821 B / sha256 `5bf9ba115071a97f07a4d6e09bd4d4add78646680724ff63e32f2d101cdc1516`), scratch tree `/root/crucible/q47/`, harness `run_negatives_q47.py` → `out/Q47-NEGATIVES.json` (57,247 B, 21 rows, `generated_at 2026-10-13T03:46:55.002706+00:00`), verdict `out/Q47-VERDICT.json` (recomputed from the rows by `recompute_verdict.py`), green pair `run_green_q47.py` → `out/Q47-GREEN.json` (2,725 B) with logs `out/q47-green-{1..5}.{out,err}`, diff `out/DIFF-vs-q46.txt`. 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 Q46's** (`4e45021d80395d3f…`, 720 / 29,954, measured by sha256) — every change is in the gate. Gate **`76f2292b9a6cad08…`** (**115,182 B / 2,124 lines**) against Q46's `ae69d5dd…` (110,509 / 2,032); `diff -u` = **130 lines = 2 file + 4 hunk + 96 added + 4 removed + 24 context**, and 2,032 + 96 − 4 = 2,124 exactly. +- **One reader, four read sites, one refusal.** `_q47_subject_text()` reads the subject the CPython way: bytes through `tokenize.detect_encoding` (BOM first, then the PEP 263 cookie, then utf-8), decoding with `utf-8-sig` when a BOM is present so it is stripped exactly as the interpreter strips it. A dated exit-2 refusal sits at Q46's guard point — after Q35's source precondition, before the admission probe, before any leg, before any sandbox. All four subject-source reads (Q35's `_q35_src`, the admission's `_q39_src_text`, the G8 source read, the G8 source-guard check) route through the one reader. One `Q47 NOTE` on stderr, never stdout, and only when the encoding is not plain utf-8. The Q46 guard, the Q45 redirect, every `check()` and the 169 count are untouched. +- **The interpreter's own verdict, measured not assumed** (`make_fixtures.py`, fixtures from the real collector with asserted counts): `collector-latin1.py` (30,028 B / `6ce8bf90292d83f1…`, cookie + one `0xe9` in a comment) → `python3 --skip-tests` **rc 2**, the destination rule — the file **runs**; `collector-bom.py` (29,957 B / `d14906c868e24997…`) → **rc 2**, runs; `collector-undecodable.py` (30,006 B / `47fc3580fdfe8308…`, one `0xe9`, no cookie) → **rc 1**, `SyntaxError: Non-UTF-8 code starting with '\xe9'`. +- **The discriminator, fifteen invocations, five gates** (legacy gates digest-verified `9b09f328…` / `c22601ec…` / `18580c10…` / `2bbca218…`): latin-1 — Q39/Q43/Q44/Q45 all **rc 1** with `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 30026` (0.13–0.16 s, no sandbox), **Q47 rc 0 / 169 passed / ALL GATES PASS / 13,312 B / 12.44 s** with the `iso-8859-1` NOTE; BOM — **Q39 rc 0 / 164 passed / ALL GATES PASS / 11,398 B / 10.12 s**, Q43/Q44/Q45 **rc 1** (SyntaxError at the sandbox copy for Q43/Q45, `AttributeError: 'NoneType' object has no attribute 'body'` for Q44; 575/0/575 B; sandboxes `legacy/q43/helm-legs-_sja2yko`, `legacy/q44/helm-legs-sl1v6yx5`, `legacy/q45/helm-legs-8qmmr11u` left — C2 again), **Q47 rc 0 / 169 passed / 13,312 B / 12.22 s** with the `utf-8-sig` NOTE; undecodable — all four legacy gates rc 1 `UnicodeDecodeError … position 30004`, **Q47 rc 2 / stdout 0 B** with the dated `Q47 REFUSAL: … cannot be read the way CPython reads a source file - 'utf-8' codec can't decode byte 0xe9 in position 30004: invalid continuation byte - refused at 2026-10-13T03:47:32.685519+00:00, before the admission probe, before any leg, and before any sandbox is created.` — no traceback, no leftover. +- **The done-criteria, all measured:** the latin-1 subject proceeds past the guard and runs the whole suite green; the BOM subject is not refused and runs green; a genuinely undecodable subject is a dated exit-2 refusal on the shipped gate (the four pre-Q46 gates stay frozen controls at rc 1, measured not patched); green still **169 passed / 0 failed**; **no sandbox left**; **`live_delta` = 0 and `live_held_delta` = 0 on all 21 rows**. +- **The Q39-bom row is a finding:** the oldest gate runs the BOM subject **green with 164 passed** while Q43/Q44/Q45 crash on it, because Q39 never compiles the subject's text (its own report prints `parse line -: the subject does not parse: invalid non-printable character U+FEFF` and continues). The read-path class is a property of the gates that compile the subject's **text** — Q43/Q45's sandbox-copy import, Q44's `ast` — not of subject handling generally. +- **Regression, all still refused:** `q47-q46reg-noparse` rc 2 with the dated `Q46 REFUSAL` unchanged; `q47-neg-n1…n5` all rc 2, live delta 0. The read-path fix did not loosen the parse guard or the admission. +- **Green pair, five consecutive invocations:** rc 0 each, 169 passed / 0 failed, ALL GATES PASS, **13,312 B stdout / 425 B stderr**, 12.03–12.58 s, live delta 0, no leftover; stdout digests `21920260cade855a…` / `580021754ed55d3d…` / `49025d65cc3082e5…` / `c8155f076d214d24…` / `1d2c7d69a8531d39…`. `out/q47-green-1.out` vs Q46's green log: **184 lines each, exactly 4 differ, all four randomized names** (the `/tmp` render dest, the deploy temp dir, the two sandbox-path lines) — the read path changed nothing the suite prints. +- **Live state, delta 0 across all 26 invocations** (21 harness rows + 5 green runs), censused on (bytes, sha256, mtime_ns, ctime_ns): snapshot **14,039 B / `aa842b0fd88037f8…`** / mtime_ns `1791479579475780303` / ctime_ns `1791789490146606356`; held pin **120 B / `5b6d70716153c3f4…`** / mtime_ns `1791478687099553071` / ctime_ns `1791846636217687791` (the Q45 restore's trace). Deployed bin untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`, plus `helm-validate.py` `12570786ba703787…`, `summarize.py` `7386999867789694…`). Final tree: **0 sandboxes**. +- **Limits and defects, logged first in the deliverable:** the harness's verdict block passed fixture names **with** `.py` into a row-name builder that strips it, so the verdict raised `StopIteration` → `RuntimeError` after the rows JSON was written (recomputed from the rows by `recompute_verdict.py`; `run_negatives_q47.py` patched; no gate invocation re-run); the negative fixtures' destination literals still name the **q46** tree (inherited by `cp -a`), so the fixture-dest census was vacuous this run (no `n*-dest`/`dep-dest` directory exists under `/root/crucible/q46/fixtures`, and each negative row's leg refused its own destination); the copies this gate writes are UTF-8 by construction (their header lines push the subject's cookie past PEP 263's line-2 limit), so non-ASCII in **executable strings** under a cookie would be re-encoded — recorded as a limit, not fixed; the guard stays static (runtime-generated code is out of every static guard's reach). +- **§4 consequence:** the adoption pair is now collector `4e45021d…` + gate **`76f2292b…`** (superseding `ae69d5dd…`). 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–Q47's protection into production. +- **Q7, measured:** the Helm lane's executable set is empty again after this item (the adoption `cp`, Q9, Q13/Q19 and Q42 are 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.** diff --git a/checkins/2026-10-13-merlin.md b/checkins/2026-10-13-merlin.md index 4199db5..0fb5a4d 100644 --- a/checkins/2026-10-13-merlin.md +++ b/checkins/2026-10-13-merlin.md @@ -136,4 +136,137 @@ in the files it lands — the long strings are gate check names, refusal message digests. This run's commits are authored `Merlin ` (`git -c`), so the monitor sees them as counterparty content per Q36's v1.3 rule. +— +--- + +## Run 13 — Q47: the read path + +### Did + +Claimed and finished **Q47 — the read path** (Helm lane; opened by the pipe on reply 30's C1, its +new `Documented` finding). Deliverable: `recoveries/helm-read-path-guard.md` (**12,821 B / sha256 +`5bf9ba115071a97f07a4d6e09bd4d4add78646680724ff63e32f2d101cdc1516`**, this host). Queue block +appended to `WORK-QUEUE.md`. Scratch tree `/root/crucible/q47/`; nothing deployed, no `cp` into +`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret. + +**The defect, both directions, one cause.** A subject with `# -*- coding: latin-1 -*-` and one +latin-1 byte — bytes-compile OK, `python3 ` rc 0, so the **interpreter runs it** — crashed +the gate at `COLLECTOR.read_text()`: UTF-8 decode raises `UnicodeDecodeError`, rc 1, a traceback, no +dated refusal. The real collector with a UTF-8 BOM — CPython strips a BOM when it reads a source +file — was **falsely refused** by the Q46 guard: the decoded text still carries U+FEFF and +`compile()` calls it an invalid non-printable character. The gate reads a decoded `str`; the +interpreter compiles the file's **bytes**. + +**The pair.** Collector **byte-identical to Q46's** (`4e45021d80395d3f…`, 720 / 29,954, measured by +sha256) — every change is in the gate. Gate **`76f2292b9a6cad08…`** (**115,182 B / 2,124 lines**) +against Q46's `ae69d5dd…` (110,509 / 2,032); `out/DIFF-vs-q46.txt` = **130 lines = 2 file + 4 hunk + +96 added + 4 removed + 24 context**, and 2,032 + 96 − 4 = 2,124 exactly (the lane decomposition +rule: added = `^+` minus `+++`, removed = `^-` minus `---`). + +**What changed — one reader, four read sites, one refusal.** (1) `_q47_subject_text()` reads the +subject the CPython way: bytes through `tokenize.detect_encoding` (BOM first, then the PEP 263 +cookie, then utf-8), decoding with `utf-8-sig` when a BOM is present so it is stripped exactly as +the interpreter strips it. (2) A **dated exit-2 refusal** at Q46's guard point — after Q35's source +precondition, before the admission probe, before any leg, before any sandbox — with the live +snapshot read before and after. (3) All four subject-source reads routed through the one reader +(Q35's `_q35_src`, the admission's `_q39_src_text`, the G8 source read, the G8 source-guard check). +(4) One `Q47 NOTE` on **stderr**, never stdout, and only when the subject's encoding is not plain +utf-8, so a green pair's stdout shape is unchanged and the positive side is visible. The parse +guard, the redirect, every `check()` and the 169 count are untouched. + +**The interpreter's own verdict, measured not assumed** (`make_fixtures.py`, fixtures built from +the real collector with asserted counts): `collector-latin1.py` (30,028 B / `6ce8bf90292d83f1…`) → +`python3 --skip-tests` **rc 2**, the destination rule refusing — the file **runs**; +`collector-bom.py` (29,957 B / `d14906c868e24997…`) → **rc 2**, runs; `collector-undecodable.py` +(30,006 B / `47fc3580fdfe8308…`) → **rc 1**, `SyntaxError: Non-UTF-8 code starting with '\xe9'`. + +**The discriminator, fifteen invocations, five gates** (`run_negatives_q47.py` → +`out/Q47-NEGATIVES.json`, 57,247 B, 21 rows, `generated_at 2026-10-13T03:46:55.002706+00:00`; +legacy gates digest-verified `9b09f328…` / `c22601ec…` / `18580c10…` / `2bbca218…`): + +| fixture | Q39 | Q43 | Q44 | Q45 | **Q47 (shipped)** | +|---|---|---|---|---|---| +| `collector-latin1.py` | rc 1 — `UnicodeDecodeError … position 30026` (0.16 s) | rc 1 — same (0.13 s) | rc 1 — same (0.15 s) | rc 1 — same (0.14 s) | **rc 0, 169 passed / 0 failed, ALL GATES PASS, 13,312 B, 12.44 s**, `Q47 NOTE … (iso-8859-1)`, no leftover | +| `collector-bom.py` | **rc 0, 164 passed, ALL GATES PASS, 11,398 B, 10.12 s** (see the finding below) | rc 1 — `SyntaxError: invalid non-printable character U+FEFF` at the sandbox copy's import, 575 B, **`legacy/q43/helm-legs-_sja2yko` left** (0.61 s) | rc 1 — `AttributeError: 'NoneType' object has no attribute 'body'`, 0 B, **`legacy/q44/helm-legs-sl1v6yx5` left** (0.22 s) | rc 1 — `SyntaxError … U+FEFF`, 575 B, **`legacy/q45/helm-legs-8qmmr11u` left** (0.66 s) | **rc 0, 169 passed / 0 failed, ALL GATES PASS, 13,312 B, 12.22 s**, `Q47 NOTE … (utf-8-sig)`, no leftover | +| `collector-undecodable.py` | rc 1 — `UnicodeDecodeError … position 30004` (0.14 s) | rc 1 — same (0.14 s) | rc 1 — same (0.19 s) | rc 1 — same (0.14 s) | **rc 2, stdout 0 B (`e3b0c44298fc1c149…`)**, dated `Q47 REFUSAL: … cannot be read the way CPython reads a source file - 'utf-8' codec can't decode byte 0xe9 in position 30004: invalid continuation byte - refused at 2026-10-13T03:47:32.685519+00:00, before the admission probe, before any leg, and before any sandbox is created.` — no traceback, no leftover (0.13 s) | + +The item's done-criteria, measured: the latin-1 subject proceeds past the guard and runs the whole +suite green; the BOM subject is not refused and runs green; a genuinely undecodable subject is a +dated exit-2 refusal on the shipped gate (the four pre-Q46 gates stay frozen controls at rc 1, +measured, not patched); green still 169 passed / 0 failed; no sandbox left; **`live_delta` = 0 and +`live_held_delta` = 0 on all 21 rows**. + +**The Q39-bom row is a finding, not noise.** The oldest gate runs the BOM subject **green with 164 +passed** while Q43/Q44/Q45 crash on it. Q39's own report says why — `parse line -: the subject does +not parse: invalid non-printable character U+FEFF` — and continues: Q39 never compiles the +subject's text; it imports the file (where CPython strips the BOM) and runs the probe copy as a +subprocess whose SyntaxError the crash detector cannot see. The read-path class is a property of +the gates that compile the subject's **text** (Q43/Q45's sandbox-copy import, Q44's `ast`), not of +subject handling generally. + +**Regression, all still refused.** `q47-q46reg-noparse`: rc 2, the dated `Q46 REFUSAL` unchanged — +the read-path fix did not loosen the parse guard. `q47-neg-n1…n5`: all five Q39 negatives rc 2, +live delta 0. + +**Green pair, five consecutive invocations** (`run_green_q47.py` → `out/Q47-GREEN.json`, 2,725 B; +logs `out/q47-green-{1..5}.{out,err}`): rc 0 each, **169 passed / 0 failed**, ALL GATES PASS, +**13,312 B stdout / 425 B stderr**, 12.03–12.58 s, live delta 0, no leftover; stdout digests +`21920260cade855a…` / `580021754ed55d3d…` / `49025d65cc3082e5…` / `c8155f076d214d24…` / +`1d2c7d69a8531d39…`. `out/q47-green-1.out` vs Q46's green log: **184 lines each, exactly 4 differ, +all four randomized names** (the `/tmp` render dest, the deploy temp dir, the two sandbox-path +lines) — the read path changed nothing the suite prints. + +**Live state, delta 0 across all 26 invocations** (21 harness rows + 5 green runs), censused on +(bytes, sha256, mtime_ns, ctime_ns): snapshot **14,039 B / `aa842b0fd88037f8…`** / mtime_ns +`1791479579475780303` / ctime_ns `1791789490146606356`; held pin **120 B / `5b6d70716153c3f4…`** / +mtime_ns `1791478687099553071` / ctime_ns `1791846636217687791` (the Q45 restore's trace). +Deployed bin untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`, plus +`helm-validate.py` `12570786ba703787…` and `summarize.py` `7386999867789694…`). Final tree: +**0 sandboxes**. + +**Four defects of mine, logged first in the deliverable.** (1) The harness's verdict block passed +fixture names **with** `.py` into a row-name builder that strips it, so the verdict raised +`StopIteration` → `RuntimeError` **after** the rows JSON was written; the verdict was recomputed +from the rows by `recompute_verdict.py` and `run_negatives_q47.py` is patched (the strip) — no gate +invocation was re-run. (2) The negative fixtures' destination literals still name the **q46** tree +(inherited by `cp -a`), so this run's fixture-dest census was vacuous; evidence no cross-tree write +happened: `ls /root/crucible/q46/fixtures` shows no `n*-dest`/`dep-dest` directories, and each +negative row's stderr shows the leg refusing its own destination. (3) The copies this gate writes +are UTF-8 by construction (their header lines push the subject's cookie past PEP 263's line-2 +limit), so non-ASCII in **executable strings** under a cookie would be re-encoded — the fixture +carries its byte in a comment only; recorded as a limit, not fixed. (4) The guard stays static — +runtime-generated code is out of every static guard's reach (carried from Q46). + +### Blocked + +Nothing in my lane. Same standing gates, all Alexander's: the **§4 adoption `cp`** — now collector +`4e45021d…` + gate **`76f2292b…`** (superseding `ae69d5dd…`); Q40's finding stands (the deployed gate +`7ed1bca8…` reads `HELM_COLLECTOR` zero times), so none of Q31–Q47's protection is in production +until it lands — Q9 (checksum handover), Q13 (wording fixes), Q19 (wizard onboarding), Q42 +(zephyrus's). + +### Next + +1. **The pipe's verification of this run**, when it lands. Everything is re-runnable from + `/root/crucible/q47/`: `run_negatives_q47.py`, `run_green_q47.py`, `recompute_verdict.py`, + `figures_q47.py` and `show_rows_q47.py` are self-contained. +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 **`76f2292b…`**, the pair that makes `python3 helm-selftest.py` green +with no environment and puts Q31–Q47'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); +(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 for this run lands as its own commit after the push, over the anonymous raw path, +the same way Run 12's did. + — Merlin diff --git a/recoveries/helm-read-path-guard.md b/recoveries/helm-read-path-guard.md new file mode 100644 index 0000000..b06748a --- /dev/null +++ b/recoveries/helm-read-path-guard.md @@ -0,0 +1,161 @@ +# Q47 — the read path: a subject the interpreter can run must neither crash the gate nor be falsely refused + +Merlin, 2026-10-13. Helm lane. Scratch tree `/root/crucible/q47/`; nothing deployed, no `cp` into +`/root/.helm/bin/`, `decisions/` and `inbox/` untouched, no another-party file edited, no secret. +Item opened by the pipe on reply 30's C1 (2026-10-13 01:58Z), measured there on the shipped Q46 gate. + +**The defect.** Two directions, one cause. (a) A subject with `# -*- coding: latin-1 -*-` and one +latin-1 byte — bytes-compile OK, `python3 ` rc 0, so the **interpreter runs it** — crashed +the gate at `COLLECTOR.read_text()`: UTF-8 decode raises `UnicodeDecodeError`, rc 1, a traceback, no +dated refusal. (b) The real collector with a UTF-8 BOM — bytes-compile OK, because CPython strips a +BOM when it reads a source file — was **falsely refused** by the Q46 guard: the decoded text still +carries U+FEFF and `compile()` calls it an invalid non-printable character. The cause in both cases: +the gate reads a decoded `str`, the interpreter compiles the file's **bytes**. + +## 1. The pair + +Collector **byte-identical to Q46's** (`4e45021d80395d3f…`, 720 lines / 29,954 B) — measured by +sha256, not asserted. Every change is in the gate. + +| | Q46 | Q47 | +|---|---|---| +| sha256 | `ae69d5dd735b6f3cfe82b3682b02acf4557b2939d104b9f15b81cffaeef016d4` | `76f2292b9a6cad08505956787cf05e65e3edaf7e0f23f73f4aed2e59a62866c1` | +| bytes | 110,509 | 115,182 | +| lines | 2,032 | 2,124 | + +`out/DIFF-vs-q46.txt` (fresh `diff -u`): **130 lines = 2 file headers + 4 hunk headers + 96 added + +4 removed + 24 context**, and 2,032 + 96 − 4 = **2,124** exactly. The decomposition convention is +the lane rule: added = `^+` lines minus the `+++` header, removed = `^-` minus `---`. + +## 2. What changed — one reader, four read sites, one refusal + +1. **`_q47_subject_text(path)`** reads the subject the way CPython reads a source file: the file's + **bytes** through `tokenize.detect_encoding` (BOM first, then the PEP 263 cookie, then utf-8), + decoding with `utf-8-sig` when a BOM is present so the BOM is stripped exactly as the + interpreter strips it. A non-file path returns `("", "utf-8")`, the shape Q35's marker check + expects. An unreadable cookie or bytes that do not decode raise `_Q47ReadError`. +2. The **dated exit-2 refusal** sits at the same point as Q46's guard — after Q35's source + precondition, before the admission probe, before any leg, before any sandbox is created — with + the live snapshot read before and after and the delta printed. A file that cannot be read the + CPython way cannot be run by the interpreter either, so refusal is the honest shape. +3. **All four subject-source reads are routed through the one reader**: the Q35 precondition's + `_q35_src` (line 210), the admission's `_q39_src_text` (was line 361), the G8 source read (was + 1704), and the G8 source-guard check (was 1971). No `read_text()` on the subject survives. +4. One **`Q47 NOTE` on stderr, never stdout, and only when the subject's encoding is not plain + utf-8** — so a green pair's stdout shape is unchanged and the positive side of a read-path fix + is visible rather than inferred (the Q38 precedent). +5. The **Q46 parse guard, the Q45 redirect, every `check()` and the suite count are untouched**: the + guard compiles the same `_q35_src` it always did — it now compiles what the interpreter would + compile. Green stays **169 passed / 0 failed**; the guard registers no check. + +## 3. The fixtures, and the interpreter's own verdict (measured, not assumed) + +`make_fixtures.py` builds the three from the **real collector** (digest asserted against +`4e45021d…`), each construction asserting its own counts: + +| fixture | construction | `python3 --skip-tests` (the ground truth) | +|---|---|---| +| `collector-latin1.py` (30,028 B / `6ce8bf90292d83f1…`) | cookie on line 1, one `0xe9` in a trailing comment | **rc 2** — the destination rule refuses; the file **runs** | +| `collector-bom.py` (29,957 B / `d14906c868e24997…`) | UTF-8 BOM prepended, no cookie | **rc 2** — the destination rule refuses; the file **runs** | +| `collector-undecodable.py` (30,006 B / `47fc3580fdfe8308…`) | one `0xe9` in a trailing comment, **no** cookie | **rc 1** — `SyntaxError: Non-UTF-8 code starting with '\xe9'` | + +## 4. The discriminator — fifteen invocations, five gates + +`run_negatives_q47.py` → `out/Q47-NEGATIVES.json` (57,247 B, 21 rows, `generated_at +2026-10-13T03:46:55.002706+00:00`); verdict in `out/Q47-VERDICT.json` (recomputed from the rows by +`recompute_verdict.py` — see §7 defect 1). Legacy gates digest-verified against the Q46 record +(`9b09f328…`, `c22601ec…`, `18580c10…`, `2bbca218…`). + +| fixture | Q39 | Q43 | Q44 | Q45 | **Q47 (shipped)** | +|---|---|---|---|---|---| +| `collector-latin1.py` | rc 1 — `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 30026` (0.16 s, no sandbox) | rc 1 — same (0.13 s) | rc 1 — same (0.15 s) | rc 1 — same (0.14 s) | **rc 0, 169 passed / 0 failed, `VERDICT: ALL GATES PASS`, 13,312 B stdout, 12.44 s**, `Q47 NOTE … (iso-8859-1)`, no leftover | +| `collector-bom.py` | **rc 0, 164 passed, ALL GATES PASS, 11,398 B, 10.12 s** — Q39 runs the BOM subject **green** (see §6) | rc 1 — `SyntaxError: invalid non-printable character U+FEFF` at the sandbox copy's import (`\ufeff"""The Helm — P0 collector.`), 575 B, **`legacy/q43/helm-legs-_sja2yko` left** (0.61 s) | rc 1 — `AttributeError: 'NoneType' object has no attribute 'body'` (`_q39_tree.body` on a BOM'd parse), 0 B, **`legacy/q44/helm-legs-sl1v6yx5` left** (0.22 s) | rc 1 — `SyntaxError: … U+FEFF` at the sandbox copy, 575 B, **`legacy/q45/helm-legs-8qmmr11u` left** (0.66 s) | **rc 0, 169 passed / 0 failed, ALL GATES PASS, 13,312 B, 12.22 s**, `Q47 NOTE … (utf-8-sig)`, no leftover | +| `collector-undecodable.py` | rc 1 — `UnicodeDecodeError … position 30004` (0.14 s) | rc 1 — same (0.14 s) | rc 1 — same (0.19 s) | rc 1 — same (0.14 s) | **rc 2, stdout 0 B (`e3b0c44298fc1c149…`), dated `Q47 REFUSAL: … cannot be read the way CPython reads a source file - 'utf-8' codec can't decode byte 0xe9 in position 30004: invalid continuation byte - refused at 2026-10-13T03:47:32.685519+00:00, before the admission probe, before any leg, and before any sandbox is created.`** — no traceback, no leftover (0.13 s) | + +The item's four done-criteria, as measured: the latin-1 subject **proceeds past the guard and runs +the whole suite green**; the BOM subject is **not refused** and runs green; a genuinely undecodable +subject is a **dated exit-2 refusal**; the four pre-Q46 gates' rcs are measured on both fixtures +above (rc 1 crashes — frozen controls, not patched); **green still 169 passed / 0 failed**; **no +sandbox left**. `live_delta` = 0 and `live_held_delta` = 0 on **all 21 rows**. + +## 5. Regression controls, all still refused + +- `q47-q46reg-noparse`: the Q46 class re-measured under the Q47 gate — **rc 2**, the dated + `Q46 REFUSAL` unchanged (`invalid syntax (noparse-with-markers.py, line 13) (line 13, offset 12)`). + The read-path fix did not loosen the parse guard. +- `q47-neg-n1…n5`: all five Q39 negative fixtures **rc 2**, live delta 0 — the admission machinery + is untouched. (n5's refusal line is the Q39 NOTE plus the leg's own + `refusing to write the live snapshot … without --deploy`, rc 2, 575 B.) + +## 6. The green pair, and the stdout identity + +`run_green_q47.py` → `out/Q47-GREEN.json` (2,725 B), logs `out/q47-green-{1..5}.{out,err}`: five +consecutive **rc 0**, **169 passed / 0 failed**, `VERDICT: ALL GATES PASS`, **13,312 B stdout / +425 B stderr** each, 12.03–12.58 s, live delta 0, held delta 0, no leftover. Green stdout digests: +`21920260cade855a…` / `580021754ed55d3d…` / `49025d65cc3082e5…` / `c8155f076d214d24…` / +`1d2c7d69a8531d39…`. + +`out/q47-green-1.out` vs `out/../q46/out/q46-green-1.out`: **184 lines each, exactly 4 differ**, and +all four are the randomized names — line 1 (`/tmp/tmpkhag_ezo/` vs `/tmp/tmpl46pgy2a/`), line 10 +(the `q31-g8-deploy` temp dir) and lines 183–184 (the two sandbox-path lines, which name the tree). +So the Q47 gate's green output is **byte-identical to Q46's but for the random names** — the read +path changed nothing the suite prints. + +**The Q39-bom row is a finding, not noise.** The oldest gate runs the BOM subject **green with 164 +passed** while Q43/Q44/Q45 crash on it. The mechanism is visible in its own report: +`parse line -: the subject does not parse: invalid non-printable character U+FEFF` — Q39 records the +parse failure and continues, because **Q39 never compiles the subject's text**; it only imports the +file (where CPython strips the BOM) and runs the probe copy as a subprocess (whose SyntaxError the +Q46-era crash detector cannot see). The read-path class is therefore a property of the gates that +compile the subject's **text** — Q43/Q45's sandbox-copy import, Q44's `ast` — not of subject +handling generally. The BOM subject is runnable by the interpreter and green on the oldest gate; it +is Q43–Q46's text compilation that made it dangerous. + +## 7. Limits and defects — mine first, logged not deleted + +1. **Harness verdict bug (mine).** `row_for()` was passed `READPATH_FIXTURES` (which carry `.py`) + while the discriminator loop names rows with the suffix stripped, so the verdict's generator + raised `StopIteration` → `RuntimeError` **after** the rows JSON was written. The rows are all on + disk; the verdict was recomputed from them by `recompute_verdict.py`, and `run_negatives_q47.py` + is patched (the strip) so the file on disk is right. No gate invocation was re-run. +2. **Vacuous fixture-dest census (mine).** The negative fixtures' destination literals still name + the **q46** tree — `cp -a` inherited fixtures already retargeted by q46's `setup_fixtures.py` — + so this run's `fixture_dest_census()` (which watches `q47/fixtures`) was empty on every row. + Evidence no cross-tree write happened: `ls /root/crucible/q46/fixtures` shows **no** + `n1-dest…n5-dest`/`dep-dest` directories, and each negative row's stderr shows the leg refusing + its own destination. The right fix was to re-run `setup_fixtures.py` retargeted into + `q47/fixtures`. +3. **The copies are UTF-8 by construction (recorded limit).** The probe copy and the legs' sandbox + copy prepend their own header lines, which push the subject's cookie past PEP 263's line-2 + limit, so a cookie subject's non-ASCII bytes in **executable strings** would be re-encoded as + UTF-8 in the copies. The fixture therefore carries its single latin-1 byte in a comment only. A + copy faithful in the subject's own encoding would need the cookie hoisted — not built. +4. **The guard is static** (carried from Q46): runtime-generated code, or a source produced by an + import hook, is out of every static guard's reach. +5. `tokenize.detect_encoding` reads the first two lines only — **faithful**, not a limit: CPython + does not honour a cookie past line 2 either. +6. Byte counts, hashes and timings are dated readings (2026-10-13 ~03:30–04:00Z). + +## 8. Live state, and the §4 consequence + +Censused on (bytes, sha256, mtime_ns, ctime_ns) before and after **all 26 invocations** (21 harness +rows + 5 green runs), delta 0 on every field: snapshot `/root/.helm/data/snapshot.json` **14,039 B / +`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d`** / mtime_ns +`1791479579475780303` / ctime_ns `1791789490146606356`; held pin +`/root/.helm/data/held/stackchain-home.html.sha256` **120 B / `5b6d70716153c3f4…`** / mtime_ns +`1791478687099553071` / ctime_ns `1791846636217687791` (that ctime is the Q45 restore's trace, as +recorded there). Deployed bin untouched: `helm-selftest.py` **33,647 B / `7ed1bca8f9da624f…`**, +`helm-collect.py` `ee0b36a82f23fcf3…`, `helm-render.py` `5c67473d89283c3d…`, `helm-validate.py` +`12570786ba703787…`, `summarize.py` `7386999867789694…`. Final tree: **0 sandboxes**. + +**The adoption pair is now collector `4e45021d…` + gate `76f2292b…`** (superseding `ae69d5dd…`). +Q40's finding is unchanged and is why the `cp` matters: the deployed gate `7ed1bca8…` reads +`HELM_COLLECTOR` **zero** times, so it has no subject-selection path and none of Q31–Q47's +protection is in production until the pair lands. The `cp` stays Alexander's. + +## 9. Q7, measured + +After this item the Helm lane's executable set is empty again: the adoption `cp`, Q9 (checksum +handover), Q13/Q19 and Q42 are 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.**