Q32 DONE: the G8 two-run agreement check is deterministic — declared scrub set covers every clock-derived field (age_seconds, last_active_at, last_checkin_at, the newest-file pointer), 6 consecutive green gate invocations, 8 paired runs with zero differing leaf paths; collector 8a477376 unchanged, adoption identity is now collector 8a477376 + selftest 9ce6a4e9
This commit is contained in:
1 parent
4f5eecc71b
commit
7e12c82506
3 files changed
+234
No files matched your search
@@ -4314,3 +4314,25 @@ zero differing leaf paths across ≥5 pairs. Scratch tree: `/root/crucible/q31/`
|
||||
`8a477376…` (collector) and `1d58b823…` (selftest) until Alexander's `cp`.
|
||||
**Status: OPEN — named to the Helm lane**, ahead of the Q31 `cp` decision so that the gate that ships is the one that
|
||||
runs green deterministically.
|
||||
|
||||
**Status: DONE 2026-10-11 (Merlin, Run 8) — `recoveries/helm-g8-scrub.md`. The gate is deterministic: 6 consecutive
|
||||
green invocations (150 passed / 0 failed each), 8 paired runs with zero differing leaf paths, and the defect measured
|
||||
wider than stated.** Root cause went beyond `age_seconds`: the pre-fix paired probe (age-only patch) was still 7 of 8
|
||||
pairs red on `profiles[].last_active_at` (`helm-collect.py:208`, newest mtime over `sessions//cron/output//logs/` —
|
||||
changes whenever a live session writes) and `profiles[].evidence.source_path` (`:200`/`:212` — not a citation but the
|
||||
**pointer to that newest file**, volatile by construction), plus `members[].last_checkin_at` (`:493`, same class). The
|
||||
fix is selftest-only, the collector untouched: `SCRUB_KEYS` is declared and every key is verified present as a literal
|
||||
in the collector source; the pointer rule nulls `evidence.source_path` only inside dicts carrying a live mtime reading,
|
||||
with a check proving 12 stable citations **survive**; every key carries its fresh-collect live count in the check name
|
||||
and the pre-Q32 scrub runs as a control proving the old set kept what the new one nulls. Control measurement, same 8
|
||||
pairs under the pre-Q32 scrub: 12 differing leaf paths, 4 of 8 pairs red. Gate: `HELM_COLLECTOR=/root/crucible/q31/
|
||||
helm-collect.py python3 helm-selftest.py` → 6 runs, each exit 0 / `helm-selftest: 150 passed, 0 failed` / `VERDICT: ALL
|
||||
GATES PASS` (137 standing + 13 new Q32 assertions; G8 now 31 checks), one log digest per run in the deliverable. Diff
|
||||
`helm-selftest-q32-before.py` `1d58b823…` → `helm-selftest.py` `9ce6a4e971525fdd…` (+115/−5, `DIFF-Q32.txt`). Live
|
||||
snapshot delta 0 throughout (`aa842b0f…`, 14,039 B, mtime_ns `1791479579475780303`); deployed bin unchanged
|
||||
(`ee0b36a8…` collector, `7ed1bca8…` selftest — nothing deployed); collector artifact `8a477376…` unchanged. Limits
|
||||
stated in the deliverable: the enumeration is source-verified, not proven complete (the gate fails closed on any new
|
||||
volatile field); the `evidence.source_path`-as-mutable-pointer design smell is a collector-side follow-up, not made
|
||||
here. **Adoption ask (Alexander, §4):** `cp` the collector `8a477376…` plus the selftest **`9ce6a4e9…`** (superseding
|
||||
`1d58b823…`) into `/root/.helm/bin/` — Q31's adoption is unblocked behind it; deploy/schedule of collect+render still
|
||||
his call.
|
||||
@@ -1002,3 +1002,82 @@ edited; no secret printed.
|
||||
|
||||
— Merlin
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Run 8 — Q32: the G8 two-run agreement check is deterministic (Merlin, 2026-10-11 ≈19:5x–20:2x UTC)
|
||||
|
||||
> Screen. Every claim below carries a path, a digest, or a command with its output. No secret is
|
||||
> printed here or in the files this lands.
|
||||
|
||||
## Did
|
||||
|
||||
Claimed and finished **Q32 — the G8 two-run agreement check is nondeterministic** (the queue's only
|
||||
OPEN item, named to the Helm lane; Q20 is zephyrus-sparkborn's, Q9 blocked on the audio handover,
|
||||
Q13/Q19 gated on Alexander). Deliverable: `recoveries/helm-g8-scrub.md`, scratch tree
|
||||
`/root/crucible/q31/` (re-runnable: `q32-probe.py`, `q32-paired.py`).
|
||||
|
||||
**Root cause, and it is wider than the item stated.** The scrub covered `observed_at`,
|
||||
`rtt_seconds`, `generated_at` and `collector.duration_seconds` — and missed `age_seconds`
|
||||
(`helm-collect.py:545`, `round(age, 1)` from the run-end clock, on every source block). But a
|
||||
paired-run probe with an age-only patch was **still 7 of 8 pairs red**, on
|
||||
`profiles[].last_active_at` (`:208` — newest mtime over `sessions//cron/output//logs/`, which
|
||||
changes whenever a live session writes), `profiles[].evidence.source_path` (`:200`/`:212` — not a
|
||||
citation but the **pointer to that newest file**, volatile by construction) and
|
||||
`members[].last_checkin_at` (`:493`, same class).
|
||||
|
||||
**The fix, selftest-only — the collector artifact `8a477376…` is untouched.** `SCRUB_KEYS` is
|
||||
declared and every key is verified present as a literal in the collector source; the pointer rule
|
||||
nulls `evidence.source_path` only inside dicts carrying a live mtime reading, with a check proving
|
||||
**12 stable citations survive** the scrub; every key prints its fresh-collect live count in the
|
||||
check name and the pre-Q32 scrub runs alongside as a control proving the old set kept what the new
|
||||
one nulls. Diff `1d58b823…` → `9ce6a4e971525fdd…`, +115/−5 (`DIFF-Q32.txt`).
|
||||
|
||||
**Results, measured:**
|
||||
|
||||
- **8 paired runs, 0 differing leaf paths** under the gate's own `_scrub` (extracted from the
|
||||
selftest source by AST and exec'd — the paired script cannot drift from the gate). Control, the
|
||||
same 8 pairs under the pre-Q32 scrub: **12 differing leaf paths, 4 of 8 pairs red**.
|
||||
- **6 consecutive gate invocations**, `HELM_COLLECTOR=/root/crucible/q31/helm-collect.py python3
|
||||
helm-selftest.py`, each exit 0 / `helm-selftest: 150 passed, 0 failed` / `VERDICT: ALL GATES
|
||||
PASS` (137 standing + 13 new Q32 assertions; G8 now 31 checks). One log digest per run in the
|
||||
deliverable: `bfcc35cd92484acb`, `0a6272e3f6d88a96`, `bcf24e51049e661e`, `b9740dbd33f9f494`,
|
||||
`f69d56ef176b2eaf`, `4bf31912f396161d`.
|
||||
|
||||
**Nothing touched.** Live snapshot `/root/.helm/data/snapshot.json` delta 0 throughout
|
||||
(`aa842b0f…`, 14,039 B, mtime_ns `1791479579475780303`, before and after every run); deployed bin
|
||||
unchanged (`ee0b36a8…` collector, `7ed1bca8…` selftest — nothing deployed); live ledger index
|
||||
`https://alexanderwhitestone.com/stackchain/blocks/` 200 / 34,488 B /
|
||||
`4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e`; `decisions/` and `inbox/`
|
||||
untouched; nothing executed; no another-party file edited.
|
||||
|
||||
## Blocked
|
||||
|
||||
Nothing blocked the run. Limits, stated in the deliverable: the scrub enumeration is
|
||||
source-verified, not proven complete (the gate fails closed on any *new* volatile field — a pair
|
||||
goes red and names the leaf — but nothing proves the seven literals are all the clock-derived keys
|
||||
the collector will ever emit); the `evidence.source_path`-as-mutable-pointer design smell is a
|
||||
collector-side follow-up, not made here, because the collector is the artifact under adoption and
|
||||
Q32 deliberately does not change it; the gate runs with `--skip-tests` (the standing Q31 caveat).
|
||||
|
||||
## Next
|
||||
|
||||
- **Alexander's call, §4:** adopt the gated collector `8a477376…` **plus the selftest
|
||||
`9ce6a4e9…`** (superseding `1d58b823…`) into `/root/.helm/bin/` — Q31's adoption is unblocked
|
||||
behind it — then the deploy/schedule decision for collect+render. Same gate as Q26–Q30.
|
||||
- **Q7, measured:** after Q32 the queue's only OPEN item is none; Q20 is zephyrus-sparkborn's named
|
||||
lane, Q9 blocked on the audio handover, Q13/Q19 gated on Alexander. The unclaimed executable set
|
||||
is empty again, so a successor is owed; the natural candidate is the collector-side redesign of
|
||||
the newest-file pointer (`evidence.source_path` as a stable citation plus a named
|
||||
`latest_active_path` field) — this run's own residue, so per the standing pattern I open none and
|
||||
name it instead.
|
||||
- The deploy-or-retire ask for the honest Helm page (Run 2) and the adopt-or-hold asks on the
|
||||
patched builder (Q26–Q29) all still stand unchanged.
|
||||
|
||||
## Ask
|
||||
|
||||
1. **Alexander — adopt the gated collector + the fixed selftest?** The identity pair is now
|
||||
`8a477376…` (collector, unchanged) and `9ce6a4e9…` (selftest, the Q32 fix). It is the difference
|
||||
between a gate whose red is a coin flip and one with 6 consecutive green invocations behind it.
|
||||
Scratch until you say so.
|
||||
2. **Nothing asked of zephyrus-sparkborn this tick.** His lane is Q20.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Q32 — the G8 two-run agreement check is deterministic
|
||||
|
||||
> Screen. Every claim below carries a path, a digest, or a command with its output, measured this run
|
||||
> (2026-10-11 ≈19:5x–20:2x UTC) from this host on the scratch tree `/root/crucible/q31/`. No secret is
|
||||
> printed here or in the files this lands.
|
||||
|
||||
## The defect
|
||||
|
||||
G8 asserted "two CLI runs agree byte-for-byte once the clock fields are scrubbed", comparing two real
|
||||
subprocess collects. The scrub covered `observed_at`, `rtt_seconds`, the top-level `generated_at` and
|
||||
`collector.duration_seconds` — and nothing else. `helm-collect.py:545` stamps
|
||||
`age_seconds = round(age, 1)` on **every source block** from the run-end clock, so any run pair
|
||||
straddling a 0.1 s boundary in a source's age disagreed on that leaf alone. A gate whose red is a coin
|
||||
flip cannot be cited as evidence that an artifact is safe, and the Q31 adoption ask rests on exactly
|
||||
that citation.
|
||||
|
||||
## Root cause, and it is wider than `age_seconds`
|
||||
|
||||
Before patching, a paired-run probe ran with an `age_seconds`-only patch: **7 of 8 pairs still red**,
|
||||
on two further volatile fields the scrub also missed:
|
||||
|
||||
- `profiles[].last_active_at` (`helm-collect.py:208`) — newest mtime across `sessions/`,
|
||||
`cron/output/`, `logs/`. Changes whenever a live session writes a file, i.e. constantly on this host.
|
||||
- `profiles[].evidence.source_path` (`helm-collect.py:200`, `:212`) — **not a citation but a pointer**:
|
||||
the path of that newest file. Volatile by construction wherever a live mtime reading exists. Every
|
||||
*other* `evidence.source_path` in the snapshot is a stable citation.
|
||||
- `members[].last_checkin_at` (`helm-collect.py:493`) — check-in file mtime, the same class (the
|
||||
`checkins/` directory is written by other agents). It did not differ in this run's 8 pairs; it is
|
||||
the same class and is covered.
|
||||
|
||||
## The fix — selftest only, the collector is untouched
|
||||
|
||||
- `SCRUB_KEYS = {observed_at, rtt_seconds, age_seconds, last_active_at, last_checkin_at}`, **declared**.
|
||||
A gate check verifies every key appears as a literal in the collector source (with `generated_at` and
|
||||
`duration_seconds` handled explicitly at the top level), and a second check that the declared set is
|
||||
exactly the clock literals minus the two top-level keys.
|
||||
- **The pointer rule:** `evidence.source_path` is nulled inside any dict carrying `last_active_at` or
|
||||
`last_checkin_at`. A gate check proves the carve-out is measured, not asserted: **12 stable
|
||||
`evidence.source_path` citations survive the scrub** (the 24 newest-file pointers are nulled).
|
||||
- **Non-vacuity per key:** each check name prints the fresh collect's live (non-None) value count, and
|
||||
the **pre-Q32 scrub runs alongside as a control** proving the old set kept every value the new one
|
||||
nulls. (`email.age_seconds` is legitimately `None` — UNREACHABLE source — so 5/6 is the honest count.)
|
||||
- Diff: `helm-selftest-q32-before.py` (sha256 `1d58b823809aa76e…`) → `helm-selftest.py`
|
||||
(sha256 `9ce6a4e971525fdd4e49150fbeb49e584b7eb9cad293164f060a6f2c47739562`), `DIFF-Q32.txt`,
|
||||
**115 lines added / 5 removed**.
|
||||
|
||||
## Results, measured
|
||||
|
||||
**Paired runs** — `q32-paired.py`, 8 pairs, two real subprocess collects per pair, both bodies scrubbed
|
||||
with the **gate's own `_scrub`** (extracted from `helm-selftest.py` by AST and exec'd, so the paired
|
||||
script cannot drift from the gate), compared leaf by leaf:
|
||||
|
||||
- **8 pairs, 0 differing leaf paths** under the gate's scrub.
|
||||
- Control, same 8 pairs under the pre-Q32 scrub: **12 differing leaf paths, 4 of 8 pairs red**
|
||||
(`sources.cron_jobs.age_seconds`, `sources.hermes_profiles.age_seconds`,
|
||||
`profiles[N].last_active_at`, `profiles[N].evidence.source_path` on actively-written profiles).
|
||||
Log: `q32-paired.txt`.
|
||||
|
||||
**Gate invocations** — `HELM_COLLECTOR=/root/crucible/q31/helm-collect.py python3 helm-selftest.py`,
|
||||
6 consecutive runs, each **exit 0, `helm-selftest: 150 passed, 0 failed`, `VERDICT: ALL GATES PASS`**
|
||||
(137 standing + 13 new Q32 assertions; G8 is now 31 checks). One log's digest per run:
|
||||
|
||||
| run | exit | passed / failed | log sha256 (first 16) |
|
||||
|---|---|---|---|
|
||||
| 1 | 0 | 150 / 0 | `bfcc35cd92484acb` |
|
||||
| 2 | 0 | 150 / 0 | `0a6272e3f6d88a96` |
|
||||
| 3 | 0 | 150 / 0 | `bcf24e51049e661e` |
|
||||
| 4 | 0 | 150 / 0 | `b9740dbd33f9f494` |
|
||||
| 5 | 0 | 150 / 0 | `f69d56ef176b2eaf` |
|
||||
| 6 | 0 | 150 / 0 | `4bf31912f396161d` |
|
||||
|
||||
The digests differ run to run **by construction**: the collector's stdout carries the digest of the
|
||||
file it wrote, and the written body carries the unscrubbed clock fields. Each run's digest is the
|
||||
evidence for that run.
|
||||
|
||||
New assertions, with their fresh-collect counts from run 1's log (`q32-gate-run-1.log`):
|
||||
|
||||
```
|
||||
PASS G8 two CLI runs agree byte-for-byte once the clock fields are scrubbed
|
||||
PASS G8 every clock-derived key the collector emits appears in its source
|
||||
PASS G8 the declared scrub set is exactly the clock literals minus the two top-level keys
|
||||
PASS G8 the scrub normalises age_seconds (fresh collect: 5/6 non-None)
|
||||
PASS G8 the pre-Q32 scrub kept age_seconds (the defect, as a control)
|
||||
PASS G8 the scrub normalises last_active_at (fresh collect: 20/20 non-None)
|
||||
PASS G8 the pre-Q32 scrub kept last_active_at (the defect, as a control)
|
||||
PASS G8 the scrub normalises last_checkin_at (fresh collect: 4/4 non-None)
|
||||
PASS G8 the pre-Q32 scrub kept last_checkin_at (the defect, as a control)
|
||||
PASS G8 the scrub normalises observed_at (fresh collect: 36/36 non-None)
|
||||
PASS G8 the scrub normalises rtt_seconds (fresh collect: 1/1 non-None)
|
||||
PASS G8 the newest-file pointer evidence.source_path is scrubbed (fresh collect: 24/24 non-None)
|
||||
PASS G8 the pre-Q32 scrub kept the newest-file pointer (the defect, as a control)
|
||||
PASS G8 stable evidence.source_path citations survive the scrub (12 paths)
|
||||
```
|
||||
|
||||
## Nothing touched
|
||||
|
||||
- The gated collector `/root/crucible/q31/helm-collect.py` is unchanged at
|
||||
`8a477376f72c578b0633e07741b44c717482fe62b29a3cc3f8abb7a6696ea76c`, verified before and after every
|
||||
run this tick. Q32 changes the selftest only.
|
||||
- The deployed `/root/.helm/bin/helm-collect.py` is unchanged at `ee0b36a82f23fcf3…` and
|
||||
`/root/.helm/bin/helm-selftest.py` at `7ed1bca8f9da624f…` (the deployed gate is still the 119-pass
|
||||
one; nothing was deployed).
|
||||
- The live snapshot `/root/.helm/data/snapshot.json` is **14,039 B / sha256
|
||||
`aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns
|
||||
`1791479579475780303`** — byte- and mtime-identical before and after the paired runs and all six
|
||||
gate invocations (delta 0).
|
||||
- The live ledger index `https://alexanderwhitestone.com/stackchain/blocks/` is unchanged: **200 /
|
||||
34,488 B / sha256 `4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e`**.
|
||||
- `blocks.json`, `decisions/` and `inbox/` were not touched; nothing was executed; no another-party
|
||||
file was edited; no secret is printed in this record or in the files it lands.
|
||||
|
||||
## Limits, stated not hidden
|
||||
|
||||
- **The enumeration is declared and source-verified, not proven complete.** The gate fails closed on
|
||||
any *new* volatile field (a pair goes red and the leaf path is named in the failure), but nothing
|
||||
proves a priori that the seven clock literals are all the clock-derived keys the collector will ever
|
||||
emit.
|
||||
- **The pointer rule is a scrub convention; the design smell stands.** For a profile or a member,
|
||||
`evidence.source_path` is a mutable newest-file pointer where every other `evidence.source_path` is
|
||||
a stable citation. The clean fix is collector-side — point `source_path` at the profile directory
|
||||
(or the member's file) and name the newest-file path its own field. The collector is the artifact
|
||||
under adoption and Q32 deliberately does not change it.
|
||||
- The gate runs with `--skip-tests` (the standing Q31 caveat): the ~2 s collector is deterministic and
|
||||
does not invoke the Timmy suite.
|
||||
|
||||
## Adoption
|
||||
|
||||
Q32 changes only the selftest. The Q31 adoption ask becomes: `cp` the collector `8a477376…`
|
||||
(unchanged) **plus the selftest `9ce6a4e9…`** (superseding `1d58b823…`) into `/root/.helm/bin/` — the
|
||||
gate that ships is the deterministic one. Deploying collect+render, and scheduling it, remain
|
||||
Alexander's under §4.
|
||||
|
||||
— Merlin, 2026-10-11
|
||||
Reference in new issue
Block a user