From f607e82263ca5d2bd55f99948d1875b465df15d3 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 19 Aug 2026 14:58:50 +0200 Subject: [PATCH] docs(framing): totalise the helper contract; capture arm digests per run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings, all upheld. 1. The arm provenance was malformed and over-claimed. The "fully expanded" background command still contained and placeholders; both table rows were one cell short of the header, putting log prefixes under "binary hashes" and leaving the digest column empty; and the full binary hashes had been read later from reused paths, which cannot retroactively prove what each arm executed --- the same provenance rule this document states in §7, applied against my own record. Rather than weaken the claim, the arms were re-run at head 77b623c with FULL SHA-256 captured per run, immediately after each run, before anything could rebuild them. Both arms: identical 0890b78c...4124c and ef6ff1c1...c696, dirty=0, fg exit=0 ok=2, bg exit=101 failed=2 SigIgn=0x1007. Byte identity is now carried by the capture rather than by inference. Commands are written out with no placeholders, and the table cells line up. 2. The ledger still transported superseded operative instructions: a "remedy not selected" heading, D0b still owed under A3, journey step 12(a) still assigned, and the old three-consecutive-run A2 contract. All four now match revision 12's §8/§9 --- remedy selected, D0b satisfied and not owed, journey steps NONE with gate trustworthiness named instead, and A1-A7 replacing the three-run contract, which was written for a flakiness that is now explained. 3. The helper contract was not total. The raw probe reaches exit 0 both when the kill was a no-op AND when the kill itself failed, so a broken probe would report "inherited SIG_IGN" and fail the gate for the wrong reason. The helper now owns the classification and returns one of safe / ignored / error; consumers consume the verdict and never re-derive it. `error` is not folded into `ignored` --- it fails the gate with a different diagnosis, because "your environment ignores SIGINT" and "the guard could not run" are different problems. A6 witnesses the distinct error outcome, A7 requires a non-Linux unix exercise or an explicit statement of what was tried, and A4 gains a mutation for collapsing error into ignored. R-b's stale "needs an explicit override" is reconciled with §7c's no -override decision. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 27 ++++++++++------ docs/gpu-probe-sigint-framing.md | 35 +++++++++++++++++++-- docs/probe-sigint-evidence.md | 53 +++++++++++++++++--------------- 3 files changed, 79 insertions(+), 36 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 089c0e1..02b033c 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -270,7 +270,7 @@ hazard in a shape that looks committed. **A documented error message that never appears is worse than no documentation**, because the reader waits for a signal that is not coming. -## GPU launcher / probe SIGINT teardown — MECHANISM FOUND, remedy not selected +## GPU launcher / probe SIGINT teardown — MECHANISM FOUND, remedy SELECTED **Written with the branch's FIRST commit**, per the standing correction from #171 and #215. @@ -480,22 +480,29 @@ from #171 and #215. contract below — a bare difference decides nothing. **(a) is DONE.** **(b) is RETIRED as a precondition** (2026-08-19) because D0a yielded a reliable direct reproduction and D1/D2 measure the mechanism - itself; **its obligation survives under A3** — if D1/D2 do not + itself. **Its obligation is now SATISFIED** by §4c's controlled + explanation of the subset/full difference, so **D0b is not owed and + will not run**. As originally written it said: its obligation + survives under A3 — if D1/D2 do not account for the subset-vs-full difference, D0b runs before the lane closes. It read: re-run the matrix at `main` under a harness capturing provenance **and the artifact hashes executed at run time**, since command shape silently changed the binary once already and a hash computed later reflects only what occupies that path now. -- **Coherence: journey step 12(a), "closing is clean", IS touched** — - Ctrl-C teardown of a GPU session is that step, grade movement or not. - Revision 1 claimed no journey step, reasoning from grade movement, - which §20 explicitly warns against. +- **Coherence: journey steps touched: NONE** (framing §9). Earlier + entries here assigned 12(a) "closing is clean" on the premise that + the lane repairs Ctrl-C teardown; §4c withdraws that premise, because + no product behaviour changes. What the lane affects is **gate + trustworthiness**. - **Gates:** `./scripts/gate --protocol --acceptance - gpu_invocation_acceptance` at minimum; A2 requires `sweep-crdt` green - **three consecutive runs on the reviewed fixed head of this branch** - — not "on main", which is unobtainable before approval and merge. - 1/1 is not evidence for a defect that hid from every reduction. + gpu_invocation_acceptance` at minimum. **The old three-consecutive- + run A2 contract is SUPERSEDED** — it was written for a teardown fix + whose flakiness was unexplained. The mechanism is now known and + deterministic, so the acceptance set is framing §8's A1–A7: guard + bite, direct-test diagnosis, unaffected foreground success, mutation, + an otherwise unchanged gate, a distinct `error` outcome, and a + non-Linux-unix statement. ## `scripts/gate` TMPDIR isolation — PR #240 OPEN diff --git a/docs/gpu-probe-sigint-framing.md b/docs/gpu-probe-sigint-framing.md index 2469c0a..d840ee0 100644 --- a/docs/gpu-probe-sigint-framing.md +++ b/docs/gpu-probe-sigint-framing.md @@ -632,7 +632,7 @@ decides each: | # | remedy | effect | cost / risk | |---|---|---|---| | R-a | **Runner normalisation** — never invoke the gate so that `SIGINT` is ignored; if backgrounding is needed, restore the disposition first | removes the cause for every test at once | a *practice*, not a mechanism: nothing enforces it, and this lane exists because I violated it silently | -| R-b | **Early gate guard** — `scripts/gate` refuses to start when `SIGINT` is `SIG_IGN`, naming the reason | enforces R-a mechanically, once, for all suites | refuses runs that would mostly have succeeded; needs an explicit override for deliberate background use | +| R-b | **Early gate guard** — `scripts/gate` refuses to start when `SIGINT` is `SIG_IGN`, naming the reason | enforces R-a mechanically, once, for all suites | refuses runs that would mostly have succeeded. *This row originally added "needs an explicit override for deliberate background use"; §7c rejects that — see there* | | R-c | **Fixture isolation** — the test restores the default disposition in the spawned launcher | fixes the test wherever it runs, background included | `pre_exec` is `unsafe`, and `#![forbid(unsafe_code)]` binds the lib crate; an integration test could technically opt out, but doing so to dodge a project invariant needs its own argument | | R-d | **Test-local precondition assertion** — detect `SIG_IGN` on `SIGINT` and fail with that reason instead of "child did not exit within 5s" | converts nine revisions of misdirection into one accurate line | does not make the test *pass* when backgrounded; it only stops it lying about why it failed | @@ -666,6 +666,27 @@ it exits **23**; if `SIGINT` was inherited as `SIG_IGN` the kill is a no-op and it exits **0**. Verified in both contexts: 23 foreground, 0 under `setsid nohup … &`. +**The raw probe is not a total classifier, and must not be used as +one.** Its `exit 0` is reached both when the `kill` was a no-op *and* +when the `kill` itself failed — a failed `kill` overwrites its own +status and lands on the same exit. Read naively, a broken probe reports +"inherited `SIG_IGN`", which would fail the gate for the wrong reason +and send the next reader down this lane again. + +So the **helper owns the classification**, and returns one of three +outcomes; consumers only consume the verdict and never re-derive it: + +| outcome | meaning | how it is reached | +|---|---|---| +| `safe` | `SIGINT` is deliverable | probe exits 23 | +| `ignored` | `SIGINT` is inherited as `SIG_IGN` | probe exits 0 **and** the `kill` itself reported success | +| `error` | the probe could not decide | `kill` failed, `sh` unavailable, unexpected exit, or a signal other than the trap | + +`error` is **not** treated as `ignored`. It fails the gate too, but with +a different diagnosis, because "your environment ignores SIGINT" and +"the guard could not run" are different problems and conflating them is +what a naive `exit 0` would do. + This is **POSIX shell only** — `trap`, `kill`, `$$` — so it settles the portability question §7b raised: no `/proc`, hence not Linux-only, and no `sigaction`, hence no `unsafe`. @@ -708,7 +729,17 @@ show: in the normal case. - **A4 — mutation.** Removing the probe's `trap`, or treating exit 0 as "deliverable", makes A1 and A2 fail; each mutation is named against - the row it must bite. + the row it must bite. Additionally, collapsing `error` into `ignored` + must fail A6. +- **A6 — the `error` outcome is distinct.** With the probe forced to + fail (e.g. its interpreter made unavailable), the guard reports the + **`error`** diagnosis, not the `ignored` one, and does not claim the + environment ignores `SIGINT`. +- **A7 — a supported non-Linux unix.** The helper is exercised on a + non-`/proc` unix in the project's supported set, or — if none is + reachable — the record states which platforms the guard is *claimed* + to work on and which were actually tried. No unexercised portability + claim ships unqualified. - **A5 — the gate is otherwise unchanged**: a normal foreground run reaches and passes every stage it did before, with no stage added, skipped, reordered, or made conditional. diff --git a/docs/probe-sigint-evidence.md b/docs/probe-sigint-evidence.md index 6c728b8..abf6061 100644 --- a/docs/probe-sigint-evidence.md +++ b/docs/probe-sigint-evidence.md @@ -179,12 +179,11 @@ cargo test --features crdt --no-fail-fast \ -- ctrl_c_on_launcher_group ``` -Outer invocation, the only difference. `arms.sh` is machine-local, so -the commands are given **fully expanded** — a reader elsewhere needs no -access to it: +Outer invocation — the only difference, written out with **no +placeholders**: ``` -# fg arm +# fg arm — run directly in an interactive foreground shell cd /home/jeans/Repos/personal/pmacs-probe-sigint && \ env TMPDIR=/home/jeans/build/pmacs-gate-targets/tmp/arms \ CARGO_TARGET_DIR=/home/jeans/build/pmacs-gate-targets/pmacs-probe-sigint-84ed0f9e \ @@ -192,32 +191,38 @@ env TMPDIR=/home/jeans/build/pmacs-gate-targets/tmp/arms \ --test gpu_invocation_acceptance --test gpu_initial_target_acceptance \ -- ctrl_c_on_launcher_group -# bg arm — byte-identical inner command, wrapped: -setsid nohup sh -c '' > 2>&1 & disown +# bg arm — the identical command, wrapped +setsid nohup sh -c 'cd /home/jeans/Repos/personal/pmacs-probe-sigint && \ +env TMPDIR=/home/jeans/build/pmacs-gate-targets/tmp/arms \ + CARGO_TARGET_DIR=/home/jeans/build/pmacs-gate-targets/pmacs-probe-sigint-84ed0f9e \ + cargo test --features crdt --no-fail-fast \ + --test gpu_invocation_acceptance --test gpu_initial_target_acceptance \ + -- ctrl_c_on_launcher_group' \ + > /home/jeans/build/pmacs-gate-targets/d0a/arm2-bg.log 2>&1 & disown ``` -The wrapper additionally recorded `git rev-parse HEAD`, -`git status --porcelain | wc -l`, the exit status, both copies' -results, the executed suffixes, their hashes, and the log digest. +### Result — head `77b623c`, `dirty=0`, digests captured PER RUN -| arm | outer | exit | ok | failed | `SigIgn` | binary hashes | log sha256/16 | -|---|---|---|---|---|---|---|---| -| fg | foreground | 0 | 2 | 0 | not captured (no failure ⇒ no dump) | `aaec01673691479a…` (prefix) | -| bg | `setsid nohup … &` | 101 | 0 | 2 | `0000000000001007` | `c744d85a84cb8683…` (prefix) | +| arm | outer | exit | ok | failed | `SigIgn` | log sha256 (full) | +|---|---|---|---|---|---|---| +| fg | foreground | 0 | 2 | 0 | not captured (no failure ⇒ no dump) | `b6117619242f3dabadbe5826f59f11cef451cd09a2beb304e9a3dec1a4616465` | +| bg | `setsid nohup … &` | 101 | 0 | 2 | `0000000000001007` | `30ac6568e345c485ddf23734a239d71bd06d4a3c2d549f1b4a8942a930527507` | -Both arms executed the same two binaries, whose **full** SHA-256 are: +Binary digests, **full SHA-256, taken immediately after each run before +anything could rebuild them** — not read later from a reused path: -``` -gpu_initial_target_acceptance-91f51d0b5303ff9f - 0890b78cca22ac1e80b79845f85fb6e88def3330db15ae123a2a672d3084124c -gpu_invocation_acceptance-6b4b8223dea45247 - ef6ff1c15e11062ab53a075763814f32c1bbc9be1b146d068c60e91fa247c696 -``` +| binary | fg arm | bg arm | +|---|---|---| +| `gpu_initial_target_acceptance-91f51d0b5303ff9f` | `0890b78cca22ac1e80b79845f85fb6e88def3330db15ae123a2a672d3084124c` | *(identical)* | +| `gpu_invocation_acceptance-6b4b8223dea45247` | `ef6ff1c15e11062ab53a075763814f32c1bbc9be1b146d068c60e91fa247c696` | *(identical)* | -Same head, same target directory, `dirty=0`, and the binaries were not -rebuilt between arms — so nothing but the outer invocation varies. The -**log** digests above are 16-character **prefixes**, not full values, -and are identifiers only; no claim rests on them. +**These are byte-identical, and that claim is now carried by the +capture rather than by inference.** The earlier arms table recorded only +16-character prefixes at run time and its full values were read +afterwards from reused paths — which is exactly the provenance rule +§7/D0 states, applied against my own record. Those rows are superseded +by the table above; raw rows for both generations are in `arms.tsv` and +`arms2.tsv`. ### Disposition — UNRECORDED CORROBORATION, not a controlled arm