docs(framing): totalise the helper contract; capture arm digests per run

Three findings, all upheld.

1. The arm provenance was malformed and over-claimed. The "fully
   expanded" background command still contained <the fg command above>
   and <log> 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-19 14:58:50 +02:00
parent 77b623c6ea
commit f607e82263
No known key found for this signature in database
3 changed files with 79 additions and 36 deletions

View File

@ -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 that never appears is worse than no documentation**, because the reader
waits for a signal that is not coming. 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 **Written with the branch's FIRST commit**, per the standing correction
from #171 and #215. from #171 and #215.
@ -480,22 +480,29 @@ from #171 and #215.
contract below — a bare difference decides nothing. **(a) is DONE.** contract below — a bare difference decides nothing. **(a) is DONE.**
**(b) is RETIRED as a precondition** (2026-08-19) because D0a yielded **(b) is RETIRED as a precondition** (2026-08-19) because D0a yielded
a reliable direct reproduction and D1/D2 measure the mechanism 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 account for the subset-vs-full difference, D0b runs before the lane
closes. It read: re-run closes. It read: re-run
the matrix at `main` under a harness capturing provenance **and the the matrix at `main` under a harness capturing provenance **and the
artifact hashes executed at run time**, since command shape silently artifact hashes executed at run time**, since command shape silently
changed the binary once already and a hash computed later reflects changed the binary once already and a hash computed later reflects
only what occupies that path now. only what occupies that path now.
- **Coherence: journey step 12(a), "closing is clean", IS touched** — - **Coherence: journey steps touched: NONE** (framing §9). Earlier
Ctrl-C teardown of a GPU session is that step, grade movement or not. entries here assigned 12(a) "closing is clean" on the premise that
Revision 1 claimed no journey step, reasoning from grade movement, the lane repairs Ctrl-C teardown; §4c withdraws that premise, because
which §20 explicitly warns against. no product behaviour changes. What the lane affects is **gate
trustworthiness**.
- **Gates:** `./scripts/gate --protocol --acceptance - **Gates:** `./scripts/gate --protocol --acceptance
gpu_invocation_acceptance` at minimum; A2 requires `sweep-crdt` green gpu_invocation_acceptance` at minimum. **The old three-consecutive-
**three consecutive runs on the reviewed fixed head of this branch** run A2 contract is SUPERSEDED** — it was written for a teardown fix
— not "on main", which is unobtainable before approval and merge. whose flakiness was unexplained. The mechanism is now known and
1/1 is not evidence for a defect that hid from every reduction. 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 ## `scripts/gate` TMPDIR isolation — PR #240 OPEN

View File

@ -632,7 +632,7 @@ decides each:
| # | remedy | effect | cost / risk | | # | 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-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-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 | | 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 no-op and it exits **0**. Verified in both contexts: 23 foreground, 0
under `setsid nohup … &`. 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 This is **POSIX shell only** — `trap`, `kill`, `$$` — so it settles the
portability question §7b raised: no `/proc`, hence not Linux-only, and portability question §7b raised: no `/proc`, hence not Linux-only, and
no `sigaction`, hence no `unsafe`. no `sigaction`, hence no `unsafe`.
@ -708,7 +729,17 @@ show:
in the normal case. in the normal case.
- **A4 — mutation.** Removing the probe's `trap`, or treating exit 0 as - **A4 — mutation.** Removing the probe's `trap`, or treating exit 0 as
"deliverable", makes A1 and A2 fail; each mutation is named against "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 - **A5 — the gate is otherwise unchanged**: a normal foreground run
reaches and passes every stage it did before, with no stage added, reaches and passes every stage it did before, with no stage added,
skipped, reordered, or made conditional. skipped, reordered, or made conditional.

View File

@ -179,12 +179,11 @@ cargo test --features crdt --no-fail-fast \
-- ctrl_c_on_launcher_group -- ctrl_c_on_launcher_group
``` ```
Outer invocation, the only difference. `arms.sh` is machine-local, so Outer invocation — the only difference, written out with **no
the commands are given **fully expanded** — a reader elsewhere needs no placeholders**:
access to it:
``` ```
# fg arm # fg arm — run directly in an interactive foreground shell
cd /home/jeans/Repos/personal/pmacs-probe-sigint && \ cd /home/jeans/Repos/personal/pmacs-probe-sigint && \
env TMPDIR=/home/jeans/build/pmacs-gate-targets/tmp/arms \ env TMPDIR=/home/jeans/build/pmacs-gate-targets/tmp/arms \
CARGO_TARGET_DIR=/home/jeans/build/pmacs-gate-targets/pmacs-probe-sigint-84ed0f9e \ 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 \ --test gpu_invocation_acceptance --test gpu_initial_target_acceptance \
-- ctrl_c_on_launcher_group -- ctrl_c_on_launcher_group
# bg arm — byte-identical inner command, wrapped: # bg arm — the identical command, wrapped
setsid nohup sh -c '<the fg command above>' > <log> 2>&1 & disown 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`, ### Result — head `77b623c`, `dirty=0`, digests captured PER RUN
`git status --porcelain | wc -l`, the exit status, both copies'
results, the executed suffixes, their hashes, and the log digest.
| arm | outer | exit | ok | failed | `SigIgn` | binary hashes | log sha256/16 | | arm | outer | exit | ok | failed | `SigIgn` | log sha256 (full) |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|
| fg | foreground | 0 | 2 | 0 | not captured (no failure ⇒ no dump) | `aaec01673691479a…` (prefix) | | fg | foreground | 0 | 2 | 0 | not captured (no failure ⇒ no dump) | `b6117619242f3dabadbe5826f59f11cef451cd09a2beb304e9a3dec1a4616465` |
| bg | `setsid nohup … &` | 101 | 0 | 2 | `0000000000001007` | `c744d85a84cb8683…` (prefix) | | 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:
``` | binary | fg arm | bg arm |
gpu_initial_target_acceptance-91f51d0b5303ff9f |---|---|---|
0890b78cca22ac1e80b79845f85fb6e88def3330db15ae123a2a672d3084124c | `gpu_initial_target_acceptance-91f51d0b5303ff9f` | `0890b78cca22ac1e80b79845f85fb6e88def3330db15ae123a2a672d3084124c` | *(identical)* |
gpu_invocation_acceptance-6b4b8223dea45247 | `gpu_invocation_acceptance-6b4b8223dea45247` | `ef6ff1c15e11062ab53a075763814f32c1bbc9be1b146d068c60e91fa247c696` | *(identical)* |
ef6ff1c15e11062ab53a075763814f32c1bbc9be1b146d068c60e91fa247c696
```
Same head, same target directory, `dirty=0`, and the binaries were not **These are byte-identical, and that claim is now carried by the
rebuilt between arms — so nothing but the outer invocation varies. The capture rather than by inference.** The earlier arms table recorded only
**log** digests above are 16-character **prefixes**, not full values, 16-character prefixes at run time and its full values were read
and are identifiers only; no claim rests on them. 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 ### Disposition — UNRECORDED CORROBORATION, not a controlled arm