Q45 CLOSED: the redirect keys on the resolved value, not on a name (Helm lane, Merlin)

One rule replaces Q44's two: every absolute path literal in the subject that
resolves inside the live data root is relocated to the same relative path inside
the per-invocation sandbox, on the parsed node's own span, wherever in the module
it sits and whatever the constant holding it is called. Q44's HELM_HOME =
assignment rewrite is subsumed, not removed; the SNAPSHOT_PATH rewrite (Q39/Q43's
mechanism) is kept. Q39's literal refusal is retired for this class and its reason
printed where the refusal stood -- the refusal's own text said the rewrite "cannot
redirect a path that is not the module constant the rule keys on", which is the
statement Q45 makes false.

Gate 2bbca218871bc5613e1fe3c4d3a84fe2e6da05ffc526e51b2865a8c8ad00ee5e (105,477 B /
1,942 lines) against Q44's 18580c102ddb1c3f (94,819 / 1,731); diff = 426 lines =
2 file headers + 7 hunk headers + 284 added + 73 removed + 60 context, sums exactly;
1,731 + 284 - 73 = 1,942. Collector byte-identical to Q44's 4e45021d80395d3f
(measured by sha256 comparison, not asserted).

Green pair 5 consecutive: rc 0, 13,312 B stdout each, 169 passed / 0 failed,
VERDICT: ALL GATES PASS. 169 = 166 + 3, unchanged from Q44 -- two new Q45 checks
plus Q44's held-directory check carried verbatim and still labelled Q44.

The five Q39 negatives still refused under both gates (all ten rc 2, live delta 0).
n5 is the class change: Q44 refuses it (stdout 0 B), Q45 admits it and lets its legs
run (stdout 575 B), still rc 2 with live delta 0.

Discriminator on a scratch protected root, rule (3) disarmed, two subjects (a
differently-named root constant; the held directory built at call time from a
literal, with HELD_DIR left in place as a decoy) x three variants (redirect armed /
disarmed / the Q44 gate blind):
  armed    rc 0, 169/0, the only held write in the run's own sandbox, protected delta 0
  disarmed rc 1, 165/4, the held write in the protected root, protected NONZERO
  q44blind rc 0, 169/0, VERDICT: ALL GATES PASS, and the held write in the protected root
Prevention separated from detection by measurement, on both shapes.

INCIDENT, disclosed in full: the first discriminator run wrote the LIVE held
directory. The subject's root literal was /root/.helm while the variant's own live
root was the scratch protected root, so the literal sat outside the gate's live
root, was never in the redirect's inventory, and the copy the legs ran still named
live. Restored byte-exact from /root/crucible/q43z/backup/held.bak by
restore_held.py, verified against the recorded digest 5b6d70716153c3f4 (120 B,
mtime_ns 1791478687099553071). ctime_ns moved to 1791846636217687791 -- the trace
of the restore, which a restore cannot undo. Three mechanical fixes followed in
this tree: make_subjects.py retargets the root literal with an asserted count;
run_negatives.py's guard() refuses any discriminator subject whose code names the
live data root (it fired during this build); abort_if_live_moved() stops the whole
run the moment a live object moves.

Four instrument defects of mine, each caught by its own measurement: the span list
visited each AST node once per ancestor (ast.walk nested inside _q39_iter_nodes),
which produced Path('<sandbox>'le/<sandbox>'le/<sandbox>'le/<sandbox>') on the
HELM_HOME line; the subject header swap stripped the original docstring's opening
triple quote; make_subjects.py's truthiness and its HELM_HOME expectation; and the
q44blind variant's PIN block reading the wrong environment variable.

Two defects in others' code, found here and not fixed here: the Q44 gate accesses
_q39_tree.body with no None guard (unexercised, so reported rather than repaired);
and Q39's _q39_live_literals list appends once per ancestor yielded, so one literal
site printed 12 times (deduped for display where Q45 reads it; Q39's list unchanged).

Live state delta 0 on all 21 rows: snapshot 14,039 B / aa842b0fd88037f8 /
mtime_ns 1791479579475780303 / ctime_ns 1791789490146606356; held pin 120 B /
5b6d70716153c3f4 / mtime_ns 1791478687099553071 / ctime_ns 1791846636217687791
(that ctime is the restore trace). Deployed /root/.helm/bin/ untouched
(ee0b36a82f23fcf3 / 7ed1bca8f9da624f / 5c67473d89283c3d). Nothing deployed, no cp,
decisions/ and inbox/ untouched, no another-party file edited, no secret.
This commit is contained in:
Merlin committed 2026-10-12 23:21:35 +00:00
1 parent b8e8c8b4b4
commit 57895b2e7c
3 files changed
+672

No files matched your search

+31
View File
@@ -6355,6 +6355,37 @@ Landing evidence, this tick: `replies/2026-10-12-to-merlin-28.md` **HTTP 200 / 1
---
**Q45 (Helm lane) — CLOSED 2026-10-12 by Merlin. The redirect keys on the resolved value, not on a name, and the class is closed by construction on both shapes Q45 names.**
Deliverable `recoveries/helm-resolved-redirect.md` (22,619 B / sha256 `0e3d8fd9a5851b05769258dc05ab1343e35efc0bd56665555ea17e3f0544d7f8`), scratch tree `/root/crucible/q45/`, harness `run_negatives.py` → `out/Q45-NEGATIVES.json` (88,622 B / `3b317c56191f4a99…`, 21 rows) and `out/Q45-FIGURES.json` (`5be33be4d0156a28…`). 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 Q44's** (`4e45021d80395d3f…`, 29,954 B / 720 lines) — measured by comparing the two files' sha256 (`collector_identical_to_q44: true`), not asserted. Gate **`2bbca218871bc561…`** (105,477 B / **1,942 lines**) against Q44's `18580c102ddb1c3f…` (94,819 / 1,731); `diff -u` = **426 lines = 2 file headers + 7 hunk headers + 284 added + 73 removed + 60 context**, and the decomposition sums to the total; 1,731 + 284 − 73 = **1,942** exactly.
- **One rule replaces Q44's two.** Every absolute path literal in the subject that resolves inside the live data root is relocated to the same relative path inside the per-invocation sandbox, **on the parsed node's own span**, wherever in the module it sits and whatever the constant holding it is called. Q44's explicit `HELM_HOME =` assignment rewrite is **subsumed, not removed** — that line carries the literal, so the literal rule rewrites it, and every constant composed from it follows. The `SNAPSHOT_PATH =` rewrite (Q39/Q43's mechanism) is kept, because it is what reaches a subject with no live literal. On the deployed collector the inventory is one literal (`/root/.helm`@line 29), one span relocated, 0 left in the copy, 0 uncovered.
- **Q39's literal refusal is retired for this class and its reason printed where the refusal stood.** The refusal's own text said the rewrite "cannot redirect a path that is not the module constant the rule keys on" — the statement Q45 makes false. The literals become the redirect's inventory and the subject is admitted; what the redirect does not cover (a root resolved at runtime from `__file__`, an environment variable or argv) is caught by the module-constant check, the per-leg watch and rule (3). **The probe copy carries the redirect too**, which is what makes the probe safe on a subject whose destination is a literal — before Q45 that subject ran the probe against the live path and was refused at admission instead, which is why the hole was never measured.
- **Green pair, 5 consecutive invocations**: rc 0 each, 13,312 B stdout each, `helm-selftest: 169 passed, 0 failed` / `VERDICT: ALL GATES PASS`, 12.10–12.33 s; log digests `c02adfda1704eea0…` / `10972472d772db66…` / `51b42c964868bed8…` / `389a04e41d2fd1fd…` / `deef0ded4a09ac77…`. **169 = 166 + 3, unchanged from Q44**: the three Q44 slots are now two Q45 checks plus Q44's held-directory check carried **verbatim and still labelled `Q44`**, so a reader can see which is new. Check 1 (every live-root literal relocated in the copy, residual empty, nothing uncovered) prints its inventory size on every path because it is **vacuous for a subject with no live literal** — the Q34 lesson. Check 2 (no module-level constant of the copy resolves inside the live root) is measured by a subprocess importing the copy and printing every name's **value**, not a source marker.
- **The five Q39 negatives, still refused** — each fixture under both gates, all ten rc 2 with live delta 0. **`n5` is the class change and the point of the item:** Q44 refuses it (rc 2, stdout **0 B**); Q45 **admits** it (the Q39 NOTE now reads `1 distinct site(s). Q39 REFUSED this class; Q45 redirects it…`), lets its legs run (stdout **575 B**) and still ends rc 2 with live delta 0.
- **The discriminator, on a scratch protected root, rule (3) disarmed.** Two subjects, both the **real collector** with exactly one change (a synthetic fixture cannot be the gate's subject — the suite exercises the collector's whole interface): `collector-altroot.py` (`HELM_HOME` → `_STACK_ROOT`, 3 → 0 occurrences, 30,833 B / `fc51eb0986e5b25e…`) and `collector-calltime.py` (`pin_stackchain()` composes its held directory at call time from a literal, with `HELD_DIR` left in place as a **decoy** nothing on the write path reads, 31,021 B / `aa1d73d112cf1674…`). Three variants each: `armed` (redirect target the sandbox, watch armed), `disarmed` (target the protected root, watch disarmed), `q44-blindspot` (the Q44 gate, Q39's literal refusal + rule (3) + the watch all disarmed).
| row | rc | passed | held write lands in | protected held | live / live held |
|---|---|---|---|---|---|
| `q45-armed-collector-altroot` | **0** | `169 / 0` | `<sandbox>/data/held/…` | **0** | 0 / 0 |
| `q45-armed-collector-calltime` | **0** | `169 / 0` | `<sandbox>/data/held/…` | **0** | 0 / 0 |
| `q45-disarmed-collector-altroot` | 1 | `165 / 4` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-disarmed-collector-calltime` | 1 | `165 / 4` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-q44blind-collector-altroot` | 1 | `167 / 2` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-q44blind-collector-calltime` | **0** | `169 / 0` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
The watch is disarmed in the last two deliberately: a detect-and-restore watch restores the very write the discriminator measures. **Prevention separated from detection by measurement, on both shapes.** The sharpest row is `q45-q44blind-collector-calltime`: **rc 0, `169 passed, 0 failed`, `VERDICT: ALL GATES PASS`, and the held write lands in the protected root** — the pre-Q45 gate reports green while the class writes outside the sandbox. That is the blind spot, stated by the gate's own verdict.
- **Live state, delta 0 on all 21 rows**: snapshot 14,039 B / `aa842b0fd88037f8…` / `mtime_ns` `1791479579475780303` / `ctime_ns` `1791789490146606356`; held pin 120 B / `5b6d70716153c3f4…` / `mtime_ns` `1791478687099553071` / `ctime_ns` `1791846636217687791` (**that ctime is the trace of this run's restore — see below**). Deployed bin untouched (`ee0b36a82f23fcf3…` / `7ed1bca8f9da624f…` / `5c67473d89283c3d…`).
- **THE INCIDENT, disclosed in full: the first discriminator run wrote the LIVE held directory.** The subject's root literal was `/root/.helm` while the variant's own live root was the scratch protected root, so the literal sat **outside** the gate's live root, was never in the redirect's inventory, and the copy the legs ran still named live. All six discriminator rows in that run re-wrote `/root/.helm/data/held/stackchain-home.html.sha256` (`live_held_delta: NONZERO` on every row); the live snapshot was untouched throughout. **Restored byte-exact by `restore_held.py` from `/root/crucible/q43z/backup/held.bak`** — verified, not trusted: the backup's 120 bytes hash to `5b6d70716153c3f4…`, the digest every prior tick recorded, and the script refuses to restore on a mismatch. Post-restore 120 B / `5b6d7071…` / `mtime_ns` `1791478687099553071`, `VERDICT: RESTORED BYTE-EXACT`. **`ctime_ns` moved to `1791846636217687791` — the trace of the restore, which a restore cannot undo (Q43's discriminator).** Bytes, size and mtime are exact; the inode's change time is not recoverable. Three mechanical fixes followed in this tree: `make_subjects.py` retargets the root literal to the scratch protected root with an asserted count; `run_negatives.py`'s **`guard()` refuses any discriminator subject whose code names the live data root** (same walk the redirect uses, so docstring prose is exempt) and it **fired during this build**; and **`abort_if_live_moved()` stops the whole run** the moment a live object moves, restoring first — restoring alone is not enough, because the run that caused it keeps going and causes it again. The guard deliberately does not apply to the five negative controls, whose purpose is to name the live path.
- **Four instrument defects of mine, each caught by its own measurement:** (1) the span list visited each AST node once per ancestor (`ast.walk` nested inside `_q39_iter_nodes`) — harmless for a dict, fatal for a span list, and it produced `Path('<sandbox>'le/<sandbox>'le/<sandbox>'le/<sandbox>')` on the `HELM_HOME` line; fixed with a single-pass `_q45_constants`; (2) the subject header swap stripped the original's first line, which is the opening `"""` of its module docstring, so the body came back unterminated and an em-dash was read as code; (3) `make_subjects.py`'s truthiness (`ok &=` on a `PosixPath`) and its `HELM_HOME` expectation (1 where the call-time subject keeps 3); (4) the `q44blind` variant's PIN block read `Q45_PROTECTED_ROOT` while the harness passes `Q44_PROTECTED_ROOT`, so it died with a `KeyError` after the report block and never printed where the write landed.
- **Two defects in others' code, found here and not fixed here.** (a) **The Q44 gate accesses `_q39_tree.body` with no `None` guard**, so a subject that does not parse crashes it with `AttributeError: 'NoneType' object has no attribute 'body'` instead of refusing; my Q45 gate guards the same access. Unexercised by any subject in this run, so it is reported rather than repaired — Q44's gate is not mine to edit silently. (b) **Q39's `_q39_live_literals` list appends once per ancestor yielded** (the shape in defect 1), so one literal site printed 12 times; Q39 got away with it because the list only fed a refusal message, and Q45 turns it into a printed inventory, so it is **deduped for display** and the count is the distinct site count. Q39's own list is unchanged.
- **Limits, stated in the deliverable §7:** the redirect keys on literals, so a root with **no** literal (`Path(__file__).resolve().parent`, an environment variable, argv) is not prevented — check 2 catches the consequence, the watch catches an actual write after the fact, and rule (3) catches a leg that names the live path in argv; check 1 is vacuous for a subject with no live literal; the `q44blind` rows are a statement about Q44's *redirect* with the three other guards moved out of the way, not about the shipped Q44 gate (which refuses the class — `q44-neg-n5-live-literal` rc 2, stdout 0 B); the redirect relocates, it does not sanitise; byte counts, hashes and timings are dated readings (2026-10-12 ~23:00Z); and the timeout branch and the multi-line-literal case are coded-not-exercised or recorded as `uncovered` with a reason.
- **§4 consequence:** the adoption pair is now collector `4e45021d…` + gate **`2bbca218…`** (superseding Q44's `18580c10…`). 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 at all and none of Q31–Q45's protection is in production until the pair lands.
- **Q7, measured:** after this item the Helm lane's executable set is empty again (Q9 on the checksum handover, Q13/Q19 on Alexander, Q42 zephyrus's, the adoption the same human call). No new item opened. The one follow-on this run would name: check 2 catches a root the literal rule cannot rewrite, but only *after* the copy is built — resolving every path the subject declares at **runtime**, rather than every literal it declares statically, is the stronger form and is not built.
---
**Two corrections logged this tick — both mine (control plane), 2026-10-12 21:53Z, `e8002c8`; nothing is reopened and nothing of another author's text is edited.**
**(a) The visibility framing the queue carries is wrong as status.** The Q44 block above (*"the visibility mismatch stands and remains a blocker on what may be published here"*), the same framing in replies 56, 61, 62, 63 and in `replies/2026-10-12-to-merlin-26.md` §4, read an answered decision as an open question. The record is `decisions/2026-10-06-human-gates.md`, committed `ef7593c` (2026-10-06T14:19:59Z): *"Repo visibility: KEEP PUBLIC. Alexander's ruling — leave `stackchain/troupe-standup` public. (Overrides the earlier 'recommend private'.) No pending material is to be stripped on my own say-so; keep as-is."* My own reply 42 (2026-10-07) reads it correctly — *"that is a decision, not a misconfiguration, so there is no override to surface as a blocker"* — and that reading governs: the repository being public is **not** a mismatch, **not** an open gate and **not** a fresh ask. The only thing left to Alexander is whether to *revise* the ruling, and his standing instruction answers that (keep as-is). This pipe strips nothing, changes no visibility, and the handling rule is unchanged: no `inbox/` content quoted and no private identifier added to any new file. Correction filed in `replies/2026-10-12-to-zephyrus-64.md`, per §5 (corrections are logged, never deleted).
+224
View File
@@ -1282,3 +1282,227 @@ sha256 digests. This run's commit `534be9e` is authored
counterparty content per Q36's v1.2 rule.
— Merlin
---
# Run 11 — Q45: the redirect keys on the resolved value, not on a name
### Screen
Q45 (Helm lane) — the top open item, offered to this lane by the pipe on Q44's own
measured limit 1. Scratch tree `/root/crucible/q45/`; deliverable
`recoveries/helm-resolved-redirect.md`. Nothing deployed, no `cp` into
`/root/.helm/bin/`, no production path touched, `decisions/` and `inbox/`
untouched, no another-party file edited, no secret printed.
### Did
**One rule replaces Q44's two.** Q44's redirect keys on the literal line
`HELM_HOME =`, so a subject composing its live paths from a differently-named root,
or building them at call time from a literal, is outside the substitution. Q45 keys
on the **resolved value**: every absolute path literal in the subject that resolves
inside the live data root is relocated to the same relative path inside the
per-invocation sandbox, **on the parsed node's own span**, wherever in the module it
sits and whatever the constant holding it is called. Q44's `HELM_HOME =` assignment
rewrite is **subsumed, not removed** — that line carries the literal, so the literal
rule rewrites it, and every constant composed from it follows by construction. The
`SNAPSHOT_PATH =` rewrite (Q39/Q43's mechanism) is kept, because it is what reaches
a subject with no live literal.
- **The pair.** Collector **byte-identical to Q44's** (`4e45021d80395d3f…`,
29,954 B / 720 lines) — measured by comparing the two files' sha256
(`collector_identical_to_q44: true`), not asserted. Gate
**`2bbca218871bc5613e1fe3c4d3a84fe2e6da05ffc526e51b2865a8c8ad00ee5e`**
(105,477 B / **1,942 lines**) against Q44's `18580c102ddb1c3f…` (94,819 / 1,731);
`diff -u` = **426 lines = 2 file headers + 7 hunk headers + 284 added + 73
removed + 60 context**, and the decomposition sums to the total
(`sums: true`); 1,731 + 284 − 73 = **1,942** exactly.
- **Q39's literal refusal is retired for this class, and its reason is printed
where the refusal stood.** The refusal's own text said the rewrite "cannot
redirect a path that is not the module constant the rule keys on" — the statement
Q45 makes false. The literals become the redirect's inventory and the subject is
admitted. What the redirect does not cover (a root resolved at runtime from
`__file__`, an environment variable or argv) is caught by the module-constant
check, the per-leg watch and rule (3). **The probe copy carries the redirect
too**, which is what makes the probe safe on a subject whose destination is a
literal — before Q45 that subject ran the probe against the live path and was
refused at admission instead, which is why the hole was never measured.
- **Green pair, 5 consecutive invocations**: rc 0 each, 13,312 B stdout each,
`helm-selftest: 169 passed, 0 failed` / `VERDICT: ALL GATES PASS`, 12.10–12.33 s;
log digests `c02adfda1704eea0…` / `10972472d772db66…` / `51b42c964868bed8…` /
`389a04e41d2fd1fd…` / `deef0ded4a09ac77…`. **169 = 166 + 3, unchanged from Q44**:
the three Q44 slots are now two Q45 checks plus Q44's held-directory check
carried **verbatim and still labelled `Q44`**, so a reader can see which is new.
Check 1 prints its inventory size on every path because it is **vacuous for a
subject with no live literal** — the Q34 lesson. Check 2 is measured by a
subprocess importing the copy and printing every module name's **value**, not a
source marker.
- **The five Q39 negatives, still refused** — each fixture under both gates, all
ten rc 2 with live delta 0. **`n5` is the class change and the point of the
item:** Q44 refuses it (rc 2, stdout **0 B**); Q45 **admits** it, lets its legs
run (stdout **575 B**) and still ends rc 2 with live delta 0.
- **The discriminator, on a scratch protected root, rule (3) disarmed.** Two
subjects, both the **real collector** with exactly one change — a synthetic
fixture cannot be the gate's subject, because the suite exercises the
collector's whole interface (tried first, logged below): `collector-altroot.py`
(`HELM_HOME` → `_STACK_ROOT`, 3 → 0 occurrences, 30,833 B / 736 lines /
`fc51eb0986e5b25e…`) and `collector-calltime.py` (`pin_stackchain()` composes
its held directory at call time from a literal, with `HELD_DIR` left in place as
a **decoy** nothing on the write path reads, 31,021 B / 741 lines /
`aa1d73d112cf1674…`). Three variants each: `armed` (redirect target the sandbox,
watch armed), `disarmed` (target the protected root, watch disarmed),
`q44-blindspot` (the Q44 gate, Q39's literal refusal + rule (3) + the watch all
disarmed).
| row | rc | passed | held write lands in | protected held | live / live held |
|---|---|---|---|---|---|
| `q45-armed-collector-altroot` | **0** | `169 / 0` | `<sandbox>/data/held/…` | **0** | 0 / 0 |
| `q45-armed-collector-calltime` | **0** | `169 / 0` | `<sandbox>/data/held/…` | **0** | 0 / 0 |
| `q45-disarmed-collector-altroot` | 1 | `165 / 4` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-disarmed-collector-calltime` | 1 | `165 / 4` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-q44blind-collector-altroot` | 1 | `167 / 2` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-q44blind-collector-calltime` | **0** | `169 / 0` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
**Prevention separated from detection by measurement, on both shapes.** The
sharpest row is `q45-q44blind-collector-calltime`: **rc 0, `169 passed, 0
failed`, `VERDICT: ALL GATES PASS`, and the held write lands in the protected
root** — the pre-Q45 gate reports green while the class writes outside the
sandbox. That is the blind spot, stated by the gate's own verdict rather than by
argument.
- **Live state, delta 0 on all 21 rows** (`out/Q45-NEGATIVES.json`, 88,622 B /
`3b317c56191f4a99…`, `live_delta != 0: none`, `live_held_delta != 0: none`):
snapshot 14,039 B / `aa842b0fd88037f8…` / `mtime_ns` `1791479579475780303` /
`ctime_ns` `1791789490146606356`; held pin 120 B / `5b6d70716153c3f4…` /
`mtime_ns` `1791478687099553071` / `ctime_ns` `1791846636217687791`.
### The incident, disclosed first: the first discriminator run wrote the LIVE held directory
Restored byte-exact, and the cause is a design fault in the variant, not in the
gate. The subject's root literal was `/root/.helm` while the variant's own live
root was the scratch protected root, so the literal sat **outside** the gate's live
root, was never in the redirect's inventory, and the copy the legs ran still named
live. All six discriminator rows in that run re-wrote
`/root/.helm/data/held/stackchain-home.html.sha256` (`live_held_delta: NONZERO` on
every row); the live snapshot was untouched throughout (`live_delta: 0`).
**Restored by `restore_held.py` from `/root/crucible/q43z/backup/held.bak`** —
verified, not trusted: the backup's 120 bytes hash to
`5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297`, the digest
every prior tick recorded, and the script refuses to restore on a mismatch.
Post-restore 120 B / `5b6d7071…` / `mtime_ns` `1791478687099553071`,
`VERDICT: RESTORED BYTE-EXACT`. **`ctime_ns` moved to
`1791846636217687791` — the trace of the restore, which a restore cannot undo
(Q43's discriminator).** Bytes, size and mtime are exact; the inode's change time
is not recoverable.
Three mechanical fixes followed in this tree, and the standing rule the skill
already carries (*a negative control must never run ungated code against a live
default*) is now mechanical here rather than remembered:
1. `make_subjects.py` **retargets** the root literal to the scratch protected
root, asserting the occurrence count before and after.
2. `run_negatives.py`'s **`guard()` refuses any discriminator subject whose code
names the live data root**, using the same walk the redirect uses (string
Constants outside bare string statements), so a subject's docstring may
discuss the live root in prose without tripping it. **It fired during this
build and stopped the run.**
3. `run_negatives.py`'s **`abort_if_live_moved()` stops the whole run** the moment
a live object moves, restoring the held pin first — restoring alone is not
enough, because the run that caused it keeps going and causes it again.
The guard deliberately does not apply to the five negative-control fixtures, whose
purpose is to name the live path; what is under test there is that the gate refuses
them, and the backstop is the measured refusal plus `abort_if_live_moved`.
### Four instrument defects of mine, each caught by its own measurement
1. **The span list visited each AST node once per ancestor** (`ast.walk` nested
inside `_q39_iter_nodes`) — harmless for a dict, fatal for a span list, and it
produced `Path('<sandbox>'le/<sandbox>'le/<sandbox>'le/<sandbox>')` on the
`HELM_HOME` line. Fixed with a single-pass `_q45_constants`, used by both the
inventory and the span list.
2. **The subject header swap stripped the original's first line**, which is the
opening `"""` of its module docstring, so the body came back unterminated, the
closing `"""` opened a new string, and an em-dash was read as code. Fixed by
prepending.
3. **`make_subjects.py`'s truthiness and expectation were both wrong**
(`ok &= bool(...)` on a `PosixPath`; `HELM_HOME` expected 1 where the
call-time subject keeps 3). Both caught by the builder's own assertions.
4. **The `q44blind` variant's PIN block read `Q45_PROTECTED_ROOT`** while the
harness passes `Q44_PROTECTED_ROOT`, so it died with a `KeyError` after the
report block and never printed where the write landed — the one row whose own
evidence was missing.
**Two defects in others' code, found here and not fixed here.** (a) **The Q44 gate
accesses `_q39_tree.body` with no `None` guard**, so a subject that does not parse
crashes it with `AttributeError: 'NoneType' object has no attribute 'body'` instead
of refusing; my Q45 gate guards the same access. Unexercised by any subject in this
run, so it is reported rather than repaired — Q44's gate is not mine to edit
silently. (b) **Q39's `_q39_live_literals` list appends once per ancestor yielded**
(the shape in defect 1), so one literal site printed 12 times; Q39 got away with it
because the list only fed a refusal message, and Q45 turns it into a printed
inventory, so it is **deduped for display** and the count is the distinct site
count. Q39's own list is unchanged.
**Limits, stated in the deliverable §7:** the redirect keys on literals, so a root
with **no** literal is not prevented — check 2 catches the consequence, the watch
catches an actual write after the fact, and rule (3) catches a leg that names the
live path in argv; check 1 is vacuous for a subject with no live literal; the
`q44blind` rows are a statement about Q44's *redirect* with the three other guards
moved out of the way, not about the shipped Q44 gate (which refuses the class —
`q44-neg-n5-live-literal` rc 2, stdout 0 B); the redirect relocates, it does not
sanitise; byte counts, hashes and timings are dated readings (2026-10-12 ~23:00Z);
and the timeout branch and the multi-line-literal case are coded-not-exercised or
recorded as `uncovered` with a reason.
### Blocked
Nothing in my lane. The standing gates are unchanged and all Alexander's: the §4
adoption `cp` of the Helm pair (now collector `4e45021d…` + gate **`2bbca218…`**,
superseding Q44's `18580c10…`), Q9 (checksum handover), Q13 (the wording fixes),
Q19 (wizard onboarding). One thing this run made his, below.
### Next
1. **The pipe's verification of this run**, when it lands. Everything here is
re-runnable from `/root/crucible/q45/`: `make_subjects.py` (needs
`Q45_PROTECTED_ROOT`), `make_variants.py`, `run_negatives.py` and `figures.py`
are self-contained, and `patch_gate.py` reproduces the gate from Q44's in one
asserted pass.
2. **The Helm lane's executable set is empty again.** Q9, Q13, Q19 and the
adoption all wait on the same human call, so the next Helm unit is a decision
rather than a task. The one follow-on this run would name: check 2 catches a
root the literal rule cannot rewrite, but only *after* the copy is built —
resolving every path the subject declares at **runtime**, rather than every
literal it declares statically, is the stronger form and is not built.
3. **Q42** stays open and offered to zephyrus — the non-magazine second pass,
whose value now sits in the fresh-bytes half.
4. **Q7, measured:** no new item opened. Open and gated are Q9 (audio handover),
Q13 and Q19 (Alexander), Q42 (zephyrus's); the archive lane's unclaimed
executable set is empty and the Helm lane's is now empty too.
### Ask
**Alexander** — unchanged and consolidated: (a) rule on the §4 adoption `cp` for
the Helm pair (collector `4e45021d…` + gate **`2bbca218…`**), since Q40 measured
that the deployed gate `7ed1bca8…` reads `HELM_COLLECTOR` **zero** times and none
of Q31–Q45's protection is in production until it lands; (b) the
`stackchain-dashboard.service` 32-byte key repair or a stop; (c) the Q13
publication call, where item 13's decided note (escalate, do not edit the live
page) still controls over its `proposed_action`.
**And one disclosure he should see plainly:** this run wrote the live held pin and
restored it byte-exact. The bytes, size and mtime are exactly what every prior tick
recorded; the inode's `ctime` is not recoverable, and the cause is named above. The
Helm's `manifest_match` was already reading stale before this run (pinned
`f27326e8…` against a live body now `e40308e5…`), so the pin's *value* was already
known to be behind — but the file's identity as a dated artifact was mine to
protect and I moved it.
**The pipe** — nothing owed from this run. One thing to check rather than accept:
`q45-q44blind-collector-calltime`'s `169 passed, 0 failed` alongside
`protected_held_delta: NONZERO` is the whole finding, and it is measured by the
harness's own four-field census plus that row's `Q44-PIN:` line. A re-run of that
one variant costs about 13 seconds and would falsify it if the pin did not land in
the protected root.
+417
View File
@@ -0,0 +1,417 @@
# Q45 — the redirect keys on the resolved value, not on a name
> **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 here or in the files
> this record lands. The long strings are paths, check names, refusal messages,
> sha256 digests and one URL.
---
## 1. What this item was, and what closed it
Q44's redirect keys on the literal line `HELM_HOME =`
(`re.match(r"\s*HELM_HOME\s*=", _l)`), so a subject that composes its live paths
from a **differently-named root**, or builds them **at call time from a literal**,
is outside the substitution entirely. Q45 replaces that rule with one that keys on
the **resolved value**:
> every absolute path literal in the subject that resolves inside the live data
> root is relocated to the same relative path inside the per-invocation sandbox,
> wherever in the module it sits, and whatever the constant holding it is called.
Q44's explicit `HELM_HOME =` assignment rewrite is **subsumed, not removed**: that
line carries the literal, so the literal rule rewrites it, and every constant
composed from it follows by construction. The explicit `SNAPSHOT_PATH =` rewrite
(Q39/Q43's mechanism) is kept, because it is what reaches a subject with no live
literal at all.
**The class is closed by construction for both of the shapes Q45 names**, measured
on a scratch protected root with rule (3) disarmed. §6 is the discriminator.
| artifact | bytes | lines | sha256 |
|---|---|---|---|
| gate `helm-selftest.py` | 105,477 | 1,942 | `2bbca218871bc5613e1fe3c4d3a84fe2e6da05ffc526e51b2865a8c8ad00ee5e` |
| Q44 gate (comparator) | 94,819 | 1,731 | `18580c102ddb1c3f3bd92681402f98ef02f75ee8601c72224c508f9121573c79` |
| subject `helm-collect.py` | 29,954 | 720 | `4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c` |
| harness `out/Q45-NEGATIVES.json` | 88,622 | 1,483 | `3b317c56191f4a9961dc6c8d82bc5fadec563c5040374fba7a280c072afd223c` |
| figures `out/Q45-FIGURES.json` | — | — | `5be33be4d0156a28dd74bdfd9c17ca708dce9813067ab8e7fbf7ec3b904c434d` |
The subject collector is **byte-identical to Q44's** — measured, not asserted
(`collector_identical_to_q44: true` in `out/Q45-FIGURES.json`, by comparing the
two files' sha256). Every change in this item is in the gate.
`diff -u` Q44 gate → Q45 gate = **426 lines = 2 file headers + 7 hunk headers +
284 added + 73 removed + 60 context**, and the decomposition sums to the total
(`sums: true`). 1,731 + 284 − 73 = **1,942** exactly. Rule for the decomposition,
printed because reply 28 was returned against Q44 for getting it wrong: *added =
lines starting `+` minus the `+++` header; removed = `-` minus `---`; the
remainder is `@@` hunks plus context.*
Scratch tree `/root/crucible/q45/`. Nothing deployed, no `cp` into
`/root/.helm/bin/`, no production path touched, `decisions/` and `inbox/`
untouched, no another-party file edited, no secret printed.
---
## 2. The mechanism
Two functions, both defined **before** the Q39 scan that feeds them, because the
scan's result becomes the redirect's inventory.
```python
_q45_live_root = _q39_live_resolved.parent.parent # /root/.helm
def _q45_rel(_lit):
"""<lit> rebased onto the live data root, or None if it sits outside it."""
# normpath, not resolve(): the literals are already absolute and normalised,
# and resolve() would follow a symlink out of the comparison.
```
`_q45_constants(_tree)` visits every string `Constant` **exactly once**, skipping
bare string statements (docstrings) — the same exclusion that keeps the deployed
collector's prose, which names the live path in a comment, out of the scan.
`_q45_rewrite(_src_text, _target_root, _snapshot_dest=None)` returns
`(text, report)`:
1. every `SNAPSHOT_PATH =` line is rewritten to `_snapshot_dest` when one is given
(Q39/Q43's mechanism, unchanged);
2. every absolute literal resolving inside the live root is rewritten to the same
relative path under `_target_root`, **on the parsed node's own span**
(`lineno`/`col_offset`/`end_col_offset`), so the edit is anchored on the AST
rather than on a text search and a literal that also occurs inside a docstring
is left alone.
The report carries the inventory, the span count, and the spans left uncovered
with the reason, so the gate prints what it did instead of asserting that it did
it.
**What one rule covers.** On the deployed collector the inventory is one literal,
`/root/.helm` at line 29, one span relocated:
```
HELM_HOME = Path("/root/.helm") -> HELM_HOME = Path('<sandbox>')
SNAPSHOT_PATH = HELM_HOME / "data" / … -> follows (composed)
HELD_DIR = HELM_HOME / "data" / "held" -> follows (composed)
```
`_STACK_ROOT = Path("<root>")` takes the same rewrite as `HELM_HOME =`, and
`Path("<root>") / "data" / "held"` **inside a function body** takes the same
rewrite on the literal it shares with the module constant.
**Q39's literal refusal is retired for this class, and its reason is printed
where the refusal stood.** The refusal's own text said the rewrite "cannot
redirect a path that is not the module constant the rule keys on" — that is
exactly the statement Q45 makes false. The literals become the redirect's
inventory and the subject is admitted. What the redirect does **not** cover is a
root resolved at runtime from `__file__`, an environment variable or argv, where
there is no literal to rewrite; that class is caught by the module-constant check
(§4, check 2), by the per-leg watch and by rule (3). The refusal is re-pointed,
not deleted.
**The probe copy carries the redirect too.** Before Q45 the probe's rewrite
touched only `SNAPSHOT_PATH`, so a subject whose destination was a literal ran
the probe against the **live** path; Q39's literal scan refused such a subject at
admission, which is why that hole was never measured. The redirect makes the
probe safe on the class, which is what retires the refusal.
---
## 3. The green pair
Five consecutive invocations, `HELM_COLLECTOR=/root/crucible/q45/helm-collect.py
python3 /root/crucible/q45/helm-selftest.py`, run from `/root/crucible/q45/`:
| run | rc | stdout | verdict | seconds | sha256 of the log |
|---|---|---|---|---|---|
| green-1 | 0 | 13,312 B | `169 passed, 0 failed` / `ALL GATES PASS` | 12.20 | `c02adfda1704eea0…` |
| green-2 | 0 | 13,312 B | same | 12.10 | `10972472d772db66…` |
| green-3 | 0 | 13,312 B | same | 12.20 | `51b42c964868bed8…` |
| green-4 | 0 | 13,312 B | same | 12.10 | `389a04e41d2fd1fd…` |
| green-5 | 0 | 13,312 B | same | 12.33 | `deef0ded4a09ac77…` |
**169 = 166 + 3**, unchanged from Q44's count. The three Q44 slots are now two
Q45 checks plus Q44's held-directory check carried **verbatim and still labelled
`Q44`**, so a reader can see which check is new and which is carried.
The `Q45:` foot line, green run 1:
```
Q45: 1 live-data-root literal(s) in the inventory (/root/.helm@line 29),
1 literal span(s) relocated into the sandbox
/root/crucible/q45/helm-legs-<…>, 0 left in the copy, 0 uncovered;
the copy's path-valued module constants: HELD_DIR=<sandbox>/data/held,
HELM_HOME=<sandbox>, PROFILES_DIR=/root/.hermes/profiles,
SNAPSHOT_PATH=<sandbox>/data/snapshot.json,
STANDUP_DIR=/root/troupe-standup/checkins,
TIMMY_REPO=/root/timmy-talking-turd
(6 of 13 module constants are paths; the non-path ones -- the redaction table,
the roster, the schema string -- are not printed, because a gate log is not the
place for them); the live held directory /root/.helm/data/held holds 1 file(s),
unchanged (sha, mtime_ns and ctime_ns compared)
```
`PROFILES_DIR`, `STANDUP_DIR` and `TIMMY_REPO` are deliberately **not** relocated:
they are outside the live data root, so they are not in the inventory, and the
redirect does not touch what it does not protect.
---
## 4. The two Q45 checks
**Check 1 — every live-data-root literal in the subject is relocated in the copy
the legs run.** Condition: the residual (the copy re-parsed) is empty and nothing
is uncovered. The inventory size is printed on every path, because the check is
**vacuous for a subject carrying no live literal at all** and a vacuous pass must
be visible rather than silent — the Q34 lesson.
**Check 2 — no module-level constant of the copy resolves inside the live data
root.** Measured by a subprocess that imports the copy and prints every
module-level name's value; the gate fails on any that resolves inside the live
root. This is the behavioural half and it is what catches a root the literal rule
could not rewrite (composed at runtime from `__file__`, an environment variable or
argv). It is a value, not a source marker.
Q44's check 3 — **the live held directory is unchanged by every leg in the run**,
censused on bytes, sha256, `mtime_ns` and `ctime_ns` before any leg and again at
the foot — is carried verbatim, label included. `ctime` is in the census because
it is the stamp a restore cannot undo (Q43's discriminator).
---
## 5. The five Q39 negatives, still refused
Each fixture run under **both** gates, `HELM_COLLECTOR=<fixture>`:
| fixture | Q45 gate | Q44 gate | live delta (Q45) |
|---|---|---|---|
| `n1-incident-shape` | rc 2, stdout 0 B | rc 2, stdout 0 B | 0 |
| `n2-traceback` | rc 2, stdout 0 B | rc 2, stdout 0 B | 0 |
| `n3-decoy` | rc 2, stdout 0 B | rc 2, stdout 0 B | 0 |
| `n4-comment-only` | rc 2, stdout 0 B | rc 2, stdout 0 B | 0 |
| `n5-live-literal` | rc 2, stdout **575 B** | rc 2, stdout **0 B** | 0 |
**`n5` is the class change, and it is the point of the item.** Q39 **refused** it
(`Q39 REFUSAL: the subject collector … names the live destination as an absolute
literal … No leg is executed.`). Q45 **admits** it, relocates the literal, and
lets its legs run — the Q39 NOTE now reads `1 distinct site(s). Q39 REFUSED this
class; Q45 redirects it, so the literal(s) become the redirect's inventory and
the subject is admitted.` Its rc is still 2 and its live delta still 0, so
admitting the class costs nothing measurable; the 575 B of stdout is the legs
running where before nothing ran.
Q39's admission machinery is otherwise untouched: the behavioural probe, the
per-leg watch and rule (3) all still fire, and n1–n4 are refused exactly as
before.
---
## 6. The discriminator
Two subjects, both the **real collector** with exactly one thing changed, built
by `make_subjects.py`:
| subject | change | bytes / lines | sha256 |
|---|---|---|---|
| `subjects/collector-altroot.py` | `HELM_HOME` → `_STACK_ROOT`, whole word, 3 → 0 occurrences | 30,833 / 736 | `fc51eb0986e5b25e139df75ba2d7478be24fa8518b33ae4df31995aaee5a43a6` |
| `subjects/collector-calltime.py` | `pin_stackchain()` composes its held directory at call time from a literal; the `HELD_DIR` constant stays, still composed from `HELM_HOME`, and is a **decoy** nothing on the write path reads | 31,021 / 741 | `aa1d73d112cf1674f4a1550cae7c40a3986c0cfb4f47149b88ef7c2114b3681b` |
A synthetic fixture cannot be the gate's subject — the suite exercises the
collector's whole interface, and a fixture that stubs it measures the stub. That
was tried first and is logged in §8.
Three variants per subject, built by `make_variants.py`:
| variant | gate | rule (3) | redirect target | watch |
|---|---|---|---|---|
| `armed` | Q45 | disarmed | the per-invocation sandbox | **armed** |
| `disarmed` | Q45 | disarmed | the **protected root** | disarmed |
| `q44-blindspot` | Q44 | disarmed | n/a (Q44's own) | disarmed |
The watch is disarmed in the last two on purpose: a detect-and-restore watch
restores the very write the discriminator measures, which is how a measurement
ends up proving nothing. The measurement is the harness's own before/after census
on (bytes, sha256, `mtime_ns`, `ctime_ns`).
All six rows, `protected` = the scratch root, censused around every invocation:
| row | rc | passed | held write lands in | protected held | live / live held |
|---|---|---|---|---|---|
| `q45-armed-collector-altroot` | **0** | `169 passed, 0 failed` | `<sandbox>/data/held/…` | **0** | 0 / 0 |
| `q45-armed-collector-calltime` | **0** | `169 passed, 0 failed` | `<sandbox>/data/held/…` | **0** | 0 / 0 |
| `q45-disarmed-collector-altroot` | 1 | `165 passed, 4 failed` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-disarmed-collector-calltime` | 1 | `165 passed, 4 failed` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-q44blind-collector-altroot` | 1 | `167 passed, 2 failed` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
| `q45-q44blind-collector-calltime` | **0** | `169 passed, 0 failed` | `<protected>/data/held/…` | **NONZERO** | 0 / 0 |
The `Q45-PIN:` / `Q44-PIN:` line in each row is where "the write landed" is read
from, and both readings are on one line rather than two runs:
```
q45-armed-collector-altroot Q45-PIN: rc=0 … pinned e40308e573d046d7… ->
/root/crucible/q45/variants/helm-legs-oountx_s/data/held/stackchain-home.html.sha256
… sandbox held=120 B / sha256 676fede2c29a381a… / mtime_ns 1791847039674987167
… protected held=114 B / sha256 b58d2d0eb9f45862b… / mtime_ns 1791478687000000000
q45-disarmed-collector-altroot Q45-PIN: rc=0 … pinned e40308e573d046d7… ->
/root/crucible/q45/protected/data/held/stackchain-home.html.sha256
… sandbox held=absent; protected held=120 B / sha256 9b65871d6d8b44dd…
/ mtime_ns 1791847054218188403 / ctime_ns 1791847054222188733
```
**Prevention separated from detection by measurement, on both shapes.** Armed, the
run is green (`169 passed, 0 failed`, `VERDICT: ALL GATES PASS`) and the only held
write in the process tree is inside the run's own sandbox, with the protected root
delta 0 on all four census fields. Disarmed, the same shape lands in the protected
root with a nonzero `mtime_ns` and `ctime_ns`.
**The sharpest row is `q45-q44blind-collector-calltime`: rc 0, `169 passed,
0 failed`, `VERDICT: ALL GATES PASS`, and the held write lands in the protected
root.** The pre-Q45 gate reports green while the class writes outside the sandbox.
That is the blind spot Q45 closes, stated by the gate's own verdict rather than by
argument.
The two `disarmed` rows fail 4 checks and the two `q44blind` rows fail 2 and 0
respectively; those reds are the expected measurement of a disabled or absent
mechanism, not defects. In `q44blind-collector-calltime` nothing fails at all,
which is the finding.
**Live state, delta 0 on all 21 rows.** Snapshot 14,039 B / sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / `mtime_ns`
`1791479579475780303` / `ctime_ns` `1791789490146606356`, before and after every
invocation (`live_delta != 0: none`, `live_held_delta != 0: none`). The live root
is never named by any subject or variant — `run_negatives.py`'s `guard()` refuses
one that does, and it fired during this build (§8).
---
## 7. Limits, stated rather than buried
1. **The redirect keys on literals, so a root with no literal is not covered.**
`HELM_HOME = Path(__file__).resolve().parent`, or any root read from an
environment variable or argv, has nothing to relocate. Check 2 catches the
*consequence* (a module constant resolving inside the live root fails the run),
the per-leg watch catches an actual write after the fact, and rule (3) catches
a leg that names the live path in argv. Prevention for that class is not
claimed.
2. **The `q44blind` variant disarms Q39's literal refusal, rule (3) and the watch
together**, so its rows are not a statement about the shipped Q44 gate — they
are a statement about Q44's *redirect* on a class it cannot see, with the three
other guards moved out of the way. The shipped Q44 gate refuses the class
outright (§5, `q44-neg-n5-live-literal`: rc 2, stdout 0 B).
3. **Check 1 is vacuous for a subject with no live literal.** The inventory size
is printed on every path so the vacuity is visible. No subject this gate runs
is in that class, and the record does not claim a rate for subjects it has not
seen.
4. **The redirect relocates, it does not sanitise.** A subject that writes to a
path derived from a relocated literal writes inside the sandbox, which is the
intent; the sandbox is removed at exit and 0 paths were left behind by any
green run.
5. **Byte counts, hashes and timings are dated readings** (2026-10-12, ~23:00Z).
The `Q45-PIN:` line's pin digest `e40308e5…` is the current live Stackchain
body's normalised digest, which is a property of the remote page, not of this
run.
6. **Not exercised: the timeout branch** in `_q45_rewrite`'s caller, and a
multi-line path literal (recorded as `uncovered` with a reason rather than
silently skipped). No path literal in any subject this gate runs spans lines.
---
## 8. This run's own defects, logged first
**The incident: the first discriminator run wrote the LIVE held directory.**
Restored byte-exact; disclosed in full.
The subject's root literal was `/root/.helm`, while the variant's own live root
was the scratch protected root. So the literal sat **outside** the gate's live
root, was never in the redirect's inventory, and the copy the legs ran still
named live. All six discriminator rows in that run re-wrote
`/root/.helm/data/held/stackchain-home.html.sha256` (`live_held_delta: NONZERO` on
every row); the live snapshot was untouched (`live_delta: 0` on every row).
Restored by `restore_held.py` from `/root/crucible/q43z/backup/held.bak`, which
carries the exact original line. The restore is **verified, not trusted**: the
backup's 120 bytes hash to `5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297`,
which is the digest every prior tick recorded, and the script refuses to restore
if it does not match. Post-restore: 120 B / `5b6d7071…` / `mtime_ns`
`1791478687099553071`, `VERDICT: RESTORED BYTE-EXACT`. **`ctime_ns` moved to
`1791846636217687791` — the trace of the restore, which a restore cannot undo
(Q43's discriminator).** The bytes, size and mtime are exact; the inode's change
time is not recoverable.
Three mechanical fixes followed, all in this tree:
- `make_subjects.py` **retargets** the root literal to the scratch protected root,
asserting the occurrence count before and after. This is not part of the class
under test — it is the discipline Q43/Q44 already carry ("on a scratch
protected path, never the live one"), which this run skipped.
- `run_negatives.py`'s **`guard()` refuses any discriminator subject whose code
names the live data root**, using the same walk the redirect uses (string
Constants outside bare string statements), so a subject's docstring may discuss
the live root in prose without tripping it. It fired during this build and
stopped the run.
- `run_negatives.py`'s **`abort_if_live_moved()` stops the whole run** the moment
a live object moves, restoring the held pin from the backup first. Restoring
alone is not enough — the run that caused it keeps going and causes it again.
The guard deliberately does **not** apply to the five negative-control fixtures,
whose whole purpose is to name the live path; what is under test there is that the
gate refuses them, and the backstop is the measured refusal plus
`abort_if_live_moved`.
**Four instrument defects, each found by its own measurement:**
1. **The span list visited each AST node once per ancestor.** `_q39_iter_nodes`
yields a node *and* its children, so nesting `ast.walk` inside it reaches a
`Constant` once per ancestor — harmless for a dict (Q39 dedupes with
`setdefault`), fatal for a span list. The first rewrite produced
`Path('<sandbox>'le/<sandbox>'le/<sandbox>'le/<sandbox>')` on the `HELM_HOME`
line. Fixed with `_q45_constants`, a single-pass walk, used by both the
inventory and the span list.
2. **The subject header swap stripped the original's first line** to make room,
and that line is the opening `"""` of the module docstring — so the docstring
body came back unterminated, the closing `"""` opened a new string, and
everything after it (an em-dash) was read as code. Fixed by prepending.
3. **`make_subjects.py`'s truthiness and expectation were both wrong**
(`ok &= bool(...)` on a `PosixPath`; `HELM_HOME` expected 1 where the
call-time subject keeps all 3). Both caught by the builder's own assertions.
4. **The `q44blind` variant's PIN block read `Q45_PROTECTED_ROOT`** while the
harness passes `Q44_PROTECTED_ROOT`, so it died with a `KeyError` *after* the
report block and never printed where the write landed — the one row whose own
evidence was missing.
**One defect in the pre-Q45 gate, found here and not fixed here:** the Q44 gate
accesses `_q39_tree.body` with no `None` guard, so a subject that does not parse
crashes it with `AttributeError: 'NoneType' object has no attribute 'body'`
instead of refusing. My Q45 gate guards the same access
(`_q45_copy_tree.body if _q45_copy_tree is not None else []`). No subject in this
run fails to parse, so the defect is **unexercised** and is reported rather than
repaired — Q44's gate is not mine to edit silently.
**One cosmetic defect in Q39's scan, fixed where Q45 reads it:** Q39's
`_q39_live_literals` list appends once per ancestor yielded (the shape in defect
1), so the same literal site was printed 12 times for one site. Q39 got away with
it because the list only fed a refusal message; Q45 turns it into a printed
inventory, so it is deduped for display and the count is the **distinct** site
count. Q39's own list is unchanged.
---
## 9. What this changes for the adoption ask
The §4 pair is now collector **`4e45021d80395d3f…`** + gate
**`2bbca218871bc561…`** (superseding Q44's `18580c102ddb1c3f…`). Q40's finding is
unchanged and is the reason the `cp` matters: the deployed gate `7ed1bca8…` reads
`HELM_COLLECTOR` **zero** times (`grep -c HELM_COLLECTOR
/root/.helm/bin/helm-selftest.py` → 0), so it has no subject-selection path at
all and none of Q31–Q45's protection is in production until the pair lands.
**Q7, measured: after this item the Helm lane's executable set is empty again.**
Q9 waits on the checksum handover, Q13 and Q19 on Alexander, Q42 is
zephyrus-sparkborn's, and the adoption is the same human call. No new item opened.
The one follow-on this run would name if the lane re-opens: check 2 catches a
root the literal rule cannot rewrite, but only *after* the copy is built —
resolving every path the subject declares at runtime, rather than every literal it
declares statically, is the stronger form and is not built.
— Merlin