From 1fc0df6a8e452c7c045275a58faf7f7d9ab44bb1 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 19 Aug 2026 15:21:35 +0200 Subject: [PATCH] docs(framing): close revision 12 approval blockers Make the second controlled-arm record portable without changing what it claims: identify head 77b623c, transcribe the actual foreground and background harness invocations, include the exact evidence-recording harness, label the captured exit as cargo's, and carry both full binary digests in both arm columns. Turn the signal probe into an implementable shared ABI. The checked-in helper owns classification and diagnostics: 0 is safe, 1 is inherited ignore, and 2 is probe error. Preserve kill failure in the inner shell, surface the helper's stderr unchanged in both consumers, and witness the error outcome in both paths. Correct the mutation mapping so removing the trap bites foreground success rather than the ignored-signal rows. Synchronize the active-work ledger with the rerun head, total helper contract, A1-A7 witnesses, and qualified portability claim. --- docs/active-work.md | 33 +++++++------ docs/gpu-probe-sigint-framing.md | 81 ++++++++++++++++++++++++-------- docs/probe-sigint-evidence.md | 77 ++++++++++++++++++++---------- 3 files changed, 131 insertions(+), 60 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 02b033c..d4db566 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -287,7 +287,8 @@ from #171 and #215. the test runner was launched in the background — `SIG_IGN` is inherited across `fork` and survives `exec`, so it reached the launcher and probe, and `kill(-pgid, SIGINT)` was a no-op.** - Controlled arms on committed head `38f2af4` with byte-identical + Controlled arms re-run on committed head `77b623c` with full SHA-256 + captured per arm and byte-identical binaries: foreground both copies ok, `setsid nohup … &` both FAILED. **I caused this** by adopting background launches on 08-16 to evade the Bash tool's ten-minute cap — that is the "onset", and the @@ -298,21 +299,25 @@ from #171 and #215. correctly, only that no evidence of a user-facing defect survives. **A3/D0b are SATISFIED by that explanation** — D0b is not owed and will not run. Framing **revision 12 AWAITING APPROVAL**, and it - **selects the remedy**: R-b + R-d via one checked-in helper wrapping - a portable behavioural probe, - `sh -c 'trap "exit 23" 2; kill -INT $$; exit 0'` — exit **23** when - `SIGINT` is deliverable, **0** when inherited as ignored. POSIX shell - only: no `/proc`, so not Linux-only; no `sigaction`, so no `unsafe`. - `scripts/gate` fails immediately with the explicit diagnosis; the - target test reports the same precondition failure if run directly; + **selects the remedy**: R-b + R-d via one checked-in helper, + `scripts/check-sigint-deliverable`. Its preserved-status inner probe + maps to one complete ABI: helper exit **0** = `safe`, **1** = + `ignored`, **2** = probe `error`; the helper owns the two failure + diagnostics, and both consumers surface its stderr rather than + interpreting raw probe statuses. Inability to execute the helper is + `error`, never evidence of `SIG_IGN`. POSIX shell only: no `/proc`, + so not Linux-only; no `sigaction`, so no `unsafe`. `scripts/gate` + fails immediately with the explicit diagnosis; the target test + reports the same precondition failure if run directly; **no override**, because a gate under ignored `SIGINT` cannot produce valid evidence. R-c rejected. The Linux-only D1/D2 instrumentation is - removed once its evidence is portable. A1–A5 replaced by witnesses - for guard bite, direct-test diagnosis, unaffected foreground success, - mutation, and an otherwise unchanged gate. The mechanism is **known** - and the only implementation so far is the diagnostic instrument. Revisions 1, 2 and 3 were each rejected on - findings, all upheld; run provenance lives in - `docs/probe-sigint-evidence.md`. + removed once its evidence is portable. A1–A7 witness guard bite, + direct-test diagnosis, unaffected foreground success, mutation, an + otherwise unchanged gate, a distinct error outcome in **both** + consumers, and qualified non-Linux-unix portability. The mechanism is + **known** and the only implementation so far is the diagnostic + instrument. Revisions 1, 2 and 3 were each rejected on findings, all + upheld; run provenance lives in `docs/probe-sigint-evidence.md`. - **D0a EXECUTED 2026-08-19 — verdict: difference NOT captured by the two commits.** 10 runs, counterbalanced, N=5 per endpoint, clean detached worktrees with isolated target dirs, `dirty=0` per run, zero diff --git a/docs/gpu-probe-sigint-framing.md b/docs/gpu-probe-sigint-framing.md index d840ee0..45c718d 100644 --- a/docs/gpu-probe-sigint-framing.md +++ b/docs/gpu-probe-sigint-framing.md @@ -622,7 +622,7 @@ group. demonstrates a real session behaves correctly, only that no evidence of the contrary survives. -## 7b. Remedy options — evaluated, none yet selected +## 7b. Remedy options — revision 11 evaluation, superseded by §7c Revision 11 jumped from "`pre_exec` is `unsafe`" to "therefore a precondition assertion". That does not follow: ruling out one mechanism @@ -649,7 +649,8 @@ that touches an `unsafe` prohibition, so it needs a separate decision. Linux; `sigaction`-based querying would be portable but is `unsafe`. Whichever is chosen must state what it does on a non-`/proc` unix. -No remedy is implemented, and none is selected here. +Revision 11 implemented no remedy and selected none. Section 7c records +the later selection that governs implementation. ## 7c. Remedy — SELECTED: R-b + R-d, via one portable probe @@ -673,23 +674,60 @@ 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: +So the **helper owns the classification**. The checked-in executable is +`scripts/check-sigint-deliverable`; this is its complete interface: + +- exit **0**, no diagnostic: `safe`; +- exit **1**, canonical diagnostic on stderr: `ignored`; +- exit **2**, a distinct canonical diagnostic on stderr: `error`. + +Its complete POSIX-shell classification shape preserves failure rather +than overwriting it: + +```sh +probe_status=0 +sh -c 'trap "exit 23" 2 || exit 24; kill -INT "$$" || exit 24; exit 0' \ + || probe_status=$? +case "$probe_status" in + 23) exit 0 ;; + 0) + echo 'pmacs: SIGINT is ignored; run this command with SIGINT deliverable' >&2 + exit 1 + ;; + *) + echo "pmacs: could not determine whether SIGINT is deliverable (probe status $probe_status)" >&2 + exit 2 + ;; +esac +``` + +The helper maps inner 23 → helper 0, inner 0 → helper 1, and every +other status → helper 2. Consumers **do not parse the raw 23/0/24 +statuses and do not supply their own signal diagnosis**: they continue +only on helper exit 0 and otherwise stop while surfacing the helper's +stderr unchanged. Failure to execute the helper at all is mechanically +an `error` at the call boundary, never evidence that `SIGINT` is +ignored. + +That produces one of three total outcomes: | 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 | +| `safe` | `SIGINT` is deliverable | inner probe exits 23; helper exits 0 | +| `ignored` | `SIGINT` is inherited as `SIG_IGN` | inner probe exits 0 after a successful `kill`; helper exits 1 | +| `error` | the probe could not decide | `kill` failed, `sh` unavailable, unexpected exit, another signal, or helper execution failed; helper exits 2 or could not be executed | `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`. +This is **POSIX shell only** — `trap`, `kill`, `$$` — so the mechanism +does not depend on `/proc` or `sigaction`: it is not Linux-only and adds +no `unsafe`. That is a contract-level portability argument, not a claim +that every supported Unix has already exercised it; A7 keeps the +implementation record explicit about which platforms were actually +tried. **Both consumers use the same helper**, so the guard and the test can never disagree about what "ignored" means: @@ -727,22 +765,25 @@ show: - **A3 — foreground success is unaffected.** Both target copies pass foreground, and the guard does not fire, so the remedy costs nothing 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. Additionally, collapsing `error` into `ignored` - must fail A6. +- **A4 — mutation.** Removing the probe's `trap` makes A3 fail: a + normal foreground signal terminates the inner shell and is classified + as `error`, not `safe`. Treating inner exit 0 as `safe` makes A1 and + A2 fail by allowing inherited ignore through. Collapsing `error` into + `ignored` makes A6 fail. Each mutation is named against the distinct + row it must bite. +- **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. - **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`. + fail (for example its inner `sh` made unavailable), **both the gate + and the direct target test** report the helper's **`error`** + diagnosis, not the `ignored` one, and neither claims 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. ## 8b. Superseded criteria, kept for the record diff --git a/docs/probe-sigint-evidence.md b/docs/probe-sigint-evidence.md index abf6061..5b191fe 100644 --- a/docs/probe-sigint-evidence.md +++ b/docs/probe-sigint-evidence.md @@ -166,7 +166,7 @@ in this file are incomplete for that reason: they gave the inner ### Controlled arms, committed head, worktree-local target -Head `38f2af4`, `dirty=0`, worktree +Head `77b623c`, `dirty=0`, worktree `/home/jeans/Repos/personal/pmacs-probe-sigint`, target `/home/jeans/build/pmacs-gate-targets/pmacs-probe-sigint-84ed0f9e`, `TMPDIR=/home/jeans/build/pmacs-gate-targets/tmp/arms`. @@ -179,31 +179,53 @@ cargo test --features crdt --no-fail-fast \ -- ctrl_c_on_launcher_group ``` -Outer invocation — the only difference, written out with **no -placeholders**: +The **actual evidence-producing outer invocations** were: ``` -# 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 \ - cargo test --features crdt --no-fail-fast \ - --test gpu_invocation_acceptance --test gpu_initial_target_acceptance \ - -- ctrl_c_on_launcher_group +# fg arm +/home/jeans/build/pmacs-gate-targets/d0a/arms2.sh fg -# 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 +# bg arm +setsid nohup /home/jeans/build/pmacs-gate-targets/d0a/arms2.sh bg \ + > /dev/null 2>&1 & disown +``` + +`arms2.sh` is machine-local, so naming it is not portable provenance by +itself. This is the complete body that produced `arms2.tsv`; it records +the inner `cargo` exit rather than the status of the disowned outer job: + +```sh +#!/bin/sh +# D1/D2 controlled arms, v2: full binary SHA-256 captured PER RUN, +# immediately after the run, before anything can rebuild them. +set -u +WT=/home/jeans/Repos/personal/pmacs-probe-sigint +TD=/home/jeans/build/pmacs-gate-targets/pmacs-probe-sigint-84ed0f9e +TMP=/home/jeans/build/pmacs-gate-targets/tmp/arms; mkdir -p "$TMP" +OUT=/home/jeans/build/pmacs-gate-targets/d0a +ARM="$1"; LOG="$OUT/arm2-$ARM.log" +HEAD=$(git -C "$WT" rev-parse HEAD); DIRTY=$(git -C "$WT" status --porcelain | wc -l) +env TMPDIR="$TMP" CARGO_TARGET_DIR="$TD" sh -c \ + "cd $WT && cargo test --features crdt --no-fail-fast --test gpu_invocation_acceptance --test gpu_initial_target_acceptance -- ctrl_c_on_launcher_group" \ + > "$LOG" 2>&1 +EXIT=$? +OK=$(grep -c "ctrl_c_on_launcher_group_does_not_reach_spawned_daemon \.\.\. ok" "$LOG") +BAD=$(grep -c "ctrl_c_on_launcher_group_does_not_reach_spawned_daemon \.\.\. FAILED" "$LOG") +SIG=$(grep -m1 -o "SigIgn=[0-9a-f]*" "$LOG" || echo "SigIgn=not-captured") +# Full digests of exactly the binaries this run executed, taken now. +HASHES="" +for b in $(grep -E "Running tests/gpu_" "$LOG" | sed 's|.*deps/||; s|)||'); do + HASHES="$HASHES $b=$(sha256sum "$TD/debug/deps/$b" | cut -d' ' -f1)" +done +printf '%s\tarm=%s\thead=%s\tdirty=%s\texit=%s\tok=%s\tfailed=%s\t%s\tlogsha=%s\thashes=%s\n' \ + "$(date -Iseconds)" "$ARM" "$(echo $HEAD | cut -c1-7)" "$DIRTY" "$EXIT" "$OK" "$BAD" "$SIG" \ + "$(sha256sum "$LOG" | cut -d' ' -f1)" "$HASHES" >> "$OUT/arms2.tsv" +echo "arm2 $ARM: exit=$EXIT ok=$OK failed=$BAD $SIG" ``` ### Result — head `77b623c`, `dirty=0`, digests captured PER RUN -| arm | outer | exit | ok | failed | `SigIgn` | log sha256 (full) | +| arm | outer | cargo 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` | @@ -213,16 +235,17 @@ anything could rebuild them** — not read later from a reused path: | binary | fg arm | bg arm | |---|---|---| -| `gpu_initial_target_acceptance-91f51d0b5303ff9f` | `0890b78cca22ac1e80b79845f85fb6e88def3330db15ae123a2a672d3084124c` | *(identical)* | -| `gpu_invocation_acceptance-6b4b8223dea45247` | `ef6ff1c15e11062ab53a075763814f32c1bbc9be1b146d068c60e91fa247c696` | *(identical)* | +| `gpu_initial_target_acceptance-91f51d0b5303ff9f` | `0890b78cca22ac1e80b79845f85fb6e88def3330db15ae123a2a672d3084124c` | `0890b78cca22ac1e80b79845f85fb6e88def3330db15ae123a2a672d3084124c` | +| `gpu_invocation_acceptance-6b4b8223dea45247` | `ef6ff1c15e11062ab53a075763814f32c1bbc9be1b146d068c60e91fa247c696` | `ef6ff1c15e11062ab53a075763814f32c1bbc9be1b146d068c60e91fa247c696` | **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`. +by the table above. The raw `arms.tsv` and `arms2.tsv` remain +machine-local; the literal harness and full rows above are their +portable transcription. ### Disposition — UNRECORDED CORROBORATION, not a controlled arm @@ -237,11 +260,13 @@ evidence. | foreground | `0000000000001000` | bit 12 (SIGPIPE) only — deliverable | | `setsid nohup … &` | `0000000000000007` | SIGHUP, SIGINT, SIGQUIT — ignored | -The portable probe adopted as the remedy (framing §7c) supersedes it as -the *recorded* mechanism check: +The raw behavioural probe underlying the selected helper (framing §7c) +supersedes it as the *recorded* mechanism check: `sh -c 'trap "exit 23" 2; kill -INT $$; exit 0'` exits **23** when `SIGINT` is deliverable and **0** when it is inherited as ignored. -Verified in both contexts. +Verified in both contexts. It is **not itself the remedy** because it +does not preserve `kill` failure; §7c specifies the total checked-in +helper and its 0/1/2 consumer ABI. ### The first D1/D2 capture, and why it is superseded