151 lines
9.7 KiB
Markdown
151 lines
9.7 KiB
Markdown
# 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.
|