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