Q38: the destination guard is behavioural, not documented (Helm lane, Q36 candidate 2)

Q35's precondition reads a source marker (--deploy and SNAPSHOT_PATH.resolve() in the subject's
source), so a subject that documents the rule in a comment without implementing it is admitted —
the pipe measured exactly that at 02:4xZ today. The gate now, at helm-selftest.py:117-211, after
the Q35 check and before the subject import (:784) and G8's bare-refusal leg (:1079-1081, the Q34
incident's leg), runs a copy of the subject's source with its SNAPSHOT_PATH = assignment rewritten
to a sentinel path as a subprocess bare, and refuses the subject (exit 2, no leg runs) unless that
run exits non-zero, writes nothing, and leaves the live snapshot delta 0. The probe never imports
the subject into the gate's process.

Measured this run (scratch tree /root/crucible/q38/, nothing deployed):
- 7 consecutive green invocations, exit 0 each, 164 passed / 0 failed and ALL GATES PASS in every
  log, the Q38 OK line present in all seven, live snapshot sha256+mtime read around every
  invocation with delta 0 throughout.
- Discriminator on both sides: fake-documented-no-rule.py (both markers in its docstring, so the
  Q35 predicate over its source is true) exits 2 with the refusal naming probe rc=0 and the
  sentinel written (74 B / 817e3830...); fake-marker-only.py (the pipe's measured shape) exits 2 at
  the assignment step; the deployed collector exits 2 at the Q35 source check with zero Q38 lines —
  the ordering. The admission control: fake-marker-only.py under the Q35 gate runs G7 and dies at
  the subject import (AttributeError, helm-selftest.py:688) — the admission measured end to end.
- Gate 1,044 -> 1,141 lines (+97/-0; DIFF.txt 108 lines / ac8e52bb...), sha256 643e5387... ->
  74aba348...; collector byte-identical to the Q34/Q35 pair (4e45021d..., 720 lines).
- Live snapshot /root/.helm/data/snapshot.json 14,039 B / aa842b0f... / mtime_ns
  1791479579475780303, delta 0 across every leg; deployed bin untouched (ee0b36a8 / 7ed1bca8 /
  5c67473d); decisions/ and inbox/ untouched; no another-party file edited.

Section 4 consequence: the adoption pair is now collector 4e45021d... + gate 74aba348...
(superseding 643e5387...); the cp into /root/.helm/bin/ remains Alexander's.
This commit is contained in:
Merlin committed 2026-10-12 07:06:41 +00:00
1 parent e585b8809a
commit 0776480b51
3 files changed
+322

No files matched your search

+54
View File
@@ -5012,3 +5012,57 @@ actually is, since his own-identity commits are seen directly and need no rule.
commands; the harness output if a signal is found. **Not** in scope: changing the attribution of history — no commit
can be restated (see Q20 and Q36), and the five commits' content is already recorded as Merlin's on the author's own
statement and on the files' first-person text.
---
## Q38 — The behavioural destination guard *(Helm lane; opened 2026-10-12 by Merlin — Q36's candidate 2, on Alexander's direction to continue the Helm lane)*
Q35 made the destination-rule guard a **precondition**, but the precondition reads a **source marker** — the strings
`--deploy` and `SNAPSHOT_PATH.resolve()` present in the subject collector's source — and a subject that **documents**
the rule in a comment without implementing it is admitted. The pipe measured exactly that on 2026-10-12T02:4xZ: a
fake collector whose body is `import sys; sys.exit(0)` plus a comment carrying both strings ran the whole Q35 suite
to completion with live delta 0. This item makes the guard **behavioural**.
**Do this:** in a scratch tree, add to the gate a precondition that executes the subject against a **sentinel**
destination — a copy of the subject's source with its `SNAPSHOT_PATH =` assignment rewritten to a scratch path, run
as a subprocess bare (`--skip-tests`, no `--out`, no `--deploy`) — and refuses the subject unless the run exits
non-zero **and** writes nothing **and** the live snapshot is delta 0 around the probe. Then run the pair green for
**N ≥ 5 consecutive invocations** with each log's digest, and demonstrate the discriminator on a subject that passes
the source check but fails behaviourally.
**Done means:** `recoveries/helm-behavioural-guard.md` — the guard's code path with line numbers, the diff, the
green-pair logs with digests, the refusal transcripts, and the live snapshot delta 0.
**Status: DONE — `recoveries/helm-behavioural-guard.md` (10,232 B), Merlin, 2026-10-12T06:5x–07:1xZ, this host.**
Scratch tree `/root/crucible/q38/`; nothing deployed, nothing published; the shared tree, `/root/.helm/bin/`,
`decisions/` and `inbox/` untouched.
- **The guard** sits at `helm-selftest.py:117–211`, after the Q35 source-marker check (`:96–114`, kept as the fast
filter and measured to fire first) and before the subject import (`:784`) and G8's bare-refusal leg
(`:1079–1081` — the Q34 incident's leg). Gate 1,044 → **1,141 lines, +97/−0** (`DIFF.txt` 108 lines / sha256
`ac8e52bb38090c66…`), sha256 `643e5387…` → **`74aba34860b34ac6aebd8661e3e46c01ed8c15dcec1cd958ec766114abfa1ef0`**;
collector byte-identical to the Q34/Q35 pair (`4e45021d…`, 720 lines).
- **Green pair, 7 consecutive invocations**, exit 0 each, `helm-selftest: 164 passed, 0 failed` / `VERDICT: ALL GATES
PASS` in every log, the `Q38 OK` line (probe rc=2, sentinel not written, live delta 0) present in all seven; live
snapshot sha256+mtime read around every invocation, delta 0 on all seven. Log digests: `f606bf3877cd6a96…` /
`4cd6456713f2e1ff…` / `02ecbf989f2d162c…` / `94781f8dfa0f4ac8…` / `a2380b7ab4c61435…` / `fbd5e11713a6c048…` /
`d0cae43b79b4f5f1…`.
- **The discriminator, both sides.** `fake-documented-no-rule.py` (both markers in its docstring, so the Q35
predicate over its source is true): **exit 2**, stdout 0 bytes, the Q38 refusal naming the probe rc=0 and the
sentinel written (74 B / sha256 `817e38302aff7065…`), live delta 0. `fake-marker-only.py` (the pipe's measured
shape): **exit 2** at the assignment step, stdout 0 bytes. The deployed collector: **exit 2** at the Q35 source
check with zero Q38 lines — the ordering. The admission control: `fake-marker-only.py` under the **Q35** gate ran
G7 and died at the subject import (`AttributeError … has no attribute 'SNAPSHOT_PATH'`, `helm-selftest.py:688`) —
the admission, measured end to end — while the Q38 gate refuses the same subject before any leg.
- **Live state, delta 0 across every leg:** `/root/.helm/data/snapshot.json` 14,039 B / sha256
`aa842b0fd88037f8…` / mtime_ns `1791479579475780303`, identical before and after the 7 green runs, the 3 refusals
and the admission control. Deployed bin untouched (`ee0b36a8…` / `7ed1bca8…` / `5c67473d…`).
- **§4 consequence:** the adoption pair is now collector `4e45021d…` + gate **`74aba348…`** (superseding
`643e5387…`); the `cp` into `/root/.helm/bin/` remains Alexander's.
- **Limits stated in the deliverable:** the probe substitutes the module's `SNAPSHOT_PATH =` assignment, so a
subject resolving its destination by another route is not covered by the substitution (the live-delta-0 assertion
catches an actual write only after the fact); the refusal criterion is rc ≠ 0 **and** nothing written (a
no-write rc=0 subject is admitted — it cannot clobber); the timeout branch is coded and not exercised; and the
guard protects the gate's legs, not a direct CLI run of a pre-gate collector (the Q35 limit, unchanged).
- **Q7, measured:** the unclaimed executable set is unchanged and not empty — **Q37** (pipe tooling) is open. Nothing
opened this tick beyond this item, which discharges Q36's candidate 2 on Alexander's direction.
+111
View File
@@ -343,3 +343,114 @@ Alexander: nothing further is asked of you here. This is disclosed so the roster
reconciled by whoever owns them, and so the next tick does not repeat it.
— Merlin
---
## Run 4 — Q38: the destination guard is now behavioural, not documented
Claimed and finished **Q38 — the behavioural destination guard** (Helm lane; Q36's candidate 2, on
Alexander's direction to continue the lane). Deliverable: `recoveries/helm-behavioural-guard.md`
(**10,232 B / sha256 `69e72474c103c727c11408aca34a666309b740db66f0cd476bc13e1772d56db9`**, this
host). Queue block appended to `WORK-QUEUE.md` (was 565,470 B / sha256
`b4b8b2d5561725c8205b5da18fa190aa9a143b0dc56bf2707f7fb4d71d4d5673` before this run's append).
Scratch tree `/root/crucible/q38/`; nothing deployed, nothing published.
**What was gated.** Q35's precondition reads a *source marker* — `--deploy` and
`SNAPSHOT_PATH.resolve()` present in the subject's source — so a subject that **documents** the
rule in a comment without implementing it is admitted, which the pipe measured at 02:4xZ today
(a fake collector carrying both strings in a comment ran the whole Q35 suite to completion with
live delta 0). Q38 adds, at `helm-selftest.py:117–211` — after the Q35 check (`:96–114`, kept as
the fast filter and measured to fire first) and before the subject import (`:784`) and G8's
bare-refusal leg (`:1079–1081`, the leg that executed an ungated subject and wrote the live
snapshot in Q34's incident): a copy of the subject's source with its `SNAPSHOT_PATH =`
assignment rewritten to a **sentinel** path, run as a **subprocess** bare (`--skip-tests`, no
`--out`, no `--deploy`), and the subject is refused (**exit 2, no leg runs**) unless that run
exits non-zero **and** writes nothing **and** the live snapshot is delta 0 around the probe. The
probe never imports the subject into the gate's process — the subject's module-level code runs
only in the probe subprocess.
**Green pair, 7 consecutive invocations**, exit 0 each, `helm-selftest: 164 passed, 0 failed` and
`VERDICT: ALL GATES PASS` in every log, the `Q38 OK: … (probe rc=2, sentinel written=False, live
delta 0)` line present in all seven, live snapshot sha256+mtime read around every invocation
(`run-green.sh`), delta 0 on all seven. Log digests: `f606bf3877cd6a96…` / `4cd6456713f2e1ff…` /
`02ecbf989f2d162c…` / `94781f8dfa0f4ac8…` / `a2380b7ab4c61435…` / `fbd5e11713a6c048…` /
`d0cae43b79b4f5f1…`.
**The discriminator, both sides.** `fixtures/fake-documented-no-rule.py` — both markers in its
docstring, so the Q35 predicate over its source is **true** (measured: `deploy` ×2,
`SNAPSHOT_PATH.resolve()` ×1 by line count): **exit 2**, stdout 0 bytes (`e3b0c442…`), the refusal
naming the probe rc=0 and the sentinel **written** (74 B / sha256
`817e38302aff70659ac8f49bd1607a7843748a917eec97dc01582d95c932f437`), live delta 0.
`fixtures/fake-marker-only.py` — the pipe's measured shape: **exit 2** at the assignment step (no
`SNAPSHOT_PATH =` at all), stdout 0 bytes. The deployed collector
(`HELM_COLLECTOR=/root/.helm/bin/helm-collect.py`): **exit 2** at the Q35 source check with **zero
Q38 lines** in the output — the ordering. The admission control: `fake-marker-only.py` under the
**Q35** gate ran G7 to completion and died at the subject import (`AttributeError: module
'helm_collect_gate' has no attribute 'SNAPSHOT_PATH'`, traceback naming
`/root/crucible/q35/helm-selftest.py:688`) — the admission measured end to end — while the Q38
gate refuses the same subject before any leg runs.
**What moved.** Gate 1,044 → **1,141 lines, +97/−0** (`DIFF.txt`, 108 lines / sha256
`ac8e52bb38090c66c8592a4aee9b39667d0cbbf6fefb8567043bf1e3ed0d588d`), sha256
`643e53874e42f7ae…` → **`74aba34860b34ac6aebd8661e3e46c01ed8c15dcec1cd958ec766114abfa1ef0`**.
The collector is byte-identical to the Q34/Q35 pair (`4e45021d…`, 720 lines, 29,954 B) — the gate
moved, the subject did not. **§4 consequence:** the adoption pair is now collector `4e45021d…` +
gate **`74aba348…`** (superseding `643e5387…`); the `cp` into `/root/.helm/bin/` is Alexander's.
**Live state, delta 0 across every leg.** `/root/.helm/data/snapshot.json`: 14,039 B / sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime
`2026-10-08 17:12:59.475780303 +0000` (mtime_ns `1791479579475780303`), identical before and after
the 7 green runs, the 3 refusals and the admission control; the sentinel temp directories are
removed at exit (0 left). Deployed bin untouched: `ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` /
`5c67473d89283c3d…`. `decisions/` and `inbox/` untouched; no another-party file edited; no secret
printed here or in the files this lands — the long strings are check names, paths and digests.
**Limits, stated in the deliverable:** the probe substitutes the module's `SNAPSHOT_PATH =`
assignment, so a subject that resolves its destination by another route is not covered by the
substitution (the delta-0 assertion catches an actual write only after the fact); the criterion is
rc ≠ 0 **and** nothing written — a no-write rc=0 subject is admitted, correctly, since it cannot
clobber the live snapshot bare; the timeout branch is coded and not exercised by a fixture; and
the guard protects the gate's legs, not a direct CLI run of a pre-gate collector (the Q35 limit,
unchanged). fake-B was deliberately **not** run under the Q35 gate: its G8 legs include a bare run
and an explicit-live-`--out` run, either of which writes the live snapshot with an ungated subject
— the accident class itself — so its admission was measured by the predicate directly, with fake-C
supplying the end-to-end admission.
**Attribution note, mine:** this run's commits are authored `Merlin <merlin@agents.invalid>` via
`git -c`, per the Q36 monitor fix — the Run 3 attribution fault is not repeated.
## Blocked
Nothing in my lane. Q9 waits on the checksum handover, Q13 and Q19 on Alexander, and **Q37 is the
pipe's own tooling** — the monitor script lives in the default profile's tree and Q37's own
analysis already names where the fault is (the counterparty's push identity), so neither the
harness edit nor the record is mine to make.
## Next
The §4 adoption (collector `4e45021d…` + gate `74aba348…`, the two-file `cp` that makes
`python3 helm-selftest.py` green with no environment) and the `manifest_match` pin, both
Alexander's. Nothing else in my lane is executable; with Q38 closed the Helm candidates from Q36
are both discharged (candidate 1 was measured in Q35's post-`cp` staging; candidate 2 is this
run). If the adoption lands, the natural follow-on is a verification run of the *deployed* pair —
same discriminator, one host change.
## Ask
Alexander: nothing blocking. The §4 adoption is the only thing between this scratch pair and a
shipped gate whose default subject is green rather than self-refusing — say the word and the `cp`
is one command, with the deployed-pair verification as the next unit.
— Merlin
---
Landing evidence, Run 4, 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-behavioural-guard.md` | 200 / 10,232 | `69e72474c103c727c11408aca34a666309b740db66f0cd476bc13e1772d56db9` |
| `WORK-QUEUE.md` | 200 / <measured post-append> | <measured post-append> |
| `checkins/2026-10-12-merlin.md` | 200 / <pre-append revision, as pushed> | <measured pre-append> |
+157
View File
@@ -0,0 +1,157 @@
# The behavioural destination guard — Q38 (Helm lane, 2026-10-12)
Q35 made the destination-rule guard a **precondition**, but the precondition reads a **source
marker**: the strings `--deploy` and `SNAPSHOT_PATH.resolve()` present in the subject collector's
source. A subject that **documents** the rule in a comment without implementing it passes that
check and is admitted. The pipe measured exactly that on 2026-10-12T02:4xZ: a fake collector
whose body is `import sys; sys.exit(0)` plus a comment carrying both strings ran the whole Q35
suite to completion with live delta 0. Q38 makes the guard **behavioural**: the subject is
executed against a sentinel destination and must **refuse**.
Nothing is published, nothing is deployed. The shared build tree, `/root/.helm/bin/`, the live
snapshot, `decisions/` and `inbox/` are untouched; every figure below came from a command run in
this pass. Scratch tree `/root/crucible/q38/`.
## The guard
At `helm-selftest.py:117–211` — after the Q35 source-marker check (`:96–114`) and **before** the
subject is imported (`:784`) and before G8's bare-refusal leg (`:1079–1081`, the leg that executed
an ungated subject and wrote the live snapshot in Q34's incident):
1. A copy of the subject's source is written to the gate's own scratch tree, with its
`SNAPSHOT_PATH =` assignment rewritten to a **sentinel** path
(`<gate dir>/helm-q38-*/snapshot.json`, created per invocation, removed at exit).
2. The copy is run as a **subprocess**, bare: `python3 <probe> --skip-tests` — no `--out`, no
`--deploy`, 180 s timeout. The subject's module-level code never executes inside the gate's
process (the Q35 mechanism probe imported and rebound in-process; this one does not).
3. The subject **passes** iff the run exits non-zero **and** writes nothing to the sentinel
**and** the live snapshot is byte- and mtime-identical before and after. Otherwise the gate
exits **2** with a stderr message naming the subject, the probe path, the probe rc, the
sentinel's byte count and sha256, and the live snapshot's before/after readings — and **no
leg runs**. A subject that exits 0 and writes nothing is admitted: it cannot clobber the live
snapshot bare, which is the property under test.
4. A subject carrying no `SNAPSHOT_PATH =` assignment at all is refused at step 1 — the
destination the rule keys on does not exist, and such a subject crashes the gate at the
import (`:784`) anyway.
The Q35 source check is **kept** as the fast first filter and fires first (measured below); the
behavioural check is the authority. The precondition registers no `check()`, so the green pair
still reports exactly 164 passed / 0 failed; the positive side prints one `Q38 OK` line on stderr
in every gate log so it is visible rather than implicit.
Gate: 1,044 → **1,141 lines, +97/−0** (`git diff --no-index --numstat`; `DIFF.txt` 108 lines /
sha256 `ac8e52bb38090c66c8592a4aee9b39667d0cbbf6fefb8567043bf1e3ed0d588d`), sha256
`643e53874e42f7ae…` → **`74aba34860b34ac6aebd8661e3e46c01ed8c15dcec1cd958ec766114abfa1ef0`**.
The collector is byte-identical to the Q34/Q35 pair: `4e45021d80395d3f…`, 720 lines, 29,954 B.
Both fixtures and the runner (`run-green.sh`) live in the same tree.
## The subjects and the matrix
| subject | Q35 source check | Q38 behavioural | outcome |
|---|---|---|---|
| pair `4e45021d…` (`/root/crucible/q38/helm-collect.py`) | pass | pass (probe rc=2, sentinel not written, live delta 0) | admitted — 7× green |
| `fixtures/fake-documented-no-rule.py` | **pass** (both markers in its docstring) | **fail** (rc=0, sentinel written) | exit 2, no leg |
| `fixtures/fake-marker-only.py` (the pipe's measured shape) | **pass** | **fail** (no `SNAPSHOT_PATH =` assignment) | exit 2, no leg |
| deployed `/root/.helm/bin/helm-collect.py` (`ee0b36a8…`) | **fail** (no markers) | not reached | exit 2 at Q35 — the ordering |
### Green pair — 7 consecutive invocations
`HELM_COLLECTOR=/root/crucible/q38/helm-collect.py python3 helm-selftest.py`, exit 0 each,
`helm-selftest: 164 passed, 0 failed` and `VERDICT: ALL GATES PASS` in every log, and the
`Q38 OK: the subject refused its sentinel destination (probe rc=2, sentinel written=False, live
delta 0)` line present in all seven (`run-green.sh` reads the live snapshot's sha256 and mtime
around every invocation; delta 0 on all seven). Log digests:
`f606bf3877cd6a96…` / `4cd6456713f2e1ff…` / `02ecbf989f2d162c…` / `94781f8dfa0f4ac8…` /
`a2380b7ab4c61435…` / `fbd5e11713a6c048…` / `d0cae43b79b4f5f1…`
The logs differ run to run in the temp paths only (the Q35 tick's C15 class); the verdict line is
identical in all seven and is what the digests are quoted for.
### fake-B — documents the rule, writes its default anyway
`HELM_COLLECTOR=/root/crucible/q38/fixtures/fake-documented-no-rule.py python3 helm-selftest.py`
→ **exit 2**, stdout **0 bytes** (`e3b0c442…`, the empty-file digest — no `helm-selftest:` summary
and no PASS line exists, which is how "no leg executed" is measured), stderr
(`q38-refuse-fakeB.err`, sha256 `38dcd0e40b05f9ba…`):
> Q38 REFUSAL: the subject collector … carries no WORKING destination rule — a bare run against
> the sentinel exited rc=0 (stderr: ); sentinel written=True (74 B /
> `817e38302aff70659ac8f49bd1607a7843748a917eec97dc01582d95c932f437`); live delta 0 — so no leg
> is executed.
Its docstring carries both marker strings, so the Q35 predicate over its source is **true**
(measured: `deploy` ×2, `SNAPSHOT_PATH.resolve()` ×1 by line count on the fixture) — it is
admitted by the source check and refused by behaviour. Its unrewritten default destination is
deliberately **not** the live snapshot: the unrewritten file is never executed by this gate, but a
non-live default means even a false admission during measurement could not clobber live state.
The real-world subject this models — the deployed collector — has the live snapshot as its
default, and that is the shape Q34's incident ran.
**fake-B was not run under the Q35 gate.** Its G8 legs include a bare run (`:982` in the Q35
file) and an explicit-live-`--out` run (`:994`), either of which writes the live snapshot with an
ungated subject — the accident class itself. The admission was measured by the predicate
directly, and the end-to-end admission is shown by fake-C below.
### fake-C — the pipe's measured shape, admitted by Q35, refused by Q38
`HELM_COLLECTOR=/root/crucible/q38/fixtures/fake-marker-only.py python3 helm-selftest.py`
→ **exit 2**, stdout 0 bytes, stderr (`q38-refuse-fakeC.err`, sha256
`b3ab82a71d170998…`): *"carries no WORKING destination rule — the subject carries no
`SNAPSHOT_PATH =` assignment, so its default destination is not the module constant the rule
keys on — so no leg is executed."*
The same subject under the **Q35** gate
(`HELM_COLLECTOR=… python3 /root/crucible/q35/helm-selftest.py`, `q35-admit-fakeC.log`, 841 B /
sha256 `691155d4b99dee0b…`) ran G7 to completion and then died at the subject import —
`AttributeError: module 'helm_collect_gate' has no attribute 'SNAPSHOT_PATH'` at
`helm-selftest.py:688` — which is the admission, measured: the Q35 precondition passed the
subject and the gate proceeded past it. Under Q38 the same subject never reaches the import.
### Ordering — the deployed collector is still refused at Q35
`HELM_COLLECTOR=/root/.helm/bin/helm-collect.py python3 helm-selftest.py` → **exit 2**, stdout 0
bytes, stderr (`q38-refuse-deployed.err`, sha256 `2aaa36c1f8277e90…`, byte-identical to the Q35
tick's own `q35-refuse-deployed.err`) carrying the **Q35 REFUSAL** line and zero Q38 lines: the
source-marker check fires first, the behavioural check is not reached, live delta 0.
## Live state, measured before and after every leg
`/root/.helm/data/snapshot.json`: **14,039 B / sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime
`2026-10-08 17:12:59.475780303 +0000` (mtime_ns `1791479579475780303`)** — identical before and
after each of the 7 green runs, each of the 3 refusals, and the Q35 admission control. The
sentinel temp directories are removed at exit (0 left behind).
Deployed bin untouched: `helm-collect.py` `ee0b36a82f23fcf3…` (687 lines / 27,907 B),
`helm-selftest.py` `7ed1bca8f9da624f…` (609 / 33,647), `helm-render.py`
`5c67473d89283c3d…` (496 / 21,766). Nothing deployed, nothing rebuilt, nothing published;
`decisions/` and `inbox/` untouched; no another-party file edited; no secret printed here or in
the files this lands.
## Limits, stated
- The probe substitutes the module's `SNAPSHOT_PATH =` assignment. A subject that resolves its
destination by another route — a hardcoded path, an environment variable — is not covered by
the substitution; the live-delta-0 assertion would catch an actual write only **after** the
fact. The pair keys on the module's own `SNAPSHOT_PATH` (Q31/Q35 measured that rebinding does
not disable the rule), so the substitution is sound for this artifact family; it is not a
general sandbox.
- The refusal criterion is rc ≠ 0 **and** nothing written **and** delta 0. A subject that
refuses with rc = 0 while writing nothing is admitted — correct, since it cannot clobber the
live snapshot bare — but "refuses" is defined by the non-zero exit, not by a message.
- The timeout branch (rc `None`) is coded and not exercised by a fixture.
- The guard protects the **gate's legs**, not a direct CLI run of a pre-gate collector — the
Q35 limit, unchanged.
- The bare-run leg now admits only behaviourally gated subjects, but the shipped gate is still
the pre-Q38 one: the §4 `cp` is Alexander's, and with this run the adoption pair becomes
collector `4e45021d…` + gate **`74aba348…`** (superseding `643e5387…`).
## Artifacts
`/root/crucible/q38/`: `helm-selftest.py` (the patched gate), `helm-collect.py` (the pair,
unchanged), `fixtures/fake-documented-no-rule.py`, `fixtures/fake-marker-only.py`,
`DIFF.txt`, `run-green.sh`, `q38-gate-run-1..7.log`, `q38-refuse-fakeB.{log,err}`,
`q38-refuse-fakeC.{log,err}`, `q38-refuse-deployed.{log,err}`, `q35-admit-fakeC.log`.
— Merlin, 2026-10-12