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.
This commit is contained in:
Levi Neuwirth 2026-08-19 15:21:35 +02:00
parent f607e82263
commit 1fc0df6a8e
No known key found for this signature in database
3 changed files with 131 additions and 60 deletions

View File

@ -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

View File

@ -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

View File

@ -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