Files
troupe-standup/recoveries/helm-null-gate.md
T
Merlin a16d9e45be Q34 DONE: the pointer's null case is gated (recoveries/helm-null-gate.md, scratch /root/crucible/q34/). Four checks planted in the gate's own scratch tree between two collects on a profile with sessions/ and logs/ and no files: latest_active_path null, source_path still the profile directory, the key present with value null rather than absent, and a follow-up collect where a planted file sets the pointer while the citation holds; the member analogue stated unreachable by construction. Seven consecutive green gate invocations (exit 0, 164 passed 0 failed each, the Q33 160 plus these four), log digests in the deliverable. Red on both comparators: Q33-before collector under the full gate exit 1 / 153 passed 11 failed (the Q33 nine plus Q34 checks 3 and 4); pre-Q33 collector 2 passed 2 failed on the four checks by module import with no subprocess leg; checks 1-2 pass vacuously on both comparators and are reported as vacuous. The collector did not move (byte-identical to the Q33 pair, 4e45021d, 720 lines); the gate moved +56/-0 (929 -> 985 lines, 45e53187), so the §4 adoption ask is now a three-file ask. One incident logged first: the first red leg ran the full gate against the pre-Q33 collector, which has no destination gate, so the bare --skip-tests leg wrote the live snapshot (aa842b0f -> e4d1ad5, 14039 -> 18524 B, mtime_ns 1791479579475780303 -> 1791764761933369921 at 00:26:01Z); restored byte-exact from /root/crucible/q31/snap-before.json with the mtime re-stamped and the comparator re-run by import only; live snapshot delta 0 across every leg after the restore. Deployed bin untouched. Limits: a live empty profile is unobserved; checks 1-2 vacuous on the comparators; the null value is not schema-constrained.
2026-10-12 00:28:38 +00:00

151 lines
9.7 KiB
Markdown
Raw 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 pointer's null case, gated
**Q34 — collector-side gate. Author: Merlin (`merlin@agents.invalid`). Clock: 2026-10-12T00:24–00:28Z, this host.**
**Scratch tree: `/root/crucible/q34/`. Nothing deployed; the §4 adoption pair is untouched by this run.**
Q33's one `unverified` limit, now exercised. The patched collector emits
`evidence.latest_active_path: null` for a profile with no files under `sessions/` /
`cron/output/` / `logs/`; every planted tree in the Q33 checks has files and the live collect is
20/20 non-None, so the null branch was exercised by nothing in the suite — and on the pre-Q33
collector the two pointer non-vacuity checks pass `0/0` vacuously, since that collector emits
neither pointer key at all. Q34 plants the empty profile inside the gate's own scratch tree,
between two collects, and asserts the three properties the item names plus a fourth that makes
the null a *measured* null rather than a missing field.
## The four checks (gate section `# ---- Q34: the pointer's null case ----`, `helm-selftest.py:866-925`)
The fixture: profile `empty` with `sessions/` and `logs/` present and **no files** anywhere,
planted in a `TemporaryDirectory` under the gate's own `td`, `PROFILES_DIR` rebound exactly as
the Q33 checks do it (`:875-889`), restored in a `finally`. Two collects: before, and after
`sessions/landed.jsonl` is planted at mtime +60 s.
| # | check name (verbatim, as printed) | patched pair | Q33-before collector | pre-Q33 (pregate) collector |
|---|---|---|---|---|
| 1 | `Q34: evidence.latest_active_path is null for a profile with a sessions/ directory and no files` | PASS | PASS (vacuous — `.get()` returns `None` for an absent key) | PASS (vacuous, same reason) |
| 2 | `Q34: the null pointer's evidence.source_path is still the profile directory (the citation survives an empty profile)` | PASS | PASS | PASS |
| 3 | `Q34: the key is present with value null, not absent (a reader can tell 'no files' from 'field missing')` | PASS | **FAIL** `present=False value=None` | **FAIL** `present=False value=None` |
| 4 | `Q34: after a file lands the pointer is set and the citation is unchanged (the null was a real reading, not a missing field)` | PASS | **FAIL** `null -> None; source_path …/empty -> …/empty/sessions/landed.jsonl` | **FAIL** same shape |
Check 3 is the discriminator; checks 1 and 2 are structural on both comparators and pass there
for the stated reason — reported as vacuous passes, not as failures of the comparator. The
member analogue (`evidence.newest_checkin_file: null`) is **unreachable by construction and is
stated, not asserted**: a member row is emitted only for a file that exists under the check-ins
directory, so `collect_troupe()` cannot produce a member with no newest file. The rule is in the
gate as a comment at `:870-875`.
## Consecutive green invocations — 7, not 5
`HELM_COLLECTOR=/root/crucible/q34/helm-collect.py python3 helm-selftest.py`, exit 0 every time,
`helm-selftest: 164 passed, 0 failed` (the Q33 suite's 160 plus these four):
| run | log | sha256 of log |
|---|---|---|
| 1 | `q34-gate-run-1.log` | `7f01728d08227755fddb5baa445b75fff9c4bd8e0fb6bf5596fca2fd938ba5bf` |
| 2 | `q34-gate-run-2.log` | `997b1308242f7fe63ae331f82afbdbde8a822288d61ca54afb9906dfdd0c2205` |
| 3 | `q34-gate-run-3.log` | `96678a66bb5c8e07d8ebc17ef13ef22c83a75390c8514926ae2b84639a6e6912` |
| 4 | `q34-gate-run-4.log` | `f13e5cd600579fbc7e6bd06f781f489b59dff96f377433c8156b31d4ffe52317` |
| 5 | `q34-gate-run-5.log` | `532ab633fb36c5864ff756084e568b8052d348b004e9f57658af86086446bd7f` |
| 6 | `q34-gate-run-6.log` | `00bea9975b861c39ec9d96fcb5f09faf93aad9aa1e8c8f9266848fa069455c8c` |
| 7 | `q34-gate-run-7.log` | `d4e39936e52491ef55317915a28ee20a501faf32f3dddd1d75729399f6aea69e` |
Log digests differ run to run only by clocks and durations; the verdict line is identical in all
seven (`VERDICT: ALL GATES PASS`).
## The empty profile's `evidence` block, before and after
From `Q34-NULL-EVIDENCE.txt` (written by the gate on every run; the digest below is run 7's,
**sha256 `c625bc07068c5040d91117d604208152b33f34f0416e96dfc51d326546c3473c`** — the file is
regenerated each invocation, so it is a dated reading, not a stable artifact):
```
profile dir : /tmp/tmp5r57iyp4/q34-null/profiles/empty (sessions/ and logs/ present, no files)
collector : /root/crucible/q34/helm-collect.py
--- before (no files anywhere under the profile) ---
{
"latest_active_path": null,
"observed_at": "2026-10-12T00:27:09.204Z",
"source_path": "/tmp/tmp5r57iyp4/q34-null/profiles/empty"
}
--- after (sessions/landed.jsonl planted, mtime +60s) ---
{
"latest_active_path": "/tmp/tmp5r57iyp4/q34-null/profiles/empty/sessions/landed.jsonl",
"observed_at": "2026-10-12T00:27:09.206Z",
"source_path": "/tmp/tmp5r57iyp4/q34-null/profiles/empty"
}
```
The key is **present** in the before block with value `null`; the citation does not move.
## The comparators
- **Q33-before collector** (`helm-collect-q33-before.py`, **8a477376f72c578b…**, the Q33-before
collector with the destination gate intact): full gate, **exit 1, `153 passed, 11 failed`**
(`q34-gate-q33before.log`, sha256 `7e53bb46adcdfda5f66c93dd780732fdc89207e5d5073bf56bd5921f3a94f1d5`).
The eleven are the Q33 suite's nine (eight pointer checks + the declared-key source check,
which lists `latest_active_path, newest_checkin_file` as not emitted) plus Q34 checks 3 and 4.
- **Pre-Q33 collector** (`helm-collect-pregate.py`, **ee0b36a82f23fcf3…**, the deployed
collector): Q34 checks only, by module import with `PROFILES_DIR` rebound, **no subprocess leg**
(`q34-red-pregate.py`, output `q34-red-pregate.log`, sha256
`093f60e587186827a77e7f31247735b2048a8e86540a47485acf6324f95f1571`): **2 passed / 2 failed**,
the two failures being checks 3 and 4 as tabulated above.
## Incident, logged first — a bare run of the pre-Q33 collector wrote the live snapshot
The first red leg ran the full gate with `HELM_COLLECTOR` pointed at the **pre-Q33** collector.
That collector has **no destination gate** (Q31's finding), so the gate's own bare
`subprocess.run([sys.executable, COLLECTOR, "--skip-tests"])` leg — the one that asserts a refusal
— executed the collector, which wrote the live snapshot. Measured:
`/root/.helm/data/snapshot.json` went `aa842b0fd88037f8…` → `e4d1ad5293fa58cb…`, 14,039 → 18,524 B,
mtime_ns `1791479579475780303` → `1791764761933369921` (2026-10-12T00:26:01.933369921Z). The run
also aborted at the argparse `--deploy` error, so it produced no usable comparator.
Restored byte-exact from `/root/crucible/q31/snap-before.json` (the Q31 pre-run copy, itself
verified `aa842b0f…` / 14,039 B before the restore): `q34-restore.py` copied the bytes and re-stamped
`mtime_ns` to `1791479579475780303`. Post-restore: **14,039 B / sha256
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns
`1791479579475780303`**, `VERDICT: RESTORED BYTE-EXACT`. The pre-Q33 comparator was then re-run by
module import only. This is the harness incident the skill already warns about — *a negative control
must never run ungated code against a live default* — and it happened anyway; the standing rule is
now mechanical in this tree: no `HELM_COLLECTOR` value naming a pre-gate collector is ever passed
to the full gate.
**Live snapshot delta 0 across every other leg**, including all seven green invocations, the
Q33-before comparator and the import-only pre-Q33 comparator: `/root/.helm/data/snapshot.json` reads
`aa842b0fd88037f8…` / 14,039 B / mtime_ns `1791479579475780303` at every checkpoint after the
restore.
## What moved, and what did not
- **The collector did not move.** `/root/crucible/q34/helm-collect.py` is byte-identical to the Q33
pair's collector: sha256 `4e45021d80395d3f03fc58d7e99610e3c1c0d2fcf9c85f1db50b16cfd2043c6c`,
720 lines / 29,954 B. Q34 is a gate-only change, as the item requires — Q33's adoption pair is
not re-edited under a second item.
- **The gate moved +56 / −0** against the Q33 selftest: `/root/crucible/q31/helm-selftest.py`
(929 lines) → `/root/crucible/q34/helm-selftest.py` (**985 lines**, sha256
`45e53187c01398df7a0edcbf24a08b7d91255eda0cfe93b9cb01e8398f0d7091`).
`git diff --no-index --numstat` → `56 0`. 160 → 164 checks.
- **Deployed bin untouched:** `/root/.helm/bin/helm-collect.py` `ee0b36a82f23fcf3…`,
`helm-selftest.py` `7ed1bca8f9da624f…`, `helm-render.py` `5c67473d89283c3d…`, unchanged.
- **Consequence for §4, stated:** the adoption ask now approves **three** scratch files, not two —
collector `4e45021d…` (unchanged from Q33), selftest `4a442f62…` (Q33) **superseded by**
`45e53187…` (this item). Alexander's decision should be read as covering the pair-plus-gate.
## Limits
1. **A live empty profile is still unobserved.** Every profile under
`/root/.hermes/profiles/` currently has files; the null branch is exercised by the gate's
fixture, not by production data. Measured non-destructively (Q33's import-based probe, this
host, 2026-10-11) and now by the gate itself.
2. **Checks 1 and 2 pass vacuously on both comparators** — they hold for an absent key and for a
pointer-less citation fallback. The discriminating checks are 3 and 4, and both are red on both
comparators. Stated so the `2/2` and `2/4` red figures are not read as a weaker gate than they
are.
3. **The `latest_active_path: null` value is not schema-constrained.** Q33 measured that the
schema's `$defs.evidence` has no `additionalProperties: false`; nothing asserts the field's
type, only its presence and its null-ness in this fixture. Out of Q34's scope.
4. **Testimony only, not verified this run:** the Q33 suite's own checks above the Q34 section
(160 of the 164) were not re-derived here; this run measures that the Q34 section is green,
discriminating, and non-destructive.