docs(framing): rewrite the contract §4c had only contradicted

Four findings on revision 11, all upheld.

1. The operative contract still said the opposite of §4c. Bet 1 read as
   open; §7 said the mechanism was unknown with D3/D4 pending; §8 kept
   the old criteria and a conditional A5; §9 claimed a journey-12(a)
   product repair; the ledger and the revision-10 paragraph still said
   D1/D2 had not started. Each is now rewritten as executed, withdrawn,
   discharged or superseded --- §9 in particular now records journey
   steps touched: NONE, for the stated reason that no product behaviour
   changes, with gate trustworthiness named as what the lane does
   affect.

2. The causal evidence is now portable and cleanly reproduced. The
   first capture came from d12.log, which finished five minutes BEFORE
   afe3631 committed the diagnostic code and ran in the reused d0a-B
   target --- inadmissible provenance, now marked as the first sighting
   only. Replaced by controlled arms on committed head 38f2af4,
   dirty=0, in this worktree's own target, with BYTE-IDENTICAL binary
   hashes across arms (0890b78cca22ac1e, ef6ff1c15e11062a): foreground
   exit=0 ok=2, background exit=101 failed=2 SigIgn=0x1007. The outer
   invocation is recorded as a first-class column, since it is the
   causal variable and every earlier "exact command" omitted it. The
   historical foreground/background mapping is marked RECONSTRUCTED
   from the transcript, not captured --- no pre-existing row carries an
   outer-invocation field, which is precisely why the matrix stayed
   confounded for nine revisions.

3. D4 was never executed, so bet 1 is WITHDRAWN BY SCOPE rather than
   falsified, and A5 is RETIRED BY SCOPE rather than struck. Nothing
   here shows a real wgpu session behaves correctly; what is shown is
   that no observed evidence of a user-facing defect survives. The lane
   is now gate/test correctness only.

4. The remedy is not selected. §7b evaluates four candidates --- runner
   normalisation, an early gate guard, fixture isolation via pre_exec,
   and a test-local precondition assertion --- with portability as a
   selection criterion, noting /proc is Linux-only while the suite is
   cfg(unix) and sigaction querying is unsafe. Likely R-b + R-d, but
   nothing is chosen or implemented here. Revision 11's leap from
   "pre_exec is unsafe" to "therefore an assertion" did not follow.

Also renames the meaningless african_close() helper (38f2af4).

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:35:26 +02:00
parent 38f2af41f7
commit 57d8dae511
No known key found for this signature in database
3 changed files with 162 additions and 31 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 — D0a DONE, mechanism unknown ## GPU launcher / probe SIGINT teardown — MECHANISM FOUND, remedy not 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.
@ -282,9 +282,23 @@ from #171 and #215.
- **No PR. Framing revision 10 at `docs/gpu-probe-sigint-framing.md`, - **No PR. Framing revision 10 at `docs/gpu-probe-sigint-framing.md`,
APPROVED 2026-08-19 at `4fba9f6`** — revision 9 was approved at APPROVED 2026-08-19 at `4fba9f6`** — revision 9 was approved at
`15c25ec`, but did **not** cover retiring D0b. Revision 10 does, with `15c25ec`, but did **not** cover retiring D0b. Revision 10 does, with
the A3 contingency preserved: if the demonstrated D1/D2 mechanism the A3 contingency preserved. **D1/D2 HAVE RUN and found the
does not account for the subset/full difference, D0b runs before mechanism: `SIGINT` was ignored group-wide (`SigIgn=0x1007`) because
closure. **D1/D2 are authorised but have not started.** Also: **NO 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
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
subset-vs-full matrix was confounded with it throughout. A3's
subset/full obligation is discharged by explanation, so D0b is not
needed. **Bet 1 withdrawn by scope and A5 retired by scope — D4 was
never executed**, so no claim is made that a real session behaves
correctly, only that no evidence of a user-facing defect survives.
Framing revision 11 AWAITING APPROVAL; **no remedy selected** (§7b
weighs runner normalisation, a gate guard, fixture isolation and a
test-local assertion). Also: **NO
IMPLEMENTATION and no fix proposed** — the mechanism is not known yet, IMPLEMENTATION and no fix proposed** — the mechanism is not known yet,
and the framing says so and the framing says so
rather than guessing. Revisions 1, 2 and 3 were each rejected on rather than guessing. Revisions 1, 2 and 3 were each rejected on

View File

@ -15,8 +15,8 @@ not extend to revision 10**, because retiring D0b (§7) materially
changed the approved diagnostic sequence — the revision 9 text made changed the approved diagnostic sequence — the revision 9 text made
D0b mandatory before every other diagnostic. Revision 10's approval D0b mandatory before every other diagnostic. Revision 10's approval
covers that retirement and the A3 contingency that preserves its covers that retirement and the A3 contingency that preserves its
obligation. D0a's execution and result (§4b) are reported; D1/D2 are obligation. **D1/D2 have since been executed under it and found the
authorised but have not started. mechanism (§4c).**
Revisions 1 and 2 were each rejected on five findings. Every correction Revisions 1 and 2 were each rejected on five findings. Every correction
is recorded in place rather than quietly rewritten, because three of is recorded in place rather than quietly rewritten, because three of
@ -456,23 +456,34 @@ on process age at all.** Key on identity.
## 6. Bets ## 6. Bets
1. **The failure is a real teardown defect** — a user pressing Ctrl-C 1. ~~The failure is a real teardown defect a user meets.~~
on `pmacs --gpu` sees the same hang. **This is a bet, not a **WITHDRAWN BY SCOPE — not falsified.** Every observed red run is
finding**, and the current witness does not reach the real GUI explained by inherited `SIG_IGN` from a background invocation
path: it goes through a wrapper script and `--headless-managed-probe` (§4c), so **no observed evidence of a user-facing defect remains**.
(`:1090-1093`), not a live wgpu frontend. Confirming or dropping this That is weaker than proving a real wgpu session is correct, and
bet is D4 below. **D4 was never executed** (§7), so the correct statement is: this
2. It is **not** a timing margin. A green run finishes in 0.15 s against lane is now **gate/test correctness only**, and any user-facing
a 5 s deadline — 33×. Margins that large do not erode. claim is out of its scope and unevidenced in both directions.
3. Therefore **raising the deadline is not a fix** and is out of scope. 2. **UPHELD.** It is **not** a timing margin — confirmed twice over: a
If the conclusion turns out to be that the deadline is wrong, that green run finishes in ~0.19 s against a 5 s deadline, and the
needs its own argument and its own approval. foreground arm passes while the background arm fails with the same
binaries.
3. **UPHELD, and now load-bearing.** Raising the deadline is not a fix
and remains out of scope: the signal is never delivered, so no
deadline is long enough.
## 7. First step — diagnostics keyed on identity, not age ## 7. First step — diagnostics keyed on identity, not age
No fix is proposed; the mechanism is unknown. The first commit is **EXECUTED. The mechanism is known (§4c): inherited `SIG_IGN`.** This
diagnostic only, and it must **discriminate** the three live candidates: section is kept as the record of what was run. D1/D2 discriminated the
blocked delivery, inherited ignore, and an escaped or wrong process three candidates — ignored rather than blocked delivery (`SigPnd` and
per-thread `SigBlk` all zero), and no escape from the group (shared
`pgid`). D3 is discharged by the controlled arms. **D4 was NOT
executed**, and bet 1 is withdrawn by scope rather than falsified.
As written, the step read: the first commit is diagnostic only, and it
must **discriminate** the three live candidates: blocked delivery,
inherited ignore, and an escaped or wrong process
group. group.
- **D0a — reproduce the onset endpoints CLEANLY** (§4a): `7599661` - **D0a — reproduce the onset endpoints CLEANLY** (§4a): `7599661`
@ -598,9 +609,43 @@ group.
before/after pair is what makes the claim provable. before/after pair is what makes the claim provable.
- **D3 — run the full sweep under D1/D2 until the failure is captured - **D3 — run the full sweep under D1/D2 until the failure is captured
*with* its diagnostics.** Only then propose a fix. *with* its diagnostics.** Only then propose a fix.
- **D4 — settle bet 1 separately.** Establish whether a real - **D4 — NOT EXECUTED.** It would have established whether a real
`pmacs --gpu` session, not the wrapper/headless probe, reproduces the `pmacs --gpu` session, rather than the wrapper/headless probe,
hang. The answer decides whether A5 is an obligation or is dropped. reproduces the hang. It is **not run and not needed**, because bet 1
is withdrawn by scope: with every observed failure explained by the
runner's invocation, there is no user-facing claim left for this lane
to make. **A5 is retired by scope, not falsified** — nothing here
demonstrates a real session behaves correctly, only that no evidence
of the contrary survives.
## 7b. Remedy options — evaluated, none yet selected
Revision 11 jumped from "`pre_exec` is `unsafe`" to "therefore a
precondition assertion". That does not follow: ruling out one mechanism
does not select another. Four candidates, with the trade-off that
decides each:
| # | 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-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-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 |
**They are not exclusive**, and the likely answer is R-b + R-d: a guard
that stops the whole gate from running in a state where several suites
are meaningless, plus a test that explains itself if it is ever reached
that way. R-a alone is what already failed. R-c is the only one that
makes the test genuinely invocation-independent, and it is also the one
that touches an `unsafe` prohibition, so it needs a separate decision.
**Portability is a selection criterion, not an afterthought.** Reading
`SigIgn` from `/proc/self/status` is Linux-only. The suite is already
`#![cfg(unix)]`, so a `/proc`-based guard would narrow it further to
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.
## 8. Acceptance criteria for the eventual fix ## 8. Acceptance criteria for the eventual fix
@ -627,17 +672,23 @@ Written now so the fix cannot quietly become "make the test pass".
to obtain green. to obtain green.
- **A5.** **Conditional on D4.** If bet 1 holds, this is unconditional: - **A5.** **Conditional on D4.** If bet 1 holds, this is unconditional:
Ctrl-C on a real `pmacs --gpu` session tears down the frontend and Ctrl-C on a real `pmacs --gpu` session tears down the frontend and
leaves the daemon running. If D4 shows the hang is reachable only leaves the daemon running. **RETIRED BY SCOPE**: D4 was not executed,
through the wrapper/headless path, bet 1 is dropped, A5 is struck, bet 1 is withdrawn, and this lane is recorded as **gate/test
and the lane is recorded as gate-correctness only. correctness only**. A5 is not claimed satisfied and not claimed
falsified — it is out of scope, and a user-facing teardown claim would
need its own lane and its own evidence.
## 9. Coherence impact (`COHERENCE.md` §20) ## 9. Coherence impact (`COHERENCE.md` §20)
- **Journey step touched: 12(a), "closing is clean."** Ctrl-C teardown - **Journey steps touched: NONE, as finally established.** Earlier
of a GPU session is exactly that step, whether or not its grade revisions claimed 12(a) "closing is clean", on the premise that this
moves. **Revision 1 said "journey steps touched: none", which was lane repairs Ctrl-C teardown. §4c withdraws that premise: no product
false** — it reasoned from grade movement, which §20 explicitly warns behaviour changes, because the failure is an artifact of how the test
against. runner is invoked. Revision 1's "none" reached the right answer by
the wrong route (grade movement, which §20 warns against); this is
the right answer for the stated reason.
- What the lane does touch is **gate trustworthiness**: seven red
sweeps that named a product defect and had none.
- **Grade movement: none expected.** This restores a property that is - **Grade movement: none expected.** This restores a property that is
supposed to hold, rather than opening a new one. supposed to hold, rather than opening a new one.
- **Interaction islands: none added.** - **Interaction islands: none added.**

View File

@ -157,6 +157,72 @@ source/environment interaction, or a fix before `7599661` followed by a
regression before `724b785`. And an ancestor outside the interval is regression before `724b785`. And an ancestor outside the interval is
irrelevant to whether the interval contains a regression. irrelevant to whether the interval contains a regression.
## D1/D2 — EXECUTED 2026-08-19. The outer invocation is the variable
**The causal variable is the OUTER invocation of the test runner**, so
it is recorded here as a first-class column. Earlier "exact commands"
in this file are incomplete for that reason: they gave the inner
`cargo` argv and omitted how the runner itself was started.
### Controlled arms, committed head, worktree-local target
Head `38f2af4`, `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`.
Inner command, identical in both arms:
```
cargo test --features crdt --no-fail-fast \
--test gpu_invocation_acceptance --test gpu_initial_target_acceptance \
-- ctrl_c_on_launcher_group
```
Outer invocation, the only difference:
```
fg: /home/jeans/build/pmacs-gate-targets/d0a/arms.sh fg
bg: setsid nohup /home/jeans/build/pmacs-gate-targets/d0a/arms.sh bg > /dev/null 2>&1 & disown
```
| arm | outer | exit | ok | failed | `SigIgn` | binary hashes | log sha256/16 |
|---|---|---|---|---|---|---|---|
| fg | foreground | 0 | 2 | 0 | not captured (no failure ⇒ no dump) | `91f51d0b…`=`0890b78cca22ac1e`, `6b4b8223…`=`ef6ff1c15e11062a` | `aaec01673691479a` |
| bg | `setsid nohup … &` | 101 | 0 | 2 | `0000000000001007` | **identical**: `0890b78cca22ac1e`, `ef6ff1c15e11062a` | `c744d85a84cb8683` |
**The binaries are byte-identical across arms** — same hashes, same
head, same target directory, `dirty=0` — so nothing but the outer
invocation varies. `arms.tsv` holds the raw rows.
### Disposition, measured directly
| context | child `SigIgn` | `SIGINT` |
|---|---|---|
| foreground | `0000000000001000` | bit 12 (SIGPIPE) only — deliverable |
| `setsid nohup … &` | `0000000000000007` | SIGHUP, SIGINT, SIGQUIT — ignored |
### The first D1/D2 capture, and why it is superseded
The capture quoted in framing §4c came from `d12.log`, which finished
14:10 — **five minutes before `afe3631` committed the diagnostic
code** — and ran in the reused `d0a-B` target directory rather than
this worktree's. Its signal facts agree with the arms above, but it is
**not admissible provenance**: uncommitted tree, foreign target. The
arms table replaces it, and `d12.log` is retained only as the first
sighting.
### Historical foreground/background mapping — RECONSTRUCTED
The claim that "every reduction was foreground and every full sweep was
backgrounded" is **reconstructed from this session's transcript, not
captured at run time**. No run before today recorded its outer
invocation, because none of the harnesses knew it mattered. It is
consistent with every observation and with the two arms above, but it
is inference, and rows R1–R10 and F1–F7 carry **no outer-invocation
field**. That gap is the direct cause of nine revisions spent on a
confounded matrix.
## D0a — EXECUTED 2026-08-19. Verdict: difference NOT captured ## D0a — EXECUTED 2026-08-19. Verdict: difference NOT captured
Ten runs, counterbalanced `A B B A A B B A A B`, N = 5 per endpoint, Ten runs, counterbalanced `A B B A A B B A A B`, N = 5 per endpoint,