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.
158 lines
10 KiB
Markdown
158 lines
10 KiB
Markdown
# 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
|