From 8b8a6925287afb59e1591a2ea1fa89dc7dc290f1 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 19 Aug 2026 20:27:43 +0200 Subject: [PATCH] docs(framing): revision 13 round 2 --- the algorithm still implemented r12 Five blocking inconsistencies, all upheld. The first was the worst: the document specified a validated pair and then printed an algorithm that emits no tokens and a consumer flow that proceeds on exit 0 alone --- accepting 0 with a missing token, the exact defect revision 13 forbids. 1. The algorithm now emits exactly one token per arm on stdout with diagnostics on stderr; the consumer flow is pair-validation with explicit normalisation (strip one trailing newline, trim ASCII whitespace, require exactly one line); and the outcome table is keyed on pairs, with a fourth row for boundary error including macOS's status 1 with no token. `safe` is validated like the others --- a status arriving without its token did not come from this helper. 2. A6a is SCOPED TO THE GATE. R-d never sees a shell status: the gate goes through /bin/sh, which turns an exec failure into an exit status, while Rust's Command returns a spawn error with no status at all --- conformance row 12, not row 5. And macOS CI does not compile R-d's test, which is crdt-gated while the macOS jobs build without crdt. R-d on macOS is unexercised, and the framing says so rather than implying coverage. 3. A7 is restated against measurement. It cannot still say no non-Linux unix was tried when macOS ran and went red: five of six helper/gate rows pass there, one defect is named, R-d is recorded Linux-only, and the remaining portability claim is labelled a contract argument. 4. "Both consumers use the same helper so they can never disagree" is withdrawn --- true when the status WAS the verdict, false once each consumer validates a pair independently in a different language. Replaced by a twelve-case conformance matrix both validators must agree on, including the macOS case and a normalisation case. 5. The token-to-stderr mutation is remapped from A2 to A1/A3, with the reasoning recorded: with stdout empty every outcome becomes boundary error, which still satisfies A2 as written since A2 only requires "not the deadline message". A2 stays broad and A6 pins which diagnosis appears. The ledger is aligned: the mechanism is established rather than hypothesised, the "stderr prints the raw status" claim is corrected --- the number appears only in the catch-all, and this failure took the other branch --- and revision 12 is marked superseded. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- docs/active-work.md | 48 ++++++--- docs/gpu-probe-sigint-framing.md | 168 ++++++++++++++++++++++++------- 2 files changed, 165 insertions(+), 51 deletions(-) diff --git a/docs/active-work.md b/docs/active-work.md index 5359eb2..21ff3af 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -288,18 +288,39 @@ from #171 and #215. otherwise exercised there. - **The gate returned 1 = `ignored` for a helper it could not execute** — the exact conflation §7c forbids. - - **Leading hypothesis, NOT yet established: the ABI's `1` is - ambiguous by construction.** `1` means "ignored", and `1` is also a - status shells return for assorted failures. On Linux an - unexecutable file yields 126, so the catch-all maps it to 2; if - macOS's `/bin/sh` returns 1, the two cases are **the same number** - at the call boundary and no catch-all can separate them. If that - holds, the fix is to move the verdicts out of the range shells - produce, or to carry them by something other than exit status - alone — a design change needing its own revision. - - The assertion now carries the gate's stderr, which prints the raw - probe status, because the first failure could not say which status - produced it. + - **ESTABLISHED, not a hypothesis.** The macOS log shows the shell's + `Permission denied` followed by the gate's `1 | 2)` message — so + `sigint_status` was **1**: macOS `/bin/sh` returns **1** for an + exec failure where Linux returns **126**. The status-only ABI + cannot separate that from the helper's own `ignored`. + - **The raw status was NOT printed.** An earlier entry here said the + stderr carries it; the gate interpolates the number only in its + catch-all branch, and this failure took the `1 | 2)` branch. The + path was identified by **which message text appeared**, not by a + number. + - **A proposed repair was REJECTED in review and is recorded so it is + not retried:** moving `ignored` from 1 to 3 relocates the collision + without closing it, because an exec failure can return **any** + nonzero status. The generalisation: **no exit status can prove the + helper ran.** + - **Framing revision 13 — AWAITING APPROVAL — replaces the ABI with a + validated `(status, token)` pair**: `0`/`1`/`2` with + `pmacs-sigint-v1:safe|ignored|error`, token alone on stdout, + diagnostics on stderr, and any other pair — including macOS's + status 1 with no token — a boundary error mapped to 2. Every + refusing branch must print the observed status and token state as + diagnostic context, never as the classifier. A shared **conformance + matrix** replaces revision 12's withdrawn "both consumers use the + same helper so they can never disagree", which stopped being true + once each consumer validates the pair independently. + - **A6a is scoped to the GATE.** R-d never sees a shell status: Rust's + `Command` returns a spawn error with no exit status, and R-d's test + is crdt-gated so macOS CI does not compile it. R-d on macOS is + **unexercised**, recorded as a gap. + - **A7 is no longer "satisfied by disclosure"** — macOS was reached + and measured: five of six helper/gate rows pass, one defect + (`gate_maps_an_unexecutable_helper_to_error_not_ignored`), R-d + Linux-only. - **PR #241** (`https://github.com/levineuwirth/pmacs/pull/241`), opened 2026-08-19 from `gpu-probe-sigint-teardown` into `main`. **Not merged; awaiting review rounds.** @@ -340,7 +361,8 @@ from #171 and #215. result line immediately below `gate_script_acceptance`'s in the sweep log, misread as this suite's.) - **Framing revision 12 at `docs/gpu-probe-sigint-framing.md`, - APPROVED 2026-08-19 at `1fc0df6`** — revision 10 was approved at + approved 2026-08-19 at `1fc0df6` and IMPLEMENTED; **superseded by + revision 13, awaiting approval** — revision 10 was approved at `4fba9f6` and revision 9 at `15c25ec`; neither approval covered the later mechanism finding and remedy selection. **D1/D2 HAVE RUN and found the mechanism: `SIGINT` was ignored group-wide diff --git a/docs/gpu-probe-sigint-framing.md b/docs/gpu-probe-sigint-framing.md index 0746193..32e3710 100644 --- a/docs/gpu-probe-sigint-framing.md +++ b/docs/gpu-probe-sigint-framing.md @@ -744,41 +744,71 @@ Consumers therefore validate the exact pair and treat every mismatch as decides, and the pair is what makes the helper's decision distinguishable from a shell's. -Its complete POSIX-shell classification shape preserves failure rather -than overwriting it: +Its complete POSIX-shell shape. Each arm emits **exactly one token on +stdout** and its diagnostic on stderr, so a status is never the only +thing a consumer sees: ```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 ;; + 23) + echo 'pmacs-sigint-v1:safe' + exit 0 + ;; 0) + echo 'pmacs-sigint-v1:ignored' echo 'pmacs: SIGINT is ignored; run this command with SIGINT deliverable' >&2 exit 1 ;; *) + echo 'pmacs-sigint-v1:error' 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. +The helper maps inner 23 → `(0, safe)`, inner 0 → `(1, ignored)`, and +every other status → `(2, error)`. Consumers do not parse the inner +23/0/24 statuses and do not supply their own signal diagnosis. + +**The consumer flow is pair-validation, not status inspection:** + +1. Run the helper, capturing **status**, **stdout** and **stderr** + separately. A spawn failure — the helper missing, not executable, or + unrunnable for any reason — is `error` immediately, with **no + status to inspect at all**. +2. **Normalise stdout**: strip a single trailing newline, then trim + ASCII whitespace at both ends. The result must be **exactly one + line**. Anything else — empty, multi-line, or with interior + content — is `error`. +3. Accept **only** these three pairs; every other combination is + `error`: + + | status | normalised stdout | outcome | + |---|---|---| + | 0 | `pmacs-sigint-v1:safe` | `safe` | + | 1 | `pmacs-sigint-v1:ignored` | `ignored` | + | 2 | `pmacs-sigint-v1:error` | `error` | + +4. Proceed only on `safe`. Otherwise stop, surfacing the helper's + stderr unchanged plus the diagnostic context below. + +**`safe` is validated like the others.** Revision 12 let a consumer +proceed on exit 0 alone; under revision 13, `0` with a missing or wrong +token is `error` and the consumer stops. That is deliberate — a status +that arrives without the token did not come from this helper. That produces one of three total outcomes: -| outcome | meaning | how it is reached | +| outcome | pair required | reached when | |---|---|---| -| `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 | +| `safe` | `(0, pmacs-sigint-v1:safe)` | inner probe exits 23 | +| `ignored` | `(1, pmacs-sigint-v1:ignored)` | inner probe exits 0 after a successful `kill` | +| `error` | `(2, pmacs-sigint-v1:error)` | `kill` failed, `sh` unavailable, unexpected exit, another signal | +| `error` (boundary) | **anything else**, including *no* pair | helper missing or unexecutable; a status with a missing, mismatched, unknown or malformed token; **macOS's status 1 with no token** | `error` is **not** treated as `ignored`. It fails the gate too, but with a different diagnosis, because "your environment ignores SIGINT" and @@ -792,8 +822,37 @@ 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: +**Both consumers use the same helper — but that alone no longer makes +them agree.** Under revision 12 the helper's exit status *was* the +verdict, so a shared helper guaranteed a shared answer. Under +revision 13 each consumer **independently validates the pair**, in a +different language, so they can now disagree by validating differently. +Revision 12's claim that they "can never disagree" is withdrawn. + +What replaces it is a **shared conformance matrix**: both validators +are exercised against the same twelve cases, and must agree on every +one. + +| # | status | stdout | expected | +|---|---|---|---| +| 1 | 0 | `pmacs-sigint-v1:safe` | `safe` | +| 2 | 1 | `pmacs-sigint-v1:ignored` | `ignored` | +| 3 | 2 | `pmacs-sigint-v1:error` | `error` | +| 4 | 0 | *(empty)* | error | +| 5 | 1 | *(empty)* | error — **the macOS case** | +| 6 | 0 | `pmacs-sigint-v1:ignored` | error (mismatched) | +| 7 | 1 | `pmacs-sigint-v1:safe` | error (mismatched) | +| 8 | 0 | `pmacs-sigint-v2:safe` | error (unknown version) | +| 9 | 0 | `pmacs-sigint-v1:safe\npmacs-sigint-v1:safe` | error (multi-line) | +| 10 | 0 | ` pmacs-sigint-v1:safe ` | `safe` (normalisation: trim) | +| 11 | 126 | `pmacs-sigint-v1:safe` | error (status outside 0–2) | +| 12 | — (spawn failure) | — | error, with no status inspected | + +Row 10 fixes normalisation: **strip one trailing newline, then trim +ASCII whitespace, then require exactly one line.** Rows 4–9 and 11 are +the ways a status can arrive without a trustworthy verdict. + +The two consumers: - **`scripts/gate` fails immediately**, before any stage, with an explicit ignored-`SIGINT` diagnosis. @@ -866,7 +925,19 @@ show: | consumers accept a **missing** token (status only) | A6 and the macOS row — this is exactly the shipped defect | | consumers accept a **wrong** token for the status (e.g. `…:safe` with exit 1) | A6 | | consumers accept an **unknown** token (`pmacs-sigint-v2:safe`) | A6 | - | helper prints the token to **stderr** instead of stdout | A1–A3 — the pair no longer validates | + | helper prints the token to **stderr** instead of stdout | **A1 and A3** — see below | + + **Why that last one maps to A1/A3 and not A2.** With the token on + stderr, stdout is empty, so *every* outcome becomes boundary `error`. + A1 (gate refuses under ignored `SIGINT`) still refuses but with the + wrong diagnosis, and A3 (foreground success unaffected) breaks + outright because `safe` no longer validates — both bite. **A2 does + not**, because A2 only requires the direct test to report *a* + precondition failure rather than the 5 s deadline, and a boundary + `error` satisfies that as written. Revision 13 listed A2 here + incorrectly. Either mapping is defensible; this framing keeps A2 + broad — the property it protects is "never the misleading deadline + message" — and relies on A6 to pin *which* diagnosis appears. - **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. @@ -877,27 +948,48 @@ show: revision 13 to cover the pair: a **missing**, **mismatched** or **unknown** token is `error` in both consumers, whatever the status accompanying it. -- **A6a — the macOS row, stated as the concrete obligation it now is.** - An unexecutable helper on macOS yields **status 1 with no token**. - Both consumers must classify that as **boundary error → 2**, never - `ignored`. This is not a hypothetical: it is the observed CI failure - on `70f0bc9` (`Test (macos-latest / lua54)` and `… / luajit`), and - the row is only satisfied when that platform is green. -- **A7 — SATISFIED BY DISCLOSURE**, which is the fallback this - criterion allows when no non-Linux unix is reachable. Revision 12 - wrote A7 as "exercised there, **or** state what is claimed versus - what was tried"; an earlier draft of this line said A7 "stays open", - which **contradicted the approved contract** and is withdrawn. - - **Tried:** Linux `x86_64`, this machine, all three outcomes - (`safe` 0, `ignored` 1, `error` 2), for the helper, the gate and - the direct test. - - **Not tried:** every non-Linux unix. None was reachable. - - **Claimed:** the mechanism is POSIX, not Linux-specific — the - helper uses only `trap`, `kill -INT`, `$$`, `case` and `echo`, and - reads no `/proc` and calls no `sigaction`; the disposition - behaviour it detects is POSIX inheritance across `fork` and `exec`. - That is a contract argument, disclosed as such. Anyone porting to - BSD or macOS should re-run the three outcomes rather than trust it. +- **A6a — the macOS row, SCOPED TO THE GATE.** An unexecutable helper + on macOS makes `/bin/sh` exit **1 with no token**; the gate must + classify that as **boundary error → 2**, never `ignored`. Not + hypothetical: it is the observed CI failure on `70f0bc9` + (`Test (macos-latest / lua54)` and `… / luajit`), and the row is + satisfied only when that platform is green. + + **It does not apply to R-d, for two independent reasons**, and + revision 13 was wrong to state it for "both consumers": + - **R-d never sees that status.** The gate invokes the helper through + `/bin/sh`, which converts an exec failure into a shell exit status. + R-d uses Rust's `Command`, which returns a **spawn error with no + exit status at all** — a different code path reaching `error` by a + different route (conformance row 12, not row 5). + - **macOS CI does not compile R-d's test.** It lives inside + `#[cfg(feature = "crdt")] mod crdt`, and the macOS jobs run + `--no-default-features --features ` with no `crdt`; + `Test (crdt)` is `runs-on: ubuntu-latest`. + + So R-d's macOS behaviour is **unexercised**, and this framing does not + pretend otherwise. Closing that would need either a non-crdt-gated + R-d row or a macOS crdt job — **neither is proposed here**, and A7 + records the gap instead of hiding it. +- **A7 — PARTIALLY EXERCISED ON macOS, one defect found, R-d still + Linux-only.** Revision 12 closed this by disclosure because no + non-Linux unix was reachable. **That is now stale: macOS CI reached + it and measured it red**, so the disclosure fallback no longer + applies and the criterion is restated against evidence. + - **Exercised on macOS (`Test (macos-latest / lua54)` and + `… / luajit`, head `70f0bc9`):** five of the six helper/gate rows + pass — all three helper outcomes, gate refusal on `ignored`, and + gate refusal on a helper-reported `error`. + - **One known defect on macOS:** + `gate_maps_an_unexecutable_helper_to_error_not_ignored` fails, + status 1 with no token classified as `ignored`. This is the whole + reason for revision 13 (§4d), and A6a is the row that closes it. + - **R-d: Linux-only, unexercised on macOS**, because its test is + crdt-gated and the macOS jobs build without `crdt`. Stated as a + gap, not argued away. + - **Everything else remains a contract argument**: the helper is + POSIX shell only, reads no `/proc` and calls no `sigaction`. BSD + and other unixes are still untried. ## 8b. Superseded criteria, kept for the record