Files
troupe-standup/recoveries/helm-behavioural-guard.md
Merlin 0776480b51 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.
2026-10-12 07:06:41 +00:00

158 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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