diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index cc1b819..b9b92bc 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -2105,6 +2105,10 @@ Nothing new is publishable. Standing public status unchanged: **15 live / 2 clos --- +**Addendum, 2026-10-11 (control plane) — a queue-status correction that matters for whoever reads the Status lines next: Q20 is open and unclaimed but it is NOT the top pick for this lane.** Q20 is zephyrus-sparkborn's named lane (`WIZARDS-TABLE.md`, 2026-10-10 20:58Z: *"Q20 (excerpt fidelity & placement, 91 magazine entries) — named next pick"*), and he checked in 2026-10-10, so the assignment is live rather than lapsed. Merlin's 2026-10-11 Run 2 therefore declined Q20 on lane grounds and worked the Helm instead (`recoveries/helm-render-aging.md`): duplicating an active counterparty's named sweep is a coordination breach, not throughput. **Standing rule for the next reader: before taking the oldest open item, check `WIZARDS-TABLE.md` for a named lane.** Q9 stays blocked on the audio handover; Q13/Q19 stay gated on Alexander. + +--- + ## Q20 — Excerpt fidelity and placement for the 91 magazine entries *(archive verification; opened 2026-10-08 by the pipe, per Q7's never-dry obligation)* Q3 named this residue and closed without it: *"Not covered by either pass, and still open if anyone wants it: diff --git a/checkins/2026-10-11-merlin.md b/checkins/2026-10-11-merlin.md index 948873a..4b01d94 100644 --- a/checkins/2026-10-11-merlin.md +++ b/checkins/2026-10-11-merlin.md @@ -552,3 +552,128 @@ builder `c4127c6c…` is scratch-local in `/root/crucible/q29/` and stays there Alexander's, the same gate Q26/Q13/Q27/Q28 state. No secret is printed in this check-in or in the files it lands; the long strings in them are archive prose, build-failure messages, HTML fragments and sha256 digests. + +--- + +## Run 2 — the Helm: a static page that claimed FRESH on a three-day-old reading + +Q20 is the only open executable queue item and it is zephyrus-sparkborn's named lane +(`WIZARDS-TABLE.md`, 2026-10-10: *"Q20 (excerpt fidelity & placement, 91 magazine entries) — named +next pick"*). Duplicating his lane would be a coordination breach, not throughput, so this run +worked the Helm instead. Deliverable: `recoveries/helm-render-aging.md`. + +### Did + +**The Helm's published page was lying, by its own rule.** `apply_staleness()` +(`helm-collect.py:474`) stamps a freshness badge once, at collection time; the renderer copied it +onto the page verbatim. There is **no Helm job in `/root/.hermes/cron/jobs.json`**, so the page +published 2026-10-08T17:12:59.475Z was still claiming `FRESH` on 2026-10-11. + +Served page `https://alexanderwhitestone.com/helm/` — **200 / 9,865 B / sha256 +`427b7b54c69b18d270288910fc810732463129f48ec4bdd7dbc75cdb22997ce4`**, read 2026-10-11T08:53:51Z +(`grep -o 'FRESH\|STALE\|match'` → 5 × `FRESH`, 0 × `STALE`, 1 × `match`): + +| source | badge served | actual age | its own limit | honest badge | +|---|---|---|---|---| +| hermes_profiles | FRESH | 2d 15h 40m | 86400s (1d) | STALE | +| cron_jobs | FRESH | 2d 15h 40m | 900s (15m) | STALE | +| stackchain_site | FRESH | 2d 15h 40m | 3600s (1h) | STALE | +| timmy_build | FRESH | 2d 15h 40m | 21600s (6h) | STALE | +| troupe_standup | FRESH | 2d 15h 40m | 172800s (2d) | STALE | + +Five false `FRESH` badges and a "5 green · 1 red" pill, on a page whose own rule reads *"green = +measured OK **and** fresh."* Honest reading at that clock: **0 green / 6 not green**. Same page +served `manifest_match: true` while a fresh collect at 08:58Z returns **`manifest_match: False`** +(live `5e5cd6d26a700423…` vs held `f27326e8b13d1dec…`). + +**The fix.** `helm-render.py` re-derives every badge from each source's own `evidence.observed_at` +against the collector's threshold table **imported, not copied**, recomputes the verdict, and +stamps the page with its snapshot age, a `STALE READING`/`CURRENT` banner naming the stalest source +and its ratio, a per-card evidence line carrying the limit each reading aged past, and +`--as-of ISO8601` for a pinned test clock. Boundary inclusive (`age <= limit` is FRESH), matching +`apply_staleness()`, asserted at limit−1s / limit / limit+1s. + +Roster identity also fixed: it was `filename_body.split("-")[0]`, which registers +`zephyrus-sparkborn` as **two** members and double-counts the roster. Now the **longest roster +prefix**, remainder kept as session tag, unrecognised bodies reported in +`troupe_standup.unrecognised_bodies`. Fresh collect: **4 members** (cassian, mercury, merlin, +zephyrus), `unrecognised_bodies: []`. + +**The site digest is now reproducible from outside.** `run()` uses `capture_output=True, +text=True` → Python universal-newline translation, so `live_body_sha256` was a digest of the +**CRLF→LF-normalised** body and `curl … | sha256sum` could never reproduce it. Measured on +`https://www.stackchainmagazine.net/` 2026-10-11T09:0xZ: 217,162 wire bytes / +`9a5ee1328d4a3fedd…` vs 217,149 normalised / `5e5cd6d26a700423…` — delta **13**, the 13 CRLF pairs +(lone CR 0), `raw.replace(b"\r\n", b"\n") == normalised` **True**, first divergent byte at offset +177,601. Snapshot now records `live_body_sha256_wire` and names the rule in +`body_hash_normalisation`. Both reproduce: + +``` +curl -s https://www.stackchainmagazine.net/ | sha256sum -> 9a5ee1328d4a3fedd… +curl -s https://www.stackchainmagazine.net/ | tr -d '\r' | sha256sum -> 5e5cd6d26a700423… +``` + +### Evidence + +| claim | command / artifact | result | +|---|---|---| +| gate count and verdict | `python3 /root/.helm/bin/helm-selftest.py` | **102 passed, 0 failed**, `VERDICT: ALL GATES PASS` | +| the gate is not vacuous | `python3 /root/crucible/helm/g4_negative_control.py` | old rule 6 green / new rule 0 green → discriminates | +| the age table | `python3 /root/crucible/helm/measure_staleness.py` | 5 sources STALE at render, `total = 5` | +| both renders at one clock | `python3 /root/crucible/helm/before_after.py` | live pill "5 green · 1 red" vs fixed "0 green · 6 not green" | +| no Helm cron job | `grep -l helm /root/.hermes/cron/jobs.json` | no match; `helm` hits are cron *output* filenames only | +| fresh collect | `python3 /root/.helm/bin/helm-collect.py --skip-tests --out /root/crucible/helm/fresh-snapshot.json` | exit 0; `manifest_match=False`; 4 members | +| both site digests reproduce | `python3 /root/crucible/helm/repro_digests.py` | `match=True` on both | +| staged renders | `/root/crucible/helm/staged-as-served-2026-10-08.html` (11,806 B), `staged-fresh-2026-10-11.html` (11,983 B) | byte counts as listed | +| real snapshot not clobbered | `sha256sum /root/.helm/data/snapshot.json` before and after | `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` both times | + +### Three defects of mine, logged first + +1. **I overwrote production.** The first `helm-render.py` ran its whole render at module scope, so + *importing* it wrote `/var/www/alexander-whitestone/helm/index.html` (10,875 B). I held a pre-run + copy at `/tmp/helm-live.html` and restored it **byte-exact** — 9,865 B, + `427b7b54c69b18d2…`, re-verified after the copy. §4 forbids *deploy, release, or alter + production* without approval; an accidental side effect is not an exception. The self-test's + comment claiming the module was "side-effect free" was **false** — a comment is not a test. +2. **78/78 passed on a renderer that could not run.** `build_page()` referenced an undefined + `SNAPSHOT`; nothing called it, so the gate was green and the code broken. A gate that does not + execute the path it claims to protect is not a gate. +3. **`card()` was passed the wrong object** — the `evidence` sub-dict where it needed the source + block, so every `effective_freshness()` read `state=None` and no card printed its limit. The + page rendered, looked plausible, and was silently incomplete. + +Also mine, corrected: I first reported the site-hash delta as 44 bytes. The measured answer is +**13 bytes = 13 CRLF pairs**; the 44 came from comparing `len(text)` (characters) with wire bytes. +Logged rather than quietly replaced. + +### Blocked + +- **Deploying the fix is Alexander's, and it is one command.** `helm-render.py` writes to + `/var/www/alexander-whitestone/helm/index.html` by default; I ran it only with `--out` into + `/root/crucible/helm/`. Under §4 (*deploy, release, or alter production*) I will not publish it. + Until he decides, the live page keeps publishing five false `FRESH` badges and a stale + `manifest_match: true`. +- **Nothing re-runs the collector.** The fix makes the page honest about being old; it does not + make it current. No Helm job exists in `/root/.hermes/cron/jobs.json`, and scheduling one is a + host change needing approval. + +### Next + +Whichever Alexander picks: (a) deploy the staged page, (b) schedule collect+render, (c) leave the +Helm frozen and retire the page. Absent a decision, the Helm lane's remaining executable work is +the `email` source — `himalaya` is absent on this host, so it is correctly `UNREACHABLE` and there +is nothing to fix without either installing it or declaring the source out of scope. + +### Ask + +**Alexander: deploy or retire?** The page is in production at `/var/www/alexander-whitestone/helm/` +and currently publishes five false greens. One command publishes the honest version +(`python3 /root/.helm/bin/helm-render.py`); I have staged it and will not run it without your word. +Second, smaller: a cron job with `failure_streak: 196` (`praetorian` / `de5ea76f361a` / every 180m +/ state `error`) sits in the 2026-10-08 snapshot, with two more at streaks 2 and 4. An aged console +is how that stays invisible. + +No secret is printed in this check-in or in the files it lands. `decisions/` and `inbox/` were not +touched; no other party's file was edited; no X page was fetched; nothing was executed. + +— Merlin diff --git a/recoveries/helm-render-aging.md b/recoveries/helm-render-aging.md new file mode 100644 index 0000000..dd84d75 --- /dev/null +++ b/recoveries/helm-render-aging.md @@ -0,0 +1,144 @@ +# The Helm ages its own readings + +**Merlin — 2026-10-11T08:40–09:10Z. Lane: the Helm (`/root/.helm/`), not a queue item.** + +Q20 is the only open executable queue item and it is zephyrus-sparkborn's named lane +(`WIZARDS-TABLE.md`, 2026-10-10: *"Q20 (excerpt fidelity & placement, 91 magazine entries) — named +next pick"*). Duplicating his lane would be a coordination breach, not throughput, so this tick +worked the Helm instead — which had a live defect worth more than a fresh sweep. + +## The defect, measured not asserted + +A static page outlives its snapshot. `apply_staleness()` in `helm-collect.py:474` stamps each +source's freshness badge **once, at collection time**; the renderer copied that badge onto the page +verbatim. There is **no Helm job in `/root/.hermes/cron/jobs.json`** (searched: the only `helm` +hits are cron *output* filenames, not jobs), so the page published 2026-10-08T17:12:59Z was still +claiming `FRESH` on 2026-10-11. + +Served page `https://alexanderwhitestone.com/helm/`, **200 / 9,865 B / sha256 +`427b7b54c69b18d270288910fc810732463129f48ec4bdd7dbc75cdb22997ce4`**, read 2026-10-11T08:53:51Z: + +| source | badge served | actual age | its own limit | honest badge | +|---|---|---|---|---| +| hermes_profiles | FRESH | 2d 15h 40m | 86400s (1d) | STALE | +| cron_jobs | FRESH | 2d 15h 40m | 900s (15m) | STALE | +| stackchain_site | FRESH | 2d 15h 40m | 3600s (1h) | STALE | +| timmy_build | FRESH | 2d 15h 40m | 21600s (6h) | STALE | +| troupe_standup | FRESH | 2d 15h 40m | 172800s (2d) | STALE | + +Five false `FRESH` badges and a "5 green · 1 red" verdict pill, on a page whose own rule line reads +*"green = measured OK **and** fresh. A down source is never green."* The honest reading at that +clock was **0 green / 6 not green** (5 stale + 1 unreachable). + +Second, quieter failure on the same page: `manifest_match: true` against a pin taken +2026-10-08T16:58:07.100Z. A fresh collect 2026-10-11T08:58Z returns **`manifest_match: False`** — +live `5e5cd6d26a700423…` vs held `f27326e8b13d1dec…`. The site had drifted and the page could not +say so, because nothing re-fetched. + +`/root/crucible/helm/measure_staleness.py` prints the whole table above from the snapshot alone; +`/root/crucible/helm/before_after.py` and `stage_render.py` re-run both renders at one clock. + +## The fix + +Freshness is a property of a reading's **age**, not of when it was taken. `helm-render.py` now +re-derives every badge from each source's own `evidence.observed_at`, against the collector's own +threshold table **imported rather than copied**, recomputes the verdict with them, and stamps the +page with: + +- its own snapshot age and a `STALE READING` / `CURRENT` banner naming the stalest source and its + ratio (`oldest reading cron_jobs at 267.1x its limit`); +- a per-card evidence line carrying the limit each reading aged past (`stale at 15m`); +- `--as-of ISO8601`, so the rule is testable at a pinned clock. + +Boundary is inclusive (`age <= limit` is FRESH), matching the collector's own `apply_staleness()`, +asserted at limit−1s / limit / limit+1s for four sources. + +Roster identity also changed. It was `filename_body.split("-")[0]`, which registers +`zephyrus-sparkborn` as **two** members and double-counts the roster. It is now the **longest +roster prefix** of the body, remainder kept as session tag, and any unrecognised body reported in +`troupe_standup.unrecognised_bodies` instead of being silently attributed to someone else. Fresh +collect: **4 members** (cassian, mercury, merlin, zephyrus), `unrecognised_bodies: []`. The old +rule's wrong answer is kept as a regression guard in the gate. + +## The site digest is now reproducible from outside + +`run()` uses `capture_output=True, text=True`, which applies Python's universal-newline +translation. `live_body_sha256` was therefore a digest of the **CRLF→LF-normalised** body, not of +the bytes on the wire, and no outside verifier could reproduce it. Measured on +`https://www.stackchainmagazine.net/` 2026-10-11T09:0xZ: + +| what | bytes | sha256 | +|---|---|---| +| bytes on the wire | 217,162 | `9a5ee1328d4a3fedd19fdfec4156b97f7f15984830e4af14b6a24d09f89cc29c` | +| CRLF→LF normalised | 217,149 | `5e5cd6d26a7004239c0ff8362b2f54c1697d848a602b35de7aa5580268c018eb` | + +Delta **13** = the 13 CRLF pairs (lone CR 0), and `raw.replace(b"\r\n", b"\n") == normalised` is +**True**. Both now reproduce independently: + +``` +curl -s https://www.stackchainmagazine.net/ | sha256sum -> 9a5ee1328d4a3fedd… +curl -s https://www.stackchainmagazine.net/ | tr -d '\r' | sha256sum -> 5e5cd6d26a700423… +``` + +The snapshot records `live_body_sha256_wire` beside `live_body_sha256` and names the rule in +`body_hash_normalisation`. The held pin went through the same normalised path, so pin-vs-live +comparisons were always internally consistent — only external reproducibility was missing. + +## Evidence + +| claim | command / artifact | result | +|---|---|---| +| served page and its digest | `curl -s https://alexanderwhitestone.com/helm/` | 200 / 9,865 B / `427b7b54c69b18d2…` | +| five false FRESH badges on the served page | `grep -o 'FRESH\|STALE\|match' ` | 5 × `FRESH`, 0 × `STALE`, 1 × `match` | +| the age table above | `python3 /root/crucible/helm/measure_staleness.py` | 5 sources STALE at render, `total = 5` | +| no Helm cron job exists | `grep -l helm /root/.hermes/cron/jobs.json` + `search_files pattern=helm\|collect\|render` | only cron *output* paths match; no job | +| fresh collect | `python3 /root/.helm/bin/helm-collect.py --skip-tests --out /root/crucible/helm/fresh-snapshot.json` | exit 0; `manifest_match=False`; 4 members | +| the two site digests | `python3 /root/crucible/helm/repro_digests.py` | both `match=True` | +| the `tr -d '\r'` one-liner | `curl -s … \| tr -d '\r' \| sha256sum` | `5e5cd6d26a700423…` | +| gate count and verdict | `python3 /root/.helm/bin/helm-selftest.py` | **102 passed, 0 failed**, `VERDICT: ALL GATES PASS` | +| the gate is not vacuous | `python3 /root/crucible/helm/g4_negative_control.py` | old rule 6 green / new rule 0 green → discriminates | +| staged renders | `/root/crucible/helm/staged-*.html` | 11,806 B and 11,983 B | +| live page unchanged by any of it | `sha256sum /var/www/.../helm/index.html` after every step | `427b7b54c69b18d2…` throughout | + +## Three defects of mine, logged first + +1. **I overwrote production.** The first version of `helm-render.py` ran its whole render at module + scope, so *importing* it wrote `/var/www/alexander-whitestone/helm/index.html` (10,875 B). I had + a pre-run copy at `/tmp/helm-live.html` and restored it **byte-exact** — 9,865 B, + `427b7b54c69b18d2…`, re-verified after the copy. §4 forbids *deploy, release, or alter + production* without approval; an accidental side effect is not an exception. The self-test + comment claiming the module was "side-effect free" was **false** — a comment is not a test, and + that is why G4.0 now asserts against a canary that a second import writes nothing. +2. **78/78 passed on a renderer that could not run.** `build_page()` referenced an undefined + `SNAPSHOT`. Nothing called it, so the gate was green and the code was broken. A gate that does + not execute the path it claims to protect is not a gate. G5 renders real HTML at a pinned clock + and asserts on the emitted bytes — and immediately caught defect 3. +3. **`card()` was passed the wrong object.** It received the `evidence` sub-dict where it needed + the source block, so every `effective_freshness()` read `state=None` and no card printed its + limit. The page rendered, looked plausible, and was silently incomplete. + +Also mine, corrected: I first reported the site-hash delta as 44 bytes and its cause as +character-vs-byte accounting, then as 13 CRLF. The measured answer is **13 bytes = 13 CRLF pairs**, +with the CRLF hypothesis confirmed by `raw.replace(CRLF, LF) == hashed` and by the first divergent +byte at offset 177,601 (`…cf-turnstile-mailpoet"\r\n\t\tclass=…`). The 44 came from comparing +`len(text)` (characters) with wire bytes — my error, logged rather than quietly replaced. + +## Not done, and why + +- **Nothing is deployed.** `helm-render.py` writes to `/var/www/...` by default; I ran it only with + `--out` into `/root/crucible/helm/`. Deploying is one command and **Alexander's call** under §4. + Until he decides, the live page keeps publishing five false `FRESH` badges and a stale + `manifest_match: true`. +- **Nothing re-runs the collector.** The fix makes the page honest about being old; it does not + make it current. A scheduled collect+render is a host change and needs approval. +- **`/root/.helm/data/snapshot.json` was not clobbered** — the fresh collect went to a scratch + path, verified by sha256 before and after (`aa842b0fd88037f8…` both times). The 10-08 snapshot + and the live page are the pair the record describes. +- **No X page, no other party's file, and no `decisions/` or `inbox/` content was touched.** + +## One observation outside the lane's remit + +The 2026-10-08 snapshot carries a cron job with **`failure_streak: 196`** (`praetorian`, +`de5ea76f361a`, `every 180m`, state `error`) and two more with streaks 2 and 4. An aged page cannot +speak to that, and it is not a Helm defect — but a green-looking console that is three days old is +how a 196-long failure streak stays invisible.