docs(framing): revision 13 round 5 --- the spec reintroduced the shipped bug
Three blockers, all upheld. 1. The file-backed snippet dropped `|| status=$?` and invoked the helper bare. Under scripts/gate's `set -eu` that terminates the gate at the helper's non-zero exit, before the status is ever read --- which is the ORIGINAL shipped bug, reintroduced in the very section written to replace it. The load-bearing shape is restored and commented as such. 2. $tmp does not exist where the guard runs. The guard sits immediately after the worktree resolves and deliberately precedes the log directory, ambient root and GATE_TMPDIR, so it must create and own its capture directory --- with the cleanup trap armed BEFORE the helper is invoked, and a disarm on the safe path so the gate's own later trap setup is undisturbed. New A8 witnesses that no capture directory survives any path, including failure to create one: the guard was placed early to leave nothing behind, and a capture directory must not weaken that. 3. The case count was fiction. T0 + LF is a valid third encoding per status --- and is what the shipped helper actually emits, since it prints with echo --- and "a different valid token" has two possibilities per status, so sampling one left half the mismatches untested. Now enumerated: two valid encodings, six mismatched valid-token pairs each in both encodings, eight malformed classes, giving 14 per status x 3 = 42, plus four out-of-band cases = 46. Earlier drafts claimed twelve, then twenty-three, then thirty-four, each a count of a set that had not been enumerated; the document now says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
41b8c4c517
commit
a546a85476
|
|
@ -814,14 +814,41 @@ every other status → `(2, error)`. Consumers do not parse the inner
|
||||||
**Both consumers therefore capture to files and compare bytes.**
|
**Both consumers therefore capture to files and compare bytes.**
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
"$helper" >"$tmp/out" 2>"$tmp/err"
|
# The guard runs BEFORE the gate's own temporary roots exist, so it
|
||||||
status=$?
|
# creates and owns its capture directory --- and arms the cleanup
|
||||||
printf '%s' "$expected_token" >"$tmp/want"
|
# BEFORE the helper can be invoked, so no path can leave residue.
|
||||||
printf '%s\n' "$expected_token" >"$tmp/want_lf"
|
capture=$(mktemp -d "${TMPDIR:-/tmp}/pmacs-sigint.XXXXXX") || {
|
||||||
if cmp -s "$tmp/out" "$tmp/want" || cmp -s "$tmp/out" "$tmp/want_lf"
|
echo 'gate: could not create the SIGINT guard capture directory' >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
trap 'rm -rf "$capture"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
# `|| status=$?` IS LOAD-BEARING under `set -eu`: a bare invocation
|
||||||
|
# dies at the helper's non-zero exit and never reaches the
|
||||||
|
# assignment. This is the original shipped bug, and an earlier draft
|
||||||
|
# of THIS SECTION reintroduced it.
|
||||||
|
status=0
|
||||||
|
"$helper" >"$capture/out" 2>"$capture/err" || status=$?
|
||||||
|
|
||||||
|
printf '%s' "$expected_token" >"$capture/want"
|
||||||
|
printf '%s\n' "$expected_token" >"$capture/want_lf"
|
||||||
|
if cmp -s "$capture/out" "$capture/want" \
|
||||||
|
|| cmp -s "$capture/out" "$capture/want_lf"
|
||||||
then token_ok=1; else token_ok=0; fi
|
then token_ok=1; else token_ok=0; fi
|
||||||
|
|
||||||
|
# On the SAFE path the guard tidies up and disarms, so the gate's own
|
||||||
|
# later trap installation is undisturbed. Every refusing path exits,
|
||||||
|
# and the armed trap removes the directory for it.
|
||||||
|
# rm -rf "$capture"; trap - EXIT HUP INT TERM
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**The capture directory is guard-local by necessity.** The guard sits
|
||||||
|
immediately after the worktree resolves and deliberately *precedes*
|
||||||
|
the gate's log directory, ambient root and `GATE_TMPDIR`, so none of
|
||||||
|
those exist yet. It must therefore create its own, and it inherits
|
||||||
|
the same **no-residue invariant** the guard was placed early to
|
||||||
|
honour: a refused run leaves nothing behind.
|
||||||
|
|
||||||
Files preserve every byte including NUL, `cmp` compares bytes, and
|
Files preserve every byte including NUL, `cmp` compares bytes, and
|
||||||
`status` is the helper's own. Rust does the same by comparing
|
`status` is the helper's own. Rust does the same by comparing
|
||||||
`out.stdout` against `TOKEN` and `TOKEN + b"\n"`. Neither consumer
|
`out.stdout` against `TOKEN` and `TOKEN + b"\n"`. Neither consumer
|
||||||
|
|
@ -904,31 +931,51 @@ on every case.
|
||||||
- **`error (boundary)`** — anything else. Nothing trustworthy was
|
- **`error (boundary)`** — anything else. Nothing trustworthy was
|
||||||
returned, so the consumer owns the wording (see step 4).
|
returned, so the consumer owns the wording (see step 4).
|
||||||
|
|
||||||
**The matrix is a generated cross-product: ten token classes × three
|
**The matrix is a generated cross-product over token class, encoding
|
||||||
statuses, plus four out-of-band cases.** An earlier draft applied the
|
and status, plus four out-of-band cases — enumerated below and counted
|
||||||
|
honestly.** An earlier draft applied the
|
||||||
malformed classes only at status 0, so a validator that checked tokens
|
malformed classes only at status 0, so a validator that checked tokens
|
||||||
strictly for `0` and accepted arbitrary output at `1` passed every row.
|
strictly for `0` and accepted arbitrary output at `1` passed every row.
|
||||||
|
|
||||||
Token classes, written `T0`–`T9`:
|
**Encodings matter, and one of them is what production actually
|
||||||
|
emits.** The helper prints with `echo`, so the real output is
|
||||||
|
**`TOKEN` + LF**. Both encodings validate:
|
||||||
|
|
||||||
| class | stdout content |
|
```
|
||||||
|
E1 := TOKEN (bare)
|
||||||
|
E2 := TOKEN LF (what the shipped helper emits)
|
||||||
|
```
|
||||||
|
|
||||||
|
Per status, the cases are:
|
||||||
|
|
||||||
|
| class | stdout | count | expected |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **V** | the **correct** token for this status, in `E1` and `E2` | 2 | **validates** |
|
||||||
|
| **M** | each of the **two other valid tokens**, in `E1` and `E2` | 4 | boundary |
|
||||||
|
| **E** | empty | 1 | boundary |
|
||||||
|
| **U** | `pmacs-sigint-v2:…` | 1 | boundary |
|
||||||
|
| **L** | LF + token | 1 | boundary |
|
||||||
|
| **X** | token + LF + LF | 1 | boundary |
|
||||||
|
| **S** | `␠` token `␠` | 1 | boundary |
|
||||||
|
| **C** | token + CR + LF | 1 | boundary |
|
||||||
|
| **D** | token token (one line) | 1 | boundary |
|
||||||
|
| **N** | token + NUL | 1 | boundary |
|
||||||
|
|
||||||
|
That is **14 per status × 3 statuses = 42**.
|
||||||
|
|
||||||
|
The **six mismatched valid-token pairs** are enumerated rather than
|
||||||
|
sampled, because choosing one per status would leave half of them
|
||||||
|
untested:
|
||||||
|
|
||||||
|
| status | wrong tokens (each in `E1` and `E2`) |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `T0` | the token **correct for the status under test** |
|
| 0 | `…:ignored`, `…:error` |
|
||||||
| `T1` | empty |
|
| 1 | `…:safe`, `…:error` |
|
||||||
| `T2` | a different *valid* token (mismatched) |
|
| 2 | `…:safe`, `…:ignored` |
|
||||||
| `T3` | `pmacs-sigint-v2:…` (unknown version) |
|
|
||||||
| `T4` | LF + token (leading newline) |
|
|
||||||
| `T5` | token + LF + LF (extra newline) |
|
|
||||||
| `T6` | `␠` token `␠` (surrounding spaces) |
|
|
||||||
| `T7` | token + CR + LF |
|
|
||||||
| `T8` | token token (doubled, one line) |
|
|
||||||
| `T9` | token + NUL |
|
|
||||||
|
|
||||||
**The rule is the whole table:** for statuses 0, 1 and 2, **only `T0`
|
Only **V validates** — `(0,safe)` → `safe`, `(1,ignored)` → `ignored`,
|
||||||
validates** — giving `safe`, `ignored` and `error (validated)`
|
`(2,error)` → `error (validated)`. The other **36** are
|
||||||
respectively. **All other 27 combinations are `error (boundary)`.**
|
`error (boundary)`.
|
||||||
`T0` with a single trailing LF also validates, at every status, since
|
|
||||||
the grammar admits it.
|
|
||||||
|
|
||||||
Out-of-band cases, which have no `(status, token)` form:
|
Out-of-band cases, which have no `(status, token)` form:
|
||||||
|
|
||||||
|
|
@ -936,12 +983,14 @@ Out-of-band cases, which have no `(status, token)` form:
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| X1 | status 126 with a correct token | `error (boundary)` — status outside 0–2 |
|
| X1 | status 126 with a correct token | `error (boundary)` — status outside 0–2 |
|
||||||
| X2 | spawn failure (missing / unexecutable helper) | `error (boundary)`, **no status inspected** |
|
| X2 | spawn failure (missing / unexecutable helper) | `error (boundary)`, **no status inspected** |
|
||||||
| X3 | status 1, `T1`, stderr = canonical ignored text | `error (boundary)`, and the output **must not** present "SIGINT is ignored" as the diagnosis |
|
| X3 | status 1, empty stdout, stderr = canonical ignored text | `error (boundary)`, and the output **must not** present "SIGINT is ignored" as the diagnosis |
|
||||||
| X4 | status 0, `T0`, plus extra bytes on **stderr** | `safe` — stderr is not consulted for classification |
|
| X4 | status 0, correct token, plus extra bytes on **stderr** | `safe` — stderr is not consulted for classification |
|
||||||
|
|
||||||
That is **34 cases**: 30 from the cross-product plus X1–X4. Both
|
**Truthful total: 42 + 4 = 46 concrete cases.** Earlier drafts said
|
||||||
validators are exercised against all of them and must agree on every
|
twelve, then twenty-three, then thirty-four; each was a count of a set
|
||||||
one, including the distinction between validated and boundary error.
|
that had not actually been enumerated. Both validators are exercised
|
||||||
|
against all forty-six and must agree on every one, including the
|
||||||
|
distinction between validated and boundary error.
|
||||||
|
|
||||||
The two consumers:
|
The two consumers:
|
||||||
|
|
||||||
|
|
@ -1042,6 +1091,14 @@ show:
|
||||||
revision 13 to cover the pair: a **missing**, **mismatched** or
|
revision 13 to cover the pair: a **missing**, **mismatched** or
|
||||||
**unknown** token is `error` in both consumers, whatever the status
|
**unknown** token is `error` in both consumers, whatever the status
|
||||||
accompanying it.
|
accompanying it.
|
||||||
|
- **A8 — the guard leaves no residue, on every path.** The guard
|
||||||
|
creates its own capture directory because it runs before the gate's
|
||||||
|
temporary roots exist, and arms its cleanup **before** invoking the
|
||||||
|
helper. After `safe`, `ignored`, validated `error`, boundary `error`,
|
||||||
|
and a failure to create the directory at all, **no capture directory
|
||||||
|
survives**. This is the same no-residue invariant that put the guard
|
||||||
|
early in the first place; adding a capture directory must not weaken
|
||||||
|
it.
|
||||||
- **A6b — a boundary failure never speaks with the helper's voice.**
|
- **A6b — a boundary failure never speaks with the helper's voice.**
|
||||||
A helper exiting **1 with no token but the canonical
|
A helper exiting **1 with no token but the canonical
|
||||||
`SIGINT is ignored` text on stderr** classifies as boundary `error`
|
`SIGINT is ignored` text on stderr** classifies as boundary `error`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue