Files
troupe-standup/recoveries/helm-runwide-census-hoist.md
Merlin 57d8e6bcc7 Q51: the run-wide census hoist
The ordering defect the class hid behind, closed: the run-wide held census
(_q44_held_before) was read ~470 lines after the admission probe spawn, so the
run-wide instrument structurally could not see a probe write. Measured on the
base gate first (Q44 PASS while the guard reports the live pin MOVED), then
fixed by a pure move of the census machinery above the probe branch.

Gate e4967da9bdc4537802993523c7044a762a9a6366576df4e4234444789310107e
(134,427 B / 2,558 lines / 125 registrations) on the Q50 gate a951dbe8...
diff = 2 hunks + 33 added - 27 removed; 2552 + 33 - 27 = 2558 exactly.
Disarmed twin b1d96976... (134,378 / 2,557 / 125).

Cells: armed x named 166/9 with Q44 PASS and Q50 PASS, delta {} on both
objects; disarmed x named 164/11 with Q44 FAIL and Q50 FAIL, the run-wide
check naming the moved held pin and failing on ctime alone after the window
detector's byte-exact restore. Green pair 5/5 at 175/0, 13,929 B. Regressions
11/11 (latin1 and bom green at 13,929 B). Live delta 0 on bytes/sha256/mtime
across all 32 invocations; ctime movement disclosed; 0 sandboxes.

Still open: Merlin's Q45 s9 follow-on (runtime path resolution) and the s4
adoption cp (collector 4e45021d + gate e4967da9), the manifest_match pin, and
the Q13 publication call -- all Alexander's.

Nothing deployed; decisions/ and inbox/ untouched; no another-party file
edited; no secret printed.
2026-10-13 13:40:02 +00:00

12 KiB
Raw Permalink Blame History

Q51 — the run-wide census, hoisted above the admission probe

Screen. Merlin, Helm lane, 2026-10-13, this host. Every figure below was measured this run; the three things I could not measure are labelled. Nothing deployed, no cp into /root/.helm/bin/, decisions/ and inbox/ untouched, no another-party file edited, no secret printed.


1. The item

Q51 — OPEN (pipe, 2026-10-13, unclaimed). Give the gate a run-wide instrument for the probe window. The queue's own text: "the run-wide held census (_q44_held_before, q50:1388) is taken after the admission probe (q50:917), so the per-run instrument structurally cannot see a probe write." Done means, verbatim from the item:

with the disarmed discriminator (6b1c431f…) and Q48_ROOT=/root/.helm, the run-wide check also fails and names the moved object; with the shipped Q50 gate (a951dbe8…) in the same cell, run-wide and window delta are both {}; the 2×2 and the 11 regressions re-recorded; the green pair still 175 / 0.

The defect, measured on the base gate before anything was changed. In base-q49-named (the Q49 gate 085ddf4f…, subject env-root-v1.py, Q48_ROOT=/root/.helm) the admission probe wrote the live held pin — the guard's own delta record says MOVED and it restored byte-exact — and the run-wide Q44 check passed (165 passed / 9 failed, VERDICT: SELF-TEST FAILED for other reasons). The run-wide instrument's baseline was read after the write, so before == after. The live write was caught only by the guard, which lives outside the gate. That is the ordering defect in its purest form, and it is the row the fix has to flip.

The ordering, cited to the line (Q50 gate a951dbe8…, 134,086 B / 2,552 lines): the admission probe's spawn at :917, the run-wide held census read _q44_held_before = _q44_held_census() at :1388 — 471 lines later. The Q50 per-window detector at :913/:933 covers the window; the run-wide census covers only what happened after it was taken.

2. The pair

Collector untouched — the change is in the gate only, exactly as the item's note requires ("the gate is Merlin's file"). Gate e4967da9bdc4537802993523c7044a762a9a6366576df4e4234444789310107e (134,427 B / 2,558 lines / 125 check( registrations) on the Q50 gate a951dbe87d3394e030e16139781d447d86958818c186cc8ee296e4ed23ebaa10 (134,086 / 2,552 / 125).

out/DIFF-vs-q50.txt (76 lines / sha256 c8b2ac1367fbe5f2…): 2 hunks + 33 added − 27 removed, and 2,552 + 33 − 27 = 2,558 exactly (diff -u, counted by the patch script's own decomposition rule: added = ^+ minus +++, removed = ^- minus ---). The diff is a pure move: the Q44 census machinery (the _q44_held_live path, _q44_held_census(), and the _q44_held_before read) was cut from its old site — leaving a dated note — and re-inserted with the Q51 rationale immediately before the probe branch. The foot check (check("Q44", …) at :2476, unchanged byte-for-byte) stays the run-wide instrument, which is why the green count does not move: 125 registrations in, 125 out, check("Q44" still exactly 1, check("Q50" still exactly 2.

The ordering, after the patch (Q51 gate): hoisted before-read at :890, probe branch at :893, probe spawn at :948, foot census at :2477 — asserted by the patch script (i_before < i_branch < i_spawn < i_foot), not by inspection.

The disarmed twin — the same one-line cut Q50's twin carried (the spawn's env= removed): b1d969767f347d179ce14df1803bef1fa409d880beeafc8b898e34cf57d63add (134,378 B / 2,557 lines / 125 registrations), 49 B and one line smaller than the armed gate, the same relationship Q50's pair had against 6b1c431f… (134,037 / 2,551). Removing the environment hand-off changes nothing the file counts, which is the cleanest way to see that the twin isolates the hand-off rather than the suite.

3. The discriminator — five cells, subject env-root-v1.py, HELM_COLLECTOR pointed at it

run_matrix_q51.py → out/Q51-MATRIX.json; every row's logs saved as out/<row>.{out,err}.

row gate Q48_ROOT rc passed / failed Q44 run-wide Q50 window guard live delta
base-q49-named Q49 085ddf4f… /root/.helm 1 165 / 9 PASS (the defect) n/a (base gate has no Q50 check) MOVED, restored
q51-armed-unset Q51 e4967da9… unset 1 166 / 9 PASS PASS 0
q51-armed-named Q51 e4967da9… /root/.helm 1 166 / 9 PASS PASS 0
q51-dis-unset Q51-disarmed b1d96976… unset 1 166 / 9 PASS PASS 0
q51-dis-named Q51-disarmed b1d96976… /root/.helm 1 164 / 11 FAIL FAIL 0 (restored in-gate)

The item's two cells, verbatim:

  • Blank cell, shipped gate: q51-armed-named — run-wide and window both clean, live delta {} on both objects across the whole matrix (final live delta {}). The redirect fires and nothing moves.

  • Disarmed discriminator: q51-dis-named — the Q50 window detector fires first and prints the moved object, restores byte-exact, and fails Q50 the admission probe's spawn moved neither live object; the run-wide Q44 check then also fails and names the same object:

    FAIL Q44 the live held directory is unchanged by every leg in the run the held directory changed: before={"/root/.helm/data/held/stackchain-home.html.sha256": [120, "5b6d70716153c3f4…", 1791478687099553071, **1791898508989942384**]} after={"…": [120, "5b6d70716153c3f4…", 1791478687099553071, **1791898514021357042**]}

The finding this run produced, and it is stronger than the item asked for. Before and after are identical on size, sha256 and mtime — the window detector restored byte-exact — and the run-wide check fails on ctime alone (1791898508989942384 → 1791898514021357042). The run-wide instrument cannot be fooled by a restore, because ctime is the stamp a restore cannot undo (Q43's discriminator, now doing load-bearing work in the run-wide instrument rather than only the window one). Two honest caveats: (a) the run-wide failure in that cell is attributable to the restore trace, not to a persisted byte change — I state both, and the Q50 window check is what names the write itself; (b) this property is inherited from the census's four-field shape, which the move did not alter.

The base row is the defect's receipt and stays in the matrix as the control: the run-wide check passes on the base gate while the guard, outside the gate, reports the live pin moved.

4. Green pair — five consecutive invocations

run_green_q51.py → out/Q51-GREEN.json, logs out/q51-green-{1..5}.{out,err}. Subject /root/crucible/q48/helm-collect.py (4e45021d…, the Q33–Q48 collector — not the deployed ee0b36a8…), Q48_ROOT unset.

rc 0 on all five, 175 passed / 0 failed, VERDICT: ALL GATES PASS, 13,929 B stdout each, 12.41–13.04 s, live delta {} on both objects each run. 175 = the Q50 green count exactly; no pre-existing check moved and no check was added — the hoist is invisible on the subject class that needs no redirect. The green log carries both instruments green: PASS Q44 the live held directory is unchanged by every leg in the run and PASS Q50 the admission probe's spawn moved neither live object.

5. Regressions — all 11 hold

run_regressions_q51.py → out/Q51-REGRESSIONS.json. Every negative fixture the lane has accumulated, expected rcs from the Q46/Q47 records: n1–n5, noparse-with-markers.py, dep-unparseable.py, collector-undecodable.py, broken_dep.py rc 2 (Q39 refusals / Q46 parse guard / repair 3 / Q47 read path); collector-latin1.py and collector-bom.py green at 13,929 B — the same size as the green pair, i.e. the read-path fix's subjects are unaffected by the hoist. Live delta {} on all 11. VERDICT: ALL REGRESSIONS HOLD.

6. Live state, delta 0 across the whole run

Measured after every row (final_state.py):

  • snapshot /root/.helm/data/snapshot.json — 14,039 B / sha256 aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d / mtime_ns 1791479579475780303 — identical to every prior run's record;
  • held pin /root/.helm/data/held/stackchain-home.html.sha256 — 120 B / sha256 5b6d70716153c3f4bd82045d5d71edf88f793a240ce1275c3dfca5759d0ee297 / mtime_ns 1791478687099553071.

Bytes, sha256 and mtime_ns are unchanged on both objects across all 32 invocations (5 green + 5 matrix + 11 recorded regressions + 11 void runs, §7 defect 1). The ctime_ns values moved on both objects (1791898508989942384 / 1791898514021357042) — the dis-named row's in-gate restores and the guard's restores; a restore cannot undo ctime, disclosed rather than hidden. Final tree: 0 sandboxes (glob for helm-legs-* and helm-q39-* → []). Host disk 45 GiB free of 153 GiB. Deployed /root/.helm/bin/ untouched (ee0b36a8… / 7ed1bca8… / 12570786… / 73869998…), so none of Q31–Q51 is in production.

7. Defects of mine, logged first

  1. The regression runner's first run was void. The q51 tree was built by copying the runners, not the fixtures/ directory, so all 11 "regressions" ran against nonexistent paths and were refused by Q35's source precondition (0 B stdout, Q35 REFUSAL: … carries no destination rule). No gate body executed, live delta 0 throughout; re-run after cp -r the fixtures, all 11 as recorded in §5. The void logs are not on disk.
  2. The matrix JSON's Q44/Q50 cells are empty — the extractors matched lines starting with Q44 /Q50 while the foot prints PASS Q44 … / FAIL Q44 …, the same class of comparator defect the Q50 record logged against its own figures script. out/Q51-MATRIX.json keeps the empty cells (not edited after the fact); §3's table is re-derived from the saved logs by show_matrix.py, and no gate invocation was re-run for it.
  3. rm -rf fixtures/__pycache__ is blocked in this shell (recursive delete, single-query mode), so the copied fixtures tree carries its __pycache__ directory. Harmless — q50's tree carries the same — and it is deployment hygiene only, which the adoption cp must not copy in (already logged as C32 by the pipe).
  4. The green runner's assertion is weaker than the record implies. assert not delta catches a live move, but the ctime trace is not part of the guard's delta (bytes + sha256 only), which is why §6 states the ctime movement as a disclosure rather than deriving it from the guard.

8. What this does not close

  • The Q45 §9 follow-on stays open and unclaimed: resolve every path the subject declares at runtime, rather than rewriting the literals it declares statically. The hoist instruments the run; it does not relocate a runtime-composed root.
  • The §4 adoption cp is unchanged and still Alexander's: collector 4e45021d… + gate e4967da9… (superseding a951dbe8…). Q40's finding stands — the deployed gate 7ed1bca8… reads HELM_COLLECTOR zero times — so the cp remains the only thing that puts Q31–Q51's protection into production.
  • The manifest_match pin (stale since 2026-10-08; a static render cannot say so) and the Q13 publication call (item 13's decided note controls over its proposed_action) stand.

9. Reproducibility

Everything re-runs from /root/crucible/q51/: patch_gate_q51.py (count-asserted edits; aborts on a miscount) builds the gate and the twin from the Q50 tree; run_green_q51.py, run_matrix_q51.py, run_regressions_q51.py are self-contained; show_matrix.py re-derives the check outcomes from the saved logs; final_state.py re-measures the live objects. Digests are printed by the patch script, not asserted from memory.