Merge pull request #229 from levineuwirth/gate-protocol-build
build(gate): run the build the CRDT sweep depends on
This commit is contained in:
commit
7cf4653417
|
|
@ -265,6 +265,208 @@ also removed: this branch's "R8 NEEDS A LANE" investigation block, and
|
|||
durable facts are in the retired registry row and the handoff §6
|
||||
census.
|
||||
|
||||
## `scripts/gate --protocol` build step — IMPLEMENTED at `49bc141`, RE-OPENED by review, witness CLOSED at `677fd25`. No PR yet
|
||||
|
||||
**PR #229 OPEN** — https://github.com/levineuwirth/pmacs/pull/229,
|
||||
opened at `93d557a`. **Held, not merged.** Its first CI run went red on
|
||||
`Test (macos-latest / lua54)`; the rerun turned that selector green and
|
||||
went red on a **different** one. Both are recorded as **U4** and **U5**
|
||||
in `docs/ci-red-signatures.md`, as separate incidents per the matching
|
||||
rule rather than one signature twice.
|
||||
|
||||
**Registry numbering conflict, expected at merge.**
|
||||
`worker-identity-stage1` independently defines its own **U4** and **U5**
|
||||
on its branch. This lane merges first, so on `main` the rows above are
|
||||
U4/U5 and **worker identity must renumber its pair on rebase**. Flagged
|
||||
here because a rebase that resolves the textual conflict without
|
||||
renumbering produces two different incidents sharing an id, which is
|
||||
exactly the failure the registry's matching rule exists to prevent.
|
||||
|
||||
**Written with the lane's first commit**, per the standing correction
|
||||
from #171 and #215.
|
||||
|
||||
**Branch `gate-protocol-build`**, base `githubsucks/main` @ `4bc55e8`
|
||||
(the #225 merge). **`githubsucks/gate-protocol-build` is the
|
||||
authoritative tip** — the ref, not a SHA. Recover with
|
||||
`git fetch githubsucks && git checkout gate-protocol-build`.
|
||||
|
||||
- **Framing `docs/gate-protocol-build-framing.md`, revision 5.** The
|
||||
fix itself is implemented and green at `49bc141`; **its regression
|
||||
witness landed separately at `677fd25`**, after review found the
|
||||
original witness did not reach the step it named. Narrow by design:
|
||||
one missing step in one script, plus the boundary question that let
|
||||
it go missing. No `src/`, no protocol, no feature work.
|
||||
- **WAS THE OPEN BLOCKER — the witnesses did not reach the step they
|
||||
name. CLOSED at `677fd25`.** `--print-plan` **strips names** before
|
||||
printing, so the ordering assertion saw only commands; `--self-test`
|
||||
**hardcodes** `build-crdt` inside its own synthetic plan. Review
|
||||
demonstrated the consequence: **renaming the real build step to
|
||||
`sweep-crdt` left both tests passing.** So the lane had shipped
|
||||
without the regression guard it was created to provide. §7 requires
|
||||
**both** real emitter pairs — `build-crdt` and `sweep-crdt`, name
|
||||
*and* exact command — because the hole is symmetric and revision 4
|
||||
closed only half of it. The synthetic `--self-test` stays: it
|
||||
witnesses the *runner* (failure naming, `FAILED:` list, log paths,
|
||||
non-zero exit, and continuation via the sentinel), which is a
|
||||
different thing from attributing the real step, and it may no longer
|
||||
stand in for it. **What closed it is the "THE WITNESS DID NOT REACH
|
||||
THE STEP" bullet further down** — `--print-plan-named`, with all four
|
||||
renames and drifts mutated red.
|
||||
- **The defect, as found.** `--protocol` adds the CRDT workspace sweep,
|
||||
whose documented precondition is `cargo build --workspace
|
||||
--no-default-features --features luajit,crdt` — documented in handoff
|
||||
**§5** at the time, **§3** now. The plan emitter had **no build step
|
||||
at all** — read from the source, not inferred from the failure.
|
||||
- **Why it was latent, and why that makes it urgent rather than tidy.**
|
||||
Before #225 every worktree shared one `CARGO_TARGET_DIR`, which
|
||||
almost always already held a `pmacs-gpu` binary, so the precondition
|
||||
was satisfied **by accident**. Per-worktree target dirs start empty.
|
||||
The hazard is not the red gate that stops you — it is a **green**
|
||||
`--protocol` run whose crdt sweep was decided by the state of the
|
||||
build directory rather than by the diff. That is a gate reporting
|
||||
coverage it does not have, which is what #225 exists to prevent.
|
||||
- **Observed on PR #228's first gate run:** twelve
|
||||
`gpu_invocation_acceptance::crdt::*` failures, all *"build pmacs-gpu
|
||||
before this acceptance suite"*, with `debug/pmacs-gpu` absent.
|
||||
- **The durable half is a boundary question.** `scripts/gate`'s header
|
||||
names handoff **§3** as the owner of its reasoning, and this
|
||||
precondition lives in **§5** — a coherent cause for the omission, not
|
||||
mere oversight.
|
||||
|
||||
**Resolved in framing revision 2: §3 becomes the SOLE normative home,
|
||||
§5 keeps the incident and its signature as history, and the script's
|
||||
header keeps citing §3 ALONE.** Revision 1 proposed citing both,
|
||||
which splits one executable contract across two homes and weakens the
|
||||
script's only clean boundary at the same time as Q#GR-4 declines to
|
||||
build any automated check for prose drift. This entry recorded that
|
||||
superseded decision until now; a recovering machine reading the stale
|
||||
version would have rebuilt revision 1's wrong boundary.
|
||||
- **Q#GR-1 — SETTLED BY OBSERVATION, 2026-08-09, before any fix was
|
||||
written.** On a **disposable** target directory (never a live lane's),
|
||||
with `debug/pmacs-gpu` asserted **absent before each run** as a
|
||||
recorded precondition, each sweep run **alone** from that same cold
|
||||
state so neither could have built the binary for the other:
|
||||
|
||||
| sweep | exit | result | `pmacs-gpu` after |
|
||||
|---|---|---|---|
|
||||
| default | **0** | green, 114 test targets | **still absent** |
|
||||
| crdt | **101** | exactly **12** failures, all `gpu_invocation_acceptance::crdt::*`, all *"build pmacs-gpu before this acceptance suite"* | still absent |
|
||||
|
||||
So framing §3's inference **holds** and §4's *"only under
|
||||
`--protocol`"* is correct — the default sweep never builds the binary
|
||||
and never needs it. **Mechanism, now established rather than
|
||||
guessed:** `pmacs-gpu` has no `tests/` directory, so cargo never
|
||||
uplifts its bin to `debug/pmacs-gpu`; only an explicit `cargo build`
|
||||
produces it.
|
||||
|
||||
**Found while doing it, and worse than the twelve:**
|
||||
`bottom_panel_stage2b_gpu` a54 reported **`ok`** in that cold crdt
|
||||
sweep. Its only path that does not spawn `pmacs-gpu` is its skip
|
||||
branch, so a test whose whole purpose is real wgpu rendering passed
|
||||
having rendered nothing. The missing build does not merely fail
|
||||
twelve tests — it voids coverage in tests that report green.
|
||||
(`vterm_stage3` a37 has the same shape by source read; cargo captures
|
||||
passing tests' output, so the skip is invisible in the log.)
|
||||
- **What landed.** A named `build-crdt` step emitted immediately before
|
||||
`sweep-crdt` under `--protocol`, carrying the exact §5 invocation —
|
||||
**not** folded into the sweep command, because `cargo build … &&
|
||||
cargo test …` reports a *build* failure under the name `sweep-crdt`.
|
||||
Plus **`--self-test`** (Q#GR-5): a hardcoded three-line synthetic plan
|
||||
— pass, fail-named-`build-crdt`, **pass sentinel** — driven through
|
||||
the *real* runner loop, which is what makes the **runner's** failure
|
||||
naming *and* continuation observable at all. (It does **not** witness
|
||||
the real step's name — see the round-two entry below, which is where
|
||||
that gap was found and closed.) `PLAN_FILE` is deliberately **not**
|
||||
injectable: that would turn the runner's `eval` into a general command
|
||||
executor, the same defect this script's review caught in
|
||||
`--acceptance`.
|
||||
- **THE WITNESS DID NOT REACH THE STEP — found in review of the
|
||||
implementation, closed at `677fd25`.** The lane shipped without the
|
||||
regression guard it was created to provide, because **neither witness
|
||||
could see a name**: `--print-plan` renders `emit_plan | cut -f2-`, so
|
||||
the ordering test compared *commands* with the names cut off, and
|
||||
`--self-test` hardcodes the string `build-crdt` in its **own
|
||||
synthetic** plan, so it proves things about the runner and nothing
|
||||
about the real emitter. Review demonstrated it directly: **renaming
|
||||
the real build step to `sweep-crdt` left both tests passing** — a plan
|
||||
that would report a build failure under the sweep's name, sitting
|
||||
green, which is the exact misattribution the separate step exists to
|
||||
prevent.
|
||||
|
||||
**The fix is `--print-plan-named`**: a second *rendering* of the same
|
||||
`emit_plan`, printing the `name<TAB>command` text the runner reads
|
||||
back from `PLAN_FILE`, asserted by **whole-line equality** so name and
|
||||
command are pinned together, and `sweep-crdt`'s pair asserted too
|
||||
(asserting only the build's name leaves the identical hole open in the
|
||||
other direction). **`PLAN_FILE` remains uninjectable** — a test that
|
||||
supplied the runner's plan would turn its `eval` into a general
|
||||
command executor, the defect the `--acceptance` refusal exists to
|
||||
prevent — and **`--self-test` stays**, witnessing the *runner* (failure
|
||||
naming, `FAILED:`, log paths, non-zero exit, continuation via the
|
||||
sentinel), which it may no longer *stand in for* attribution of the
|
||||
real step. A companion test pins `--print-plan` as that rendering
|
||||
minus its names, so the two cannot drift into asserting a name the
|
||||
runner never uses. Both new tests are on the **no-gates** paths.
|
||||
|
||||
**Mutated individually, each now red** (the first is the one the
|
||||
previous round passed): build renamed `build-crdt` → `sweep-crdt`;
|
||||
sweep renamed `sweep-crdt` → `crdt-sweep`; build features
|
||||
`luajit,crdt` → `luajit`; build emitted **after** the sweep. Suite is
|
||||
20 tests.
|
||||
- **AUDITED FOR THE SAME DEFECT ELSEWHERE, and one instance is left
|
||||
open deliberately.** Renaming **every other** plan step — `fmt`,
|
||||
`clippy`, `lib`, `m4`, `gpu`, `sweep`, `diff-check`,
|
||||
`acceptance-<suite>` — leaves all 20 tests green: no test asserts any
|
||||
step name but `build-crdt` and `sweep-crdt`. For most that is only a
|
||||
log filename and a `FAILED:` entry. **`sweep` is not**: the runner's
|
||||
end-of-run listing globs `"$LOGDIR"/*-sweep.log` and
|
||||
`*-sweep-crdt.log`, so renaming that step silently empties the *"read
|
||||
these, do not re-run and grep"* listing that is the U2/U3 remedy, with
|
||||
the suite still green. **Not closed here**: the listing only exists on
|
||||
the *run* path, and every test in this file is deliberately no-gates,
|
||||
so there is no cheap witness for it — recorded rather than papered
|
||||
over.
|
||||
- **Blocks PR #228 (discovery Stage 2).** That lane's `--protocol`
|
||||
result needs re-establishing on a fresh target dir under the repaired
|
||||
script. Deliberately **not** folded into that feature branch, and it
|
||||
happens **after** this lands, not inside it.
|
||||
- **Acceptance criterion, witnessed 2026-08-09.**
|
||||
`scripts/gate --acceptance gate_script_acceptance --protocol` on a
|
||||
target root that **did not exist** (precondition recorded, not
|
||||
assumed): all eleven steps green, `09 build-crdt ok` producing
|
||||
`debug/pmacs-gpu`, and `gpu_invocation_acceptance` at **15 passed /
|
||||
0 failed** where the same suite was 3/12 without the build step.
|
||||
Zero occurrences of the *"build pmacs-gpu"* signature in the sweep
|
||||
log, and a54/a37 ran for real rather than taking their skip branches.
|
||||
**No manual build anywhere** — which is the thing that was false.
|
||||
- **UNEXPLAINED RED, recorded rather than swept up.** An earlier
|
||||
attempt at the same cold run failed step 10 with **36 + 4 + 6 + 4
|
||||
failures across `m5_5`/`m5_6`/`m5_7`/`m5_8`**, all real-daemon
|
||||
suites, all with signature *"daemon exited with exit status: 101
|
||||
before socket appeared; socket=/tmp/.tmpXXXX/pmacs.sock — `<stderr
|
||||
empty>`"*. **Not** the `pmacs-gpu` signature, and no row in
|
||||
`docs/ci-red-signatures.md` matches it. Re-running the same test
|
||||
binary from the same target directory gave 36/36 green, which by
|
||||
that registry's own rule establishes **intermittence only, never
|
||||
environmental cause** — so this stays open rather than being
|
||||
attributed to the load (~25–30 across four concurrent lanes' gates).
|
||||
**De-implicated from `build-crdt` by construction, not by the green
|
||||
rerun:** the root crate's `default = ["luajit"]`, so
|
||||
`--no-default-features --features luajit,crdt` enables *exactly* the
|
||||
same feature set as the sweep's `--features crdt`. The build step
|
||||
cannot hand the sweep a differently-featured binary, so it has no
|
||||
mechanism by which to break a daemon suite. Local, not CI, so not a
|
||||
registry row; noted here for whoever sees it next.
|
||||
- **Gates:** `scripts/gate --acceptance gate_script_acceptance`. Note
|
||||
the recursion — this lane edits the script that runs its own gates,
|
||||
so `--print-plan`, `--print-plan-named`, `--help` and `--self-test`
|
||||
were also checked by hand after each edit: a change that breaks the
|
||||
script cannot be reported honestly by the script. The assertions were
|
||||
**mutation tested**: wrong features, wrong position, unconditional
|
||||
emission, an aborting runner, the build folded into `sweep-crdt`, and
|
||||
— added in the second round — a **rename of either** the build or the
|
||||
sweep step each fail the suite.
|
||||
|
||||
## QoL arc retirement — PR #224 OPEN (docs only)
|
||||
|
||||
**PR #224** — https://github.com/levineuwirth/pmacs/pull/224. Written
|
||||
|
|
|
|||
|
|
@ -529,10 +529,21 @@ someone forgot.
|
|||
which is why name-keyed lists are not trustworthy.
|
||||
- **`basedpyright` hangs forever** — always
|
||||
`cargo test --test m4_acceptance -- --skip basedpyright`.
|
||||
- **The crdt sweep needs `cargo build --workspace` first**, or twelve
|
||||
`gpu_invocation_acceptance` tests fail on a missing `pmacs-gpu`
|
||||
binary. `cargo build --workspace --no-default-features --features
|
||||
luajit,crdt` is the invocation that produces both binaries.
|
||||
- **INCIDENT — the crdt sweep's missing build step. §3 now owns this
|
||||
requirement; what stays here is the history and the signature.**
|
||||
Signature: twelve `gpu_invocation_acceptance::crdt::*` failures, each
|
||||
*"build pmacs-gpu before this acceptance suite"*, on a target
|
||||
directory with no `debug/pmacs-gpu` in it. Seen on **PR #228's first
|
||||
gate run** (2026-08-09) — the first `--protocol` run in a *fresh*
|
||||
per-worktree target directory after #225. Latent for the whole life
|
||||
of the shared `CARGO_TARGET_DIR`, which nearly always already held
|
||||
the binary, so the precondition was met by accident. Reproduced
|
||||
deliberately the same day on a disposable cold target: default sweep
|
||||
exit 0, crdt sweep exit 101 with exactly those twelve.
|
||||
**`scripts/gate --protocol` now runs the build as a named
|
||||
`build-crdt` step**, so this signature appearing again means the
|
||||
script was bypassed, not that the requirement moved. The invocation,
|
||||
the measurement and the reasoning are in §3.
|
||||
- **A shared `CARGO_TARGET_DIR` makes concurrent sweeps unattributable.**
|
||||
Every worktree on this machine resolves to the same target directory,
|
||||
so `target/debug/pmacs` is a **shared mutable file**: a
|
||||
|
|
@ -2298,15 +2309,50 @@ eight broken version assertions on CI. When the two disagree, **this
|
|||
list wins**.
|
||||
|
||||
**Touching `PROTOCOL_VERSION` STRENGTHENS the sweep line. It does not
|
||||
replace it:**
|
||||
replace it — and the crdt sweep has a BUILD PRECONDITION:**
|
||||
|
||||
```
|
||||
cargo test --workspace --no-fail-fast -- --skip basedpyright
|
||||
cargo build --workspace --no-default-features --features luajit,crdt
|
||||
cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright
|
||||
```
|
||||
|
||||
Every part is load-bearing:
|
||||
|
||||
- **The build before the crdt sweep is a PRECONDITION, not a courtesy,
|
||||
and it is gate policy rather than an ops tip.** The crdt sweep spawns
|
||||
`pmacs-gpu` as a *process*, and no `cargo test` run produces that
|
||||
binary: `pmacs-gpu` has no `tests/` directory, so cargo never uplifts
|
||||
its bin to `debug/pmacs-gpu`. Omit the build on a cold target
|
||||
directory and twelve `gpu_invocation_acceptance::crdt::*` tests fail
|
||||
on *"build pmacs-gpu before this acceptance suite"* — and, worse,
|
||||
crdt tests that drive the real binary (`vterm_stage3` a37,
|
||||
`bottom_panel_stage2b_gpu` a54) take their skip branch and report
|
||||
**`ok`**, so the missing build also voids coverage *silently*.
|
||||
`scripts/gate --protocol` emits it as its own named `build-crdt`
|
||||
step, never folded into the sweep command, so a build failure is
|
||||
attributed to the build rather than to the sweep.
|
||||
|
||||
**Only the crdt sweep needs it, and that is MEASURED rather than
|
||||
reasoned.** On 2026-08-09, on a disposable target directory with
|
||||
`debug/pmacs-gpu` asserted **absent before each run** and each sweep
|
||||
run alone from that cold state: the **default** sweep exited **0**
|
||||
(114 test targets green) and left `debug/pmacs-gpu` **still absent**
|
||||
— it never builds the binary and never needs it — while the **crdt**
|
||||
sweep exited **101** with exactly those twelve failures. An
|
||||
unconditional build would be a real cost paid for nothing on every
|
||||
ordinary lane.
|
||||
|
||||
**Why this was latent for years.** Before per-worktree target
|
||||
directories (#225) every worktree on this machine shared one, which
|
||||
nearly always already held a `pmacs-gpu` from some earlier build, so
|
||||
the precondition was satisfied **by accident** on essentially every
|
||||
run. The hazard is not the red gate — a red gate stops you. It is a
|
||||
**green** `--protocol` run whose crdt sweep was decided by what
|
||||
happened to be in the build directory rather than by the diff, which
|
||||
is a gate reporting coverage it does not have. §5 keeps the incident
|
||||
and its signature as history; **this section owns the requirement.**
|
||||
|
||||
- **`--workspace`, never `--tests`.** `--tests` selects 108 targets
|
||||
where `--workspace` selects 110, and the two it drops are
|
||||
**`pmacs_protocol` and `pmacs_gpu`**. On a protocol bump, dropping
|
||||
|
|
|
|||
|
|
@ -566,3 +566,38 @@ The #214 occurrence is the strongest available evidence that these are
|
|||
not caused by the PRs they appeared on — that PR is **docs-only and its
|
||||
tree is byte-identical to a green `main`**. It is not evidence that any
|
||||
of them is harmless.
|
||||
|
||||
### U4 — `a_pty_resize_blanks_the_host_before_repainting`, macOS `lua54`, one occurrence
|
||||
|
||||
Surfaced on PR #229's CI.
|
||||
|
||||
| field | value |
|
||||
|---|---|
|
||||
| **selector** | `--test full_grid_resync_acceptance a_pty_resize_blanks_the_host_before_repainting` |
|
||||
| **job / flavor** | GitHub Actions, `Test (macos-latest / lua54)`, `macos-26-arm64` |
|
||||
| **required fragments** | `FG-INV: the post-resize resync must blank the host` · `no CSI 2 J appeared in the` · `bytes emitted after the first painted frame` |
|
||||
| **NOT fragments** | the byte count and the `:LINE` suffix are **occurrence-specific** and must not be matched on — the count is the collected suffix length, which varies per run, and the line moves with the file |
|
||||
| **status** | **one occurrence; INTERMITTENT — passed on rerun** |
|
||||
| **why the diff is excluded** | #229 changes only `scripts/gate`, `tests/gate_script_acceptance.rs` and documentation — **no `src/`, and the workflow never invokes `scripts/gate`**. Decisively, `full_grid_resync_acceptance` runs **before** the changed gate suite, so even a cross-suite leaked-state path is not available. The `luajit` leg passing on the same commit is **corroboration only** — a deterministic defect *can* be Lua-flavour-specific, so that observation must not be used as a structural exclusion |
|
||||
| **what IS established** | **no blank was OBSERVED after the mark** within the test's fixed 20-second deadline. The collected suffix was the **entire** post-mark output (`suffix.len()`, 25 362 bytes on this occurrence — not a capped window; only the *displayed* head is truncated to 400 bytes), and that head shows ordinary repaint traffic (`ZQXMARKERQZ` rows with SGR + CUP), so the host was painting |
|
||||
| **what is NOT** | any mechanism. Whether the blank was never emitted, emitted after the deadline, or lost in transport is **open** — and "it never emitted the blank" is a claim this evidence does not support. **The failing run's ~20 s duration is the fixed `Duration::from_secs(20)` timeout**, so the spread against a fast passing run is mechanically determined and is **not** independent timing evidence |
|
||||
| **discriminating control — ASYMMETRIC, and only one direction concludes** | the suffix is already complete, so "capture more bytes" is not the gap — arrival time is. Extending the deadline and recording whether `CLEAR_ALL` arrives, and at what offset: **if it arrives, "emitted late" is established.** **If it does not, that establishes only "not observed by the longer deadline"** — *not* "never emitted", because transport loss produces the same absence. Separating non-emission from transport loss needs **producer-side emission evidence** (did pmacs write the clear?) cross-checked against the collected stream; no deadline, however long, can do it alone |
|
||||
|
||||
### U5 — `ctrl_c_during_reconnect_sleep_yields_clean_exit`, macOS `lua54`, one occurrence
|
||||
|
||||
Surfaced on the **rerun** of PR #229's failed job — a *different*
|
||||
selector from U4, so by this file's matching rule it is a **new
|
||||
incident, not U4 occurring twice**.
|
||||
|
||||
| field | value |
|
||||
|---|---|
|
||||
| **selector** | `--test m5_8_acceptance ctrl_c_during_reconnect_sleep_yields_clean_exit` |
|
||||
| **job / flavor** | GitHub Actions, `Test (macos-latest / lua54)`, rerun attempt 2 |
|
||||
| **required fragments** | `Ctrl-C during reconnect sleep should produce a clean exit` · `ExitStatus { code: 1, signal: Some("Interrupt: 2") }` |
|
||||
| **NOT a fragment** | the `:LINE` suffix — occurrence-specific, moves with the file |
|
||||
| **status** | **one occurrence, unresolved** |
|
||||
| **what IS established** | Ctrl-C reached the process **as `SIGINT`** rather than as the raw-mode key event the test drives. That is all the exit status shows |
|
||||
| **what is NOT** | whether injection preceded raw mode, raw mode was lost, or something else. Three mechanisms remain open and this fragment separates none of them |
|
||||
| **exclusion strength — WEAKER than U4's, deliberately** | the changed `gate_script_acceptance` ran **earlier in the same job**, and it creates worktrees and directories. No leaked child or persistent signal-state mutation was observed, but "the diff touches no `src/`" is **not** the argument here that it is for U4, because cross-suite leaked state is a path reachability reasoning does not close |
|
||||
| **control 1 — CROSS-SUITE ATTRIBUTION, and asymmetric** | run `m5_8_acceptance` alone on macOS `lua54`, without the gate suite ahead of it. **A matching isolated RED proves the gate suite is not necessary** for the failure. **An isolated GREEN proves nothing beyond that run** — the failure is intermittent, so absence under one run is not evidence of dependence. It also does **not** discriminate among the three mechanisms in either direction |
|
||||
| **control 2 — mechanism** | observe **readiness and raw-mode state at the moment of injection**. Another isolated pass, however many times repeated, cannot separate "injected before raw mode" from "raw mode lost" from a third cause |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,336 @@
|
|||
# `scripts/gate --protocol` — the build its sweep depends on
|
||||
|
||||
**Status: revision 5. APPROVED and IMPLEMENTED at `49bc141`. The
|
||||
regression-witness gap found in review of that implementation is
|
||||
CLOSED at `677fd25` — Q#GR-5's criterion is now witnessed against the
|
||||
REAL emitter rather than around it.**
|
||||
|
||||
*(Revisions 1-3 read "Pre-implementation. Awaiting approval" while the
|
||||
ledger recorded this lane as approved and implemented — the exact
|
||||
contradiction class this project keeps correcting elsewhere.)*
|
||||
|
||||
**Revision 3 fixes a witness that could not fail.** Revision 2's
|
||||
`--self-test` plan put the failing step **last**, so an aborting runner
|
||||
and a continuing one produce identical output — the witness for
|
||||
Q#GR-2's "the suite keeps going" policy would have passed on a runner
|
||||
doing the opposite. A passing **sentinel after** the failure, asserted
|
||||
to have written its log, is what separates them. §7 also now pins the
|
||||
**exact** build command rather than only the step's name and position,
|
||||
since a `build-crdt` running plain `cargo build` would leave the gate
|
||||
just as unsound while looking repaired.
|
||||
|
||||
**Revision 2 takes three review findings.** The normative requirement
|
||||
goes **entirely** into handoff §3 rather than being split across §3 and
|
||||
§5 (§5, Q#GR-3). Q#GR-1's observation procedure is respecified on a
|
||||
**disposable** target with the binary's absence asserted before each
|
||||
run, rather than by deleting a file from a live worktree. And the
|
||||
build-attribution criterion, which revision 1 stated with **no way to
|
||||
observe it**, gets a witness — via a hardcoded synthetic plan, not the
|
||||
plan-file injection that would reintroduce this script's own
|
||||
`--acceptance` defect (Q#GR-5).
|
||||
|
||||
**A narrow lane, deliberately.** One missing step in one script, plus
|
||||
the boundary question that let it go missing. No feature work, no
|
||||
`src/`, no protocol.
|
||||
|
||||
---
|
||||
|
||||
## 1. The defect
|
||||
|
||||
`scripts/gate --protocol` adds the CRDT workspace sweep. That sweep has
|
||||
a documented precondition, and **the script does not run it**.
|
||||
|
||||
`docs/agent-handoff.md` §5 (`:532-535`):
|
||||
|
||||
> **The crdt sweep needs `cargo build --workspace` first**, or twelve
|
||||
> `gpu_invocation_acceptance` tests fail on a missing `pmacs-gpu`
|
||||
> binary. `cargo build --workspace --no-default-features --features
|
||||
> luajit,crdt` is the invocation that produces both binaries.
|
||||
|
||||
`scripts/gate`'s plan emitter (`:187-204`) goes
|
||||
`… → gpu → sweep → sweep-crdt → diff-check`, with **no build step
|
||||
anywhere**. Read from the source, not inferred from the failure.
|
||||
|
||||
**Observed, not theorised.** PR #228's first gate run failed step 09
|
||||
with twelve `gpu_invocation_acceptance::crdt::*` failures, all
|
||||
*"build pmacs-gpu before this acceptance suite"*, and `debug/pmacs-gpu`
|
||||
was absent from that worktree's target directory. Running the
|
||||
documented invocation and re-running the gate turned it green.
|
||||
|
||||
## 2. Why it was latent until now, which is the interesting part
|
||||
|
||||
**#225 caused this to become reachable, and #225 is also what makes it
|
||||
matter.**
|
||||
|
||||
Before #225 every worktree on this machine resolved to **one shared**
|
||||
`CARGO_TARGET_DIR`. That directory almost always already contained a
|
||||
`pmacs-gpu` binary from some earlier build, so the precondition was
|
||||
**satisfied by accident** on essentially every run. #225 gave each
|
||||
worktree its own target directory; a fresh one starts empty, and the
|
||||
omission becomes load-bearing on the very first `--protocol` run in a
|
||||
new worktree.
|
||||
|
||||
So this is not "a bug #225 introduced". It is a **pre-existing gap in
|
||||
the documented procedure that #225 stopped hiding** — and the reason it
|
||||
is urgent rather than tidy is that the failure mode is not a red gate.
|
||||
A red gate is fine; it stops you. The hazard is the *reverse*: a green
|
||||
`--protocol` run whose crdt sweep was decided by **what happened to be
|
||||
in the build directory** rather than by the diff. That is a gate that
|
||||
reports coverage it does not have, which is precisely what #225 exists
|
||||
to prevent.
|
||||
|
||||
## 3. The likely mechanism, marked as inference
|
||||
|
||||
The failing tests are namespaced `gpu_invocation_acceptance::crdt::*`,
|
||||
which suggests they are **feature-gated to `crdt`** and therefore
|
||||
compile and run only under the crdt sweep. That would explain why the
|
||||
default sweep passes on a tree with no `pmacs-gpu` binary at all — it
|
||||
never runs the tests that spawn it.
|
||||
|
||||
**This is inference from the test names and one observation, and it is
|
||||
not yet verified.** Q#GR-1 makes establishing it part of the work
|
||||
rather than an assumption the fix rests on.
|
||||
|
||||
## 4. The change
|
||||
|
||||
*My vote: **a named `build-crdt` gate step, emitted immediately before
|
||||
`sweep-crdt` and only under `--protocol`***, running the invocation
|
||||
handoff §5 names.
|
||||
|
||||
- **A named step, not a silent prelude.** It gets its own numbered log
|
||||
alongside the others and fails the suite under its own name.
|
||||
(It does **not** "appear in `--print-plan`" — that mode prints
|
||||
commands only. §7 says what witnesses the name.)
|
||||
- **Not folded into the `sweep-crdt` command.** `cargo build … && cargo
|
||||
test …` would make a *build* failure appear under the name `sweep-crdt`
|
||||
in the failure list — a wrong attribution in the one place the script
|
||||
exists to be trustworthy about. **Q#GR-5 is how that is witnessed**,
|
||||
which revision 1 asserted without supplying.
|
||||
- **Only under `--protocol`.** If §3's inference holds, the default
|
||||
sweep does not need it, and adding an unconditional workspace build
|
||||
to every gate run is a real cost paid for nothing.
|
||||
|
||||
## 5. The boundary question, which is the durable half
|
||||
|
||||
The script's own header says:
|
||||
|
||||
> `docs/agent-handoff.md` section 3 owns the REASONING for each of
|
||||
> these … `--print-plan` renders this without running anything, which
|
||||
> is what makes **drift from section 3** testable.
|
||||
|
||||
**The drift here is from §5, not §3** — and that is a coherent reason
|
||||
for the omission rather than mere oversight. `scripts/gate` was written
|
||||
against §3's gate policy; this precondition lives in §5's hazard
|
||||
register, which the script never claimed to encode.
|
||||
|
||||
So the durable fix is not only the missing line. It is deciding where a
|
||||
requirement like this belongs.
|
||||
|
||||
**The normative requirement moves ENTIRELY into §3.** It is gate policy
|
||||
— it decides whether a gate's result means anything — and §3 is already
|
||||
where such policy lives. §5 keeps the **incident and its signature**,
|
||||
which is history, not contract.
|
||||
|
||||
**The script header keeps citing §3 and only §3.** Revision 1 also
|
||||
proposed citing §5, which was wrong twice over: it splits one
|
||||
executable contract across two sections, and it weakens the single
|
||||
clean boundary the script has (*"§3 owns the reasoning"*) at the same
|
||||
time as Q#GR-4 declines to build any automated check for prose drift.
|
||||
A boundary that is neither enforced nor singular is not a boundary.
|
||||
One normative home, one citation. Q#GR-3.
|
||||
|
||||
## 6. Open questions
|
||||
|
||||
### Q#GR-1 — what exactly must be built, and does the default sweep need it too?
|
||||
|
||||
§5 names `cargo build --workspace --no-default-features --features
|
||||
luajit,crdt` and says it "produces both binaries". §3's inference says
|
||||
the default sweep is unaffected. **Neither is verified by this
|
||||
document.**
|
||||
|
||||
*Required before implementation, by observation rather than reading.*
|
||||
Revision 1 said "delete `pmacs-gpu` from a target directory", which is
|
||||
both unsafe and insufficient: it **mutates a durable worktree's build
|
||||
directory**, and removing one binary does not establish that the other
|
||||
artifacts and feature permutations are cold — a stale dependency graph
|
||||
can satisfy the run for reasons the experiment never sees.
|
||||
|
||||
**The procedure:**
|
||||
|
||||
1. A **disposable** target directory (a scratch `CARGO_TARGET_DIR`, or
|
||||
a throwaway worktree), never a live lane's. Nothing under
|
||||
`$HOME/build/pmacs-gate-targets/` belonging to a real branch is
|
||||
touched.
|
||||
2. **Assert `debug/pmacs-gpu` is ABSENT before each run**, as a
|
||||
recorded precondition rather than an assumption. A run whose
|
||||
starting state was not checked proves nothing about a cold tree.
|
||||
3. Run the **default** sweep alone. Record pass/fail and, if it fails,
|
||||
the failing test names.
|
||||
4. Reset to the same cold state, assert absence again, run the **crdt**
|
||||
sweep alone. Record the same.
|
||||
|
||||
Each sweep separately, so a result cannot be explained by the other
|
||||
having built the binary first — which is the exact accident (§2) that
|
||||
hid this defect for the entire life of the shared target directory.
|
||||
|
||||
If the default sweep also needs the binary, the step is unconditional
|
||||
and §4's "only under `--protocol`" is wrong.
|
||||
|
||||
**This is the one thing in this lane I would not accept on reasoning.**
|
||||
The whole defect is a precondition nobody checked; establishing its
|
||||
replacement by reading would repeat the error at one remove.
|
||||
|
||||
### Q#GR-2 — does a build failure fail the suite, or abort it?
|
||||
|
||||
*My vote: **fail like any other gate***, and let the remaining steps
|
||||
run. `--no-fail-fast` is the established posture of this suite, and a
|
||||
sweep that then fails for the missing binary produces a second,
|
||||
consistent signal rather than a mysterious absence.
|
||||
|
||||
The counter-argument is real: twelve downstream failures with a known
|
||||
cause is noise. But the script already prints per-gate logs and a
|
||||
`FAILED:` list, so the cause is named at the top, and suppressing
|
||||
downstream output is how a tool starts deciding what its user is
|
||||
allowed to see.
|
||||
|
||||
### Q#GR-3 — where does this requirement live?
|
||||
|
||||
See §5. **§3 gains it normatively; §5 keeps the incident; the script's
|
||||
header keeps citing §3 alone.** Revision 1 proposed citing both, which
|
||||
would have split one executable contract across two sections while
|
||||
Q#GR-4 declines to build any check for prose drift.
|
||||
|
||||
### Q#GR-5 — how is the attribution criterion witnessed at all? **(new in rev 2)**
|
||||
|
||||
Revision 1 asserted that a build failure must be attributed to
|
||||
`build-crdt` rather than `sweep-crdt`, and gave no way to observe it.
|
||||
That criterion was unwitnessable as written: `tests/gate_script_acceptance.rs`
|
||||
deliberately exercises only **no-gates** paths, so plan assertions can
|
||||
prove a step's name and its order and **nothing about runtime
|
||||
behaviour**.
|
||||
|
||||
**The obvious seam is a trap.** Making `PLAN_FILE` injectable — let a
|
||||
test hand the runner its own plan — would work, and it would turn the
|
||||
script into a general command executor via the `eval` at its runner
|
||||
loop. That is the **same class of defect this script's own review
|
||||
already caught in `--acceptance`**, which was fixed with a refusal at
|
||||
parse time. Reintroducing it one lane later, in the tool whose purpose
|
||||
is to be trustworthy, is not a trade worth making.
|
||||
|
||||
*My vote: **a `--self-test` mode running a HARDCODED synthetic plan***
|
||||
— **three** lines: a passing step, a failing one named `build-crdt`,
|
||||
and **a passing SENTINEL after it**.
|
||||
|
||||
**The third line is not padding, and revision 2's two-line plan was
|
||||
broken without it.** With the failure last, a runner that **aborts** on
|
||||
failure and one that **continues** produce identical output, so the
|
||||
witness passes either way — and Q#GR-2's whole answer is that the suite
|
||||
keeps going. A sentinel *after* the failing step, asserted to have run
|
||||
and written its log, is the only thing that distinguishes them.
|
||||
|
||||
So it asserts: the runner names the failing gate, lists it under
|
||||
`FAILED:`, writes its log where it says it does, exits non-zero, **and
|
||||
the sentinel after the failure has its own log** — which is Q#GR-2's
|
||||
policy made observable rather than declared.
|
||||
|
||||
- **No injection.** The synthetic plan is a literal inside the script;
|
||||
nothing external supplies a command.
|
||||
- **Runs no real gate**, so it stays on the cheap no-gates side of the
|
||||
existing suite. `true`/`false` are the whole workload.
|
||||
- **It tests the runner, which is the thing under test.** Whether
|
||||
`cargo build` really fails is `cargo`'s business; whether *this
|
||||
script names the right gate when a command fails* is the criterion,
|
||||
and it is orthogonal to which command failed.
|
||||
|
||||
The alternative is a **documented manual witness** — break the build by
|
||||
hand, run the gate, record the output in the lane. Honest, and it rots:
|
||||
nothing re-runs it, so it decays into a claim about a past machine.
|
||||
Named as the fallback if review rejects a new mode.
|
||||
|
||||
### Q#GR-4 — should `--print-plan` be asserted against the handoff?
|
||||
|
||||
Tempting and out of scope. A test that parses prose out of
|
||||
`agent-handoff.md` and compares it to the plan would be brittle in the
|
||||
direction that produces false confidence. **Not in this lane**, and
|
||||
named so it is not mistaken for an oversight.
|
||||
|
||||
## 7. Verification
|
||||
|
||||
- **`--print-plan --protocol` witnesses COMMAND and ORDER only** — it
|
||||
strips names before printing, so it can show that the build command
|
||||
appears immediately before the crdt sweep command and nothing about
|
||||
which step is called what. Earlier revisions of this section said
|
||||
named steps "appear in `--print-plan`"; they do not, and that wording
|
||||
is what let the attribution witness drift away from the step it
|
||||
names.
|
||||
- **BOTH real emitter pairs are asserted, explicitly and separately:**
|
||||
- `("build-crdt", "cargo build --workspace --no-default-features --features luajit,crdt")`
|
||||
- `("sweep-crdt", "cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright")`
|
||||
|
||||
Both, because the rename hole is symmetric: revision 4 closed it for
|
||||
the build and left the sweep able to be renamed just as invisibly.
|
||||
Asserted from the emitter, where the name still exists.
|
||||
|
||||
The **command** half matters as much as the name: a `build-crdt`
|
||||
running plain `cargo build` would leave the gate exactly as unsound
|
||||
while looking repaired, because the crdt sweep needs *those*
|
||||
features.
|
||||
- **`--print-plan` WITHOUT `--protocol` does not emit it** (subject to
|
||||
Q#GR-1 — if the default sweep turns out to need the binary too, this
|
||||
assertion inverts and §4 changes with it).
|
||||
- **A real fresh-target `--protocol` run goes green without a manual
|
||||
build**, which is the acceptance criterion and the thing that was
|
||||
false. Witnessed on a target directory with no `pmacs-gpu` in it.
|
||||
- **A failing gate is attributed to its own name**, witnessed through
|
||||
`--self-test`'s synthetic plan (Q#GR-5): the run exits non-zero,
|
||||
prints `build-crdt` as the failing step, lists it under `FAILED:`,
|
||||
and writes the log path it claims. This is the criterion revision 1
|
||||
stated with no way to observe it.
|
||||
- **THE REAL PLAN'S `(name, command)` PAIR IS ASSERTED** — added in
|
||||
revision 4, because the two witnesses above **do not connect to the
|
||||
step they describe**. `--print-plan` strips names before printing, so
|
||||
the order assertion sees only commands; `--self-test` hardcodes the
|
||||
string `build-crdt` in its own synthetic plan. Review demonstrated
|
||||
the gap: **renaming the real build step to `sweep-crdt` left both
|
||||
tests passing.** The plan's emitted name and its command must be
|
||||
asserted together, from the real emitter, so a rename cannot pass.
|
||||
The synthetic failure/continuation test stays — it tests the runner,
|
||||
which is a different thing — but it can no longer stand in for
|
||||
attribution of the actual step.
|
||||
|
||||
**Landed at `677fd25` as `--print-plan-named`**: a second *rendering*
|
||||
of the same `emit_plan`, printing the `name<TAB>command` text the
|
||||
runner reads back from `PLAN_FILE`, asserted by **whole-line
|
||||
equality** so the name and the command are pinned together.
|
||||
**`sweep-crdt`'s own pair is asserted too** — asserting only the
|
||||
build's name leaves the identical hole open in the other direction.
|
||||
`PLAN_FILE` stays uninjectable, and a companion test pins that
|
||||
`--print-plan` *is* that rendering minus its names, so the two cannot
|
||||
drift and the assertion cannot come to pin a name the runner never
|
||||
uses. **Mutation tested, each individually red:** the build renamed
|
||||
to `sweep-crdt` (**the one the previous round passed**), the sweep
|
||||
renamed, the build's features changed, the build emitted after the
|
||||
sweep.
|
||||
- **The suite CONTINUES past a failed gate** (Q#GR-2) — the sentinel
|
||||
step after `build-crdt` in the synthetic plan has its own log.
|
||||
**Revision 2's two-line plan could not assert this**: with the
|
||||
failure last, an aborting runner and a continuing one are
|
||||
indistinguishable, so the witness would have passed on a runner that
|
||||
does the opposite of the stated policy.
|
||||
- **The existing 15 `tests/gate_script_acceptance.rs` tests still
|
||||
pass**, and the new assertions join them on the **no-gates paths**
|
||||
(`--print-plan` runs nothing), keeping the suite cheap.
|
||||
|
||||
**What this will NOT prove:** that the plan matches the handoff in
|
||||
general (Q#GR-4), or that any other §5 hazard is encoded in the script
|
||||
— this lane fixes one and asks where such requirements belong, it does
|
||||
not audit §5.
|
||||
|
||||
## 8. Not in scope
|
||||
|
||||
Any feature work. Any `src/` change. Auditing the rest of handoff §5
|
||||
for further unencoded preconditions (worth doing; not here). A
|
||||
plan-versus-handoff consistency test (Q#GR-4). Changing which gates the
|
||||
suite runs, or the acceptance-suite selection policy — §3 remains
|
||||
authoritative for both. **Rerunning PR #228's gate**, which is that
|
||||
lane's unblocking step and happens after this lands, not inside it.
|
||||
157
scripts/gate
157
scripts/gate
|
|
@ -3,11 +3,15 @@
|
|||
# directory, with isolated ambient roots and durable logs.
|
||||
#
|
||||
# scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan]
|
||||
# scripts/gate [--acceptance SUITE]... [--protocol] --print-plan-named
|
||||
# scripts/gate --print-target-dir
|
||||
# scripts/gate --init
|
||||
# scripts/gate --prune [--force]
|
||||
# scripts/gate --self-test
|
||||
#
|
||||
# Framing: docs/gate-script-framing.md (revision 4, approved).
|
||||
# Framing: docs/gate-script-framing.md (revision 4, approved), and
|
||||
# docs/gate-protocol-build-framing.md (revision 5, approved) for the
|
||||
# crdt build step, --self-test and --print-plan-named.
|
||||
#
|
||||
# WHY A PER-WORKTREE TARGET DIRECTORY. This machine exports one
|
||||
# CARGO_TARGET_DIR for every checkout, and cargo takes an EXCLUSIVE LOCK
|
||||
|
|
@ -37,9 +41,11 @@ set -eu
|
|||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
usage: scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan]
|
||||
scripts/gate [--acceptance SUITE]... [--protocol] --print-plan-named
|
||||
scripts/gate --print-target-dir
|
||||
scripts/gate --init
|
||||
scripts/gate --prune [--force]
|
||||
scripts/gate --self-test
|
||||
|
||||
--acceptance SUITE a touched acceptance suite to run (repeatable).
|
||||
docs/agent-handoff.md section 3 stays authoritative
|
||||
|
|
@ -48,8 +54,15 @@ usage: scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan]
|
|||
working tree, and one that guessed would report
|
||||
coverage it does not have.
|
||||
--protocol the change touches PROTOCOL_VERSION; adds the CRDT
|
||||
workspace sweep on top of the default one.
|
||||
--print-plan print the exact gate commands and exit.
|
||||
workspace sweep on top of the default one, plus
|
||||
the build that sweep needs (see build-crdt below).
|
||||
--print-plan print the exact gate commands and exit. Names are
|
||||
stripped, so every line is runnable as printed.
|
||||
--print-plan-named print the plan as `name<TAB>command` lines and exit
|
||||
--- the same text the runner reads. Exists because
|
||||
a step's NAME is half its contract (a build failure
|
||||
must be attributed to build-crdt, not sweep-crdt)
|
||||
and --print-plan cannot show it.
|
||||
--print-target-dir print this worktree's build directory and exit.
|
||||
Creates nothing.
|
||||
--init create the build directory and ownership marker,
|
||||
|
|
@ -57,6 +70,13 @@ usage: scripts/gate [--acceptance SUITE]... [--protocol] [--print-plan]
|
|||
--prune list managed directories whose worktree is gone.
|
||||
Deletes NOTHING without --force.
|
||||
--force with --prune, actually delete.
|
||||
--self-test drive the real runner with a HARDCODED synthetic
|
||||
plan --- true, false, true --- to witness that a
|
||||
failing gate is named as ITSELF and that the suite
|
||||
CONTINUES past it. Runs no real gates. EXITS
|
||||
NON-ZERO BY DESIGN: the middle step fails on
|
||||
purpose, so a non-zero status is this mode
|
||||
working, not this mode broken.
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
|
@ -198,11 +218,80 @@ emit_plan() {
|
|||
if [ "$PROTOCOL" = 1 ]; then
|
||||
# Section 3: touching PROTOCOL_VERSION STRENGTHENS the sweep
|
||||
# line, it does not replace it. Both sweeps run.
|
||||
#
|
||||
# THE BUILD IS A PRECONDITION OF THE SWEEP, not a courtesy. The
|
||||
# crdt sweep spawns `pmacs-gpu` as a PROCESS, and no `cargo
|
||||
# test` run produces that binary: pmacs-gpu has no tests/
|
||||
# directory, so cargo never uplifts its bin to debug/pmacs-gpu.
|
||||
# On a cold target directory the sweep therefore fails twelve
|
||||
# gpu_invocation_acceptance::crdt::* tests on "build pmacs-gpu
|
||||
# before this acceptance suite" --- and, worse, other crdt tests
|
||||
# that render through the real binary SKIP THEMSELVES and report
|
||||
# ok, so the missing build also voids coverage silently.
|
||||
#
|
||||
# WHY ONLY UNDER --protocol, measured rather than reasoned. On
|
||||
# 2026-08-09, on a disposable target directory with
|
||||
# debug/pmacs-gpu asserted ABSENT before each run and each sweep
|
||||
# run alone from that cold state: the DEFAULT sweep exited 0 and
|
||||
# left debug/pmacs-gpu still absent --- it never builds the
|
||||
# binary and never needs it --- while the crdt sweep exited 101
|
||||
# with exactly those twelve failures. So the default gate does
|
||||
# not pay for this build.
|
||||
#
|
||||
# A SEPARATE NAMED STEP, never folded into the sweep command.
|
||||
# `cargo build ... && cargo test ...` would report a BUILD
|
||||
# failure under the name `sweep-crdt`, which is a wrong
|
||||
# attribution in the one place this script exists to be
|
||||
# trustworthy about. --self-test is what witnesses that the
|
||||
# runner names the failing gate as itself.
|
||||
printf 'build-crdt\tcargo build --workspace --no-default-features --features luajit,crdt\n'
|
||||
printf 'sweep-crdt\tcargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright\n'
|
||||
fi
|
||||
printf 'diff-check\tgit diff --check\n'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# The synthetic plan behind --self-test: the runner held to its own
|
||||
# contract.
|
||||
#
|
||||
# WHY A MODE EXISTS AT ALL. tests/gate_script_acceptance.rs drives only
|
||||
# NO-GATES paths --- a test that ran the real suite would run the gate
|
||||
# suite inside the gate suite --- so plan assertions can prove a step's
|
||||
# name and its order and NOTHING about what the runner does when a step
|
||||
# fails. That left two stated properties with no way to observe them:
|
||||
# a failing gate is attributed to ITSELF (which is the whole reason
|
||||
# build-crdt is a separate step rather than `cargo build && cargo
|
||||
# test`), and the suite CONTINUES past it rather than aborting. Same
|
||||
# shape of argument as --init: verification needs a path it can drive
|
||||
# safely, and this one is not a second implementation --- it hands the
|
||||
# REAL runner loop a different plan file.
|
||||
#
|
||||
# THE PLAN IS A LITERAL, and that is the design, not a shortcut. The
|
||||
# obvious seam --- letting a caller supply PLAN_FILE --- would work,
|
||||
# and it would turn the runner's `eval` into a general command
|
||||
# executor. That is the same class of defect this script's own review
|
||||
# caught in --acceptance and fixed with a refusal at parse time;
|
||||
# reintroducing it in the tool whose purpose is to be trustworthy is
|
||||
# not a trade worth making. Nothing external supplies a command here.
|
||||
#
|
||||
# THREE LINES, AND THE THIRD IS LOAD-BEARING. With the failure LAST, a
|
||||
# runner that aborts and a runner that continues produce IDENTICAL
|
||||
# output, so the witness would pass on a runner doing the opposite of
|
||||
# the stated policy. The sentinel after the failure, asserted to have
|
||||
# written its own log, is the only thing that separates them.
|
||||
#
|
||||
# `true` and `false` are the entire workload, so this stays on the
|
||||
# cheap side of the suite. Whether `cargo build` really fails is
|
||||
# cargo's business; whether THIS SCRIPT names the right gate when a
|
||||
# command fails is the criterion, and that is orthogonal to which
|
||||
# command failed.
|
||||
# ---------------------------------------------------------------------
|
||||
emit_self_test_plan() {
|
||||
printf 'self-pass\ttrue\n'
|
||||
printf 'build-crdt\tfalse\n'
|
||||
printf 'self-sentinel\ttrue\n'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Pruning.
|
||||
#
|
||||
|
|
@ -326,19 +415,65 @@ while [ $# -gt 0 ]; do
|
|||
shift 2 ;;
|
||||
--protocol) PROTOCOL=1; shift ;;
|
||||
--print-plan) MODE=plan; shift ;;
|
||||
--print-plan-named) MODE=plannamed; shift ;;
|
||||
--print-target-dir) MODE=printdir; shift ;;
|
||||
--init) MODE=init; shift ;;
|
||||
--prune) MODE=prune; shift ;;
|
||||
--self-test) MODE=selftest; shift ;;
|
||||
--force) FORCE=1; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "gate: unknown argument: $1" >&2; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Mode dispatch.
|
||||
#
|
||||
# TWO RENDERINGS OF ONE PLAN, and the second exists because the first
|
||||
# hid a defect. `--print-plan` pipes through `cut -f2-` so every printed
|
||||
# line is a command a reader can copy and run --- and that same cut is
|
||||
# why the emitted NAMES never reached a test. Not cosmetic: the entire
|
||||
# reason build-crdt is a separate step is that a build failure must be
|
||||
# attributed to `build-crdt` rather than to `sweep-crdt`, and with the
|
||||
# names stripped, RENAMING THE REAL BUILD STEP TO `sweep-crdt` left the
|
||||
# ordering assertion green. The witness could not reach the step it
|
||||
# named. --self-test could not either --- it hardcodes the string
|
||||
# `build-crdt` in its own synthetic plan, which proves things about the
|
||||
# RUNNER and nothing about this emitter.
|
||||
#
|
||||
# --print-plan-named prints emit_plan VERBATIM: the same
|
||||
# `name<TAB>command` text the runner reads back from PLAN_FILE, so a
|
||||
# test can assert both halves of a real step together and a rename
|
||||
# cannot pass.
|
||||
#
|
||||
# WHY THIS RATHER THAN THE TWO ALTERNATIVES.
|
||||
#
|
||||
# Injecting PLAN_FILE would let a test hand the runner a plan and read
|
||||
# the names back, and it would turn the runner's `eval` into a general
|
||||
# command executor --- the same class of defect this script's own
|
||||
# review caught in --acceptance and fixed with a parse-time refusal.
|
||||
# Declined there; declined here for the same reason.
|
||||
#
|
||||
# Re-deriving the plan test-side (sourcing this file, or parsing
|
||||
# emit_plan out of it) would be a SECOND implementation of the thing
|
||||
# under test, which is the exact failure being repaired one level up.
|
||||
#
|
||||
# A DISTINCT MODE, not a modifier on --print-plan: there is then no
|
||||
# `--with-names` without `--print-plan` whose behaviour has to be
|
||||
# defined, and --print-plan's contract --- runnable lines --- is left
|
||||
# exactly as it was. Both modes call emit_plan, and so does the runner,
|
||||
# so neither rendering can drift from what actually executes;
|
||||
# tests/gate_script_acceptance.rs pins that the stripped rendering is
|
||||
# the named one minus its names, so this stays true by test and not
|
||||
# only by reading.
|
||||
# ---------------------------------------------------------------------
|
||||
case $MODE in
|
||||
plan)
|
||||
emit_plan | cut -f2-
|
||||
exit 0 ;;
|
||||
plannamed)
|
||||
emit_plan
|
||||
exit 0 ;;
|
||||
printdir)
|
||||
target_dir_for "$(canon "$(worktree_root)")"
|
||||
echo
|
||||
|
|
@ -397,11 +532,23 @@ echo "gate: logs $LOGDIR"
|
|||
# trap removes it, so it should be gone once the run finishes.
|
||||
echo "gate: ambient $AMBIENT"
|
||||
[ -n "$ACCEPTANCE" ] && echo "gate: acceptance $ACCEPTANCE"
|
||||
[ "$PROTOCOL" = 1 ] && echo "gate: protocol yes (CRDT workspace sweep added)"
|
||||
[ "$PROTOCOL" = 1 ] && echo "gate: protocol yes (CRDT build + workspace sweep added)"
|
||||
if [ "$MODE" = selftest ]; then
|
||||
echo "gate: SELF-TEST hardcoded synthetic plan --- NO real gate runs."
|
||||
echo "gate: the middle step fails ON PURPOSE, so a non-zero"
|
||||
echo "gate: exit is this mode working, not this mode broken."
|
||||
fi
|
||||
echo
|
||||
|
||||
# The self-test hands the REAL runner loop below a different plan file.
|
||||
# Everything after this point is shared, which is the point: a witness
|
||||
# that exercised its own copy of the runner would witness nothing.
|
||||
PLAN_FILE="$LOGDIR/plan.txt"
|
||||
emit_plan > "$PLAN_FILE"
|
||||
if [ "$MODE" = selftest ]; then
|
||||
emit_self_test_plan > "$PLAN_FILE"
|
||||
else
|
||||
emit_plan > "$PLAN_FILE"
|
||||
fi
|
||||
|
||||
N=0
|
||||
FAILED=''
|
||||
|
|
|
|||
|
|
@ -127,6 +127,307 @@ fn the_crdt_workspace_sweep_is_added_by_protocol_and_absent_without_it() {
|
|||
);
|
||||
}
|
||||
|
||||
/// **The precondition the plan did not encode**, and the reason a green
|
||||
/// `--protocol` run could mean nothing.
|
||||
///
|
||||
/// The crdt workspace sweep spawns `pmacs-gpu` as a *process*, and no
|
||||
/// `cargo test` run produces that binary — `pmacs-gpu` has no `tests/`
|
||||
/// directory, so cargo never uplifts its bin to `debug/pmacs-gpu`. On a
|
||||
/// cold target directory the sweep fails twelve
|
||||
/// `gpu_invocation_acceptance::crdt::*` tests on *"build pmacs-gpu
|
||||
/// before this acceptance suite"*. Before per-worktree target
|
||||
/// directories (#225) every worktree shared one that nearly always
|
||||
/// already held the binary, so the precondition was satisfied **by
|
||||
/// accident** — and the hazard was never the red gate, it was a green
|
||||
/// one decided by the build directory rather than by the diff.
|
||||
///
|
||||
/// **The exact command is asserted, not just the step's name and
|
||||
/// position.** A `build-crdt` running plain `cargo build` would sit in
|
||||
/// the right place under the right name and leave the gate exactly as
|
||||
/// unsound: the crdt sweep needs *those* features, and the wrong ones
|
||||
/// produce a binary the sweep cannot use.
|
||||
///
|
||||
/// **What this test cannot see: the names.** `--print-plan` strips them
|
||||
/// (`emit_plan | cut -f2-`), so everything below is an assertion about
|
||||
/// *commands in an order* — renaming the real build step to `sweep-crdt`
|
||||
/// leaves it green. The step's **name** is asserted by
|
||||
/// `the_crdt_build_step_carries_its_own_name_and_its_exact_command`
|
||||
/// below, which reads the plan in the form the runner reads it.
|
||||
#[test]
|
||||
fn the_crdt_sweep_is_immediately_preceded_by_the_build_that_produces_its_binary() {
|
||||
let root = tempfile::tempdir().expect("tempdir");
|
||||
let build = "cargo build --workspace --no-default-features --features luajit,crdt";
|
||||
let crdt_sweep = "cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright";
|
||||
|
||||
let (plan, err, ok) = run(root.path(), &["--protocol", "--print-plan"]);
|
||||
assert!(ok, "--protocol --print-plan must succeed; stderr:\n{err}");
|
||||
|
||||
let b = plan
|
||||
.find(build)
|
||||
.unwrap_or_else(|| panic!("the crdt sweep's build is missing; plan was:\n{plan}"));
|
||||
let s = plan
|
||||
.find(crdt_sweep)
|
||||
.unwrap_or_else(|| panic!("the crdt sweep is missing; plan was:\n{plan}"));
|
||||
|
||||
// Ordering is asserted BEFORE the slice below, which would
|
||||
// otherwise panic with a byte-offset message ("begin > end (427 >
|
||||
// 282)") that names neither step. Mutation-tested: emitting the
|
||||
// build *after* the sweep produced exactly that, and a gate test
|
||||
// whose failure has to be decoded is a gate test nobody trusts.
|
||||
assert!(
|
||||
b < s,
|
||||
"the build must run BEFORE the crdt sweep, not after it — a sweep \
|
||||
that builds its own precondition afterwards has already failed; \
|
||||
plan was:\n{plan}"
|
||||
);
|
||||
// IMMEDIATELY before: one newline between them and nothing else. A
|
||||
// build that merely appears *somewhere* earlier could be separated
|
||||
// from the sweep by a step that rewrites the same target directory.
|
||||
assert_eq!(
|
||||
&plan[b + build.len()..s],
|
||||
"\n",
|
||||
"the build must run IMMEDIATELY before the crdt sweep; plan was:\n{plan}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The witness that reaches the step it names**, and the reason this
|
||||
/// lane needed a second round.
|
||||
///
|
||||
/// This lane exists to guarantee two things: that the crdt sweep is
|
||||
/// preceded by the build producing its binary, and that a build failure
|
||||
/// is attributed to **`build-crdt`** rather than to `sweep-crdt`. The
|
||||
/// first round shipped with neither guaranteed, because **neither
|
||||
/// witness could see a name**:
|
||||
///
|
||||
/// - `--print-plan` renders `emit_plan | cut -f2-`, so the ordering test
|
||||
/// above compares commands and never sees the names beside them.
|
||||
/// - `--self-test` hardcodes the string `build-crdt` inside its **own
|
||||
/// synthetic** plan, so it proves things about the *runner* and
|
||||
/// nothing about the real emitter.
|
||||
///
|
||||
/// Review demonstrated the consequence directly: **renaming the real
|
||||
/// build step to `sweep-crdt` left both tests passing** — a plan that
|
||||
/// reports a build failure under the sweep's name, which is exactly the
|
||||
/// misattribution the separate step exists to prevent, sitting green.
|
||||
///
|
||||
/// So the pair is asserted **together, as one emitted line**, against
|
||||
/// `--print-plan-named` — the plan in the form the runner reads it back
|
||||
/// from `PLAN_FILE`. Name and command in the same `assert`, from the
|
||||
/// real emitter, is what makes a rename unable to pass; either half
|
||||
/// alone lets the other drift.
|
||||
///
|
||||
/// The mode is a *rendering*, not a seam: `PLAN_FILE` stays
|
||||
/// uninjectable, because a test that supplied the runner's plan would
|
||||
/// turn its `eval` into a general command executor — the defect the
|
||||
/// `--acceptance` refusal below exists to prevent.
|
||||
#[test]
|
||||
fn the_crdt_build_step_carries_its_own_name_and_its_exact_command() {
|
||||
let root = tempfile::tempdir().expect("tempdir");
|
||||
let build = "build-crdt\tcargo build --workspace --no-default-features --features luajit,crdt";
|
||||
let sweep =
|
||||
"sweep-crdt\tcargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright";
|
||||
|
||||
let (plan, err, ok) = run(root.path(), &["--protocol", "--print-plan-named"]);
|
||||
assert!(
|
||||
ok,
|
||||
"--protocol --print-plan-named must succeed; stderr:\n{err}"
|
||||
);
|
||||
let lines: Vec<&str> = plan.lines().collect();
|
||||
|
||||
// Whole-line equality, not `contains`: the name, the tab, and the
|
||||
// command with nothing appended. A step is its (name, command) pair
|
||||
// and the plan is where both are decided.
|
||||
let b = lines.iter().position(|l| *l == build).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"no plan line is exactly:\n {build}\nA build step under a \
|
||||
different NAME misattributes its own failure; a build step \
|
||||
with different FEATURES hands the sweep a binary it cannot \
|
||||
use. Plan was:\n{plan}"
|
||||
)
|
||||
});
|
||||
// The sweep's own pair, for the same reason in the other direction:
|
||||
// asserting only the build's name lets a rename of the SWEEP slip
|
||||
// through the identical hole.
|
||||
let s = lines
|
||||
.iter()
|
||||
.position(|l| *l == sweep)
|
||||
.unwrap_or_else(|| panic!("no plan line is exactly:\n {sweep}\nPlan was:\n{plan}"));
|
||||
|
||||
assert_eq!(
|
||||
s,
|
||||
b + 1,
|
||||
"the build must be the step IMMEDIATELY before the crdt sweep — a \
|
||||
build merely somewhere earlier could be separated from it by a \
|
||||
step that rewrites the same target directory. Plan was:\n{plan}"
|
||||
);
|
||||
|
||||
// Conditionality, on this rendering too: an ordinary lane must not
|
||||
// carry the step at all, not merely not carry its command.
|
||||
let (default_plan, _, ok) = run(root.path(), &["--print-plan-named"]);
|
||||
assert!(ok, "--print-plan-named must succeed");
|
||||
assert!(
|
||||
!default_plan.contains("build-crdt"),
|
||||
"the default sweep never builds pmacs-gpu and never needs it, so no \
|
||||
ordinary lane may pay for a workspace build; plan was:\n{default_plan}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The new rendering must be the same plan, or the assertion above
|
||||
/// pins a string only the test ever reads.**
|
||||
///
|
||||
/// `--print-plan-named` and `--print-plan` both call one emitter, and
|
||||
/// the runner writes that same emitter to `PLAN_FILE` — so today they
|
||||
/// cannot disagree. This pins that from outside, where a later edit
|
||||
/// giving either mode its own plan text would be caught rather than
|
||||
/// producing a witness that asserts a name the runner never uses.
|
||||
///
|
||||
/// It also pins the **shape** the runner depends on: the loop reads each
|
||||
/// line with `IFS=<tab> read -r name cmd`, so a plan line without its
|
||||
/// tab would silently run under an empty command.
|
||||
#[test]
|
||||
fn the_named_plan_is_the_printed_plan_with_its_names_removed() {
|
||||
let root = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
for flags in [
|
||||
vec![],
|
||||
vec!["--protocol"],
|
||||
vec!["--acceptance", "m4_acceptance"],
|
||||
] {
|
||||
let mut named_args = flags.clone();
|
||||
named_args.push("--print-plan-named");
|
||||
let mut plain_args = flags.clone();
|
||||
plain_args.push("--print-plan");
|
||||
|
||||
let (named, err, ok_named) = run(root.path(), &named_args);
|
||||
assert!(ok_named, "{named_args:?} must succeed; stderr:\n{err}");
|
||||
let (plain, err, ok_plain) = run(root.path(), &plain_args);
|
||||
assert!(ok_plain, "{plain_args:?} must succeed; stderr:\n{err}");
|
||||
|
||||
let mut stripped = String::new();
|
||||
for l in named.lines() {
|
||||
let (_name, cmd) = l.split_once('\t').unwrap_or_else(|| {
|
||||
panic!(
|
||||
"every plan line must be `name<TAB>command` — the runner \
|
||||
splits on that tab, so a line without one runs an empty \
|
||||
command under the whole line's name. Line was:\n {l:?}"
|
||||
)
|
||||
});
|
||||
stripped.push_str(cmd);
|
||||
stripped.push('\n');
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
stripped, plain,
|
||||
"the two renderings must be one plan; with {flags:?} they diverged"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// **Conditionality, settled by measurement rather than by reading** —
|
||||
/// which is the whole methodological point of this lane, since the
|
||||
/// defect it repairs was a precondition nobody checked.
|
||||
///
|
||||
/// Measured 2026-08-09 on a disposable target directory, with
|
||||
/// `debug/pmacs-gpu` asserted **absent** before each run and each sweep
|
||||
/// run alone from that same cold state: the default sweep exited **0**
|
||||
/// and left `debug/pmacs-gpu` **still absent** — it never builds the
|
||||
/// binary and never needs it — while the crdt sweep exited **101** with
|
||||
/// exactly twelve `gpu_invocation_acceptance::crdt::*` failures.
|
||||
///
|
||||
/// So an unconditional build would be a real cost paid for nothing on
|
||||
/// every ordinary lane.
|
||||
#[test]
|
||||
fn the_crdt_build_is_absent_without_protocol() {
|
||||
let root = tempfile::tempdir().expect("tempdir");
|
||||
let (plan, _, ok) = run(root.path(), &["--print-plan"]);
|
||||
assert!(ok, "--print-plan must succeed");
|
||||
assert!(
|
||||
!plan.contains("cargo build"),
|
||||
"the default sweep passes on a tree with no pmacs-gpu at all, so a \
|
||||
normal lane must not pay for a workspace build; plan was:\n{plan}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **The attribution and continuation criteria, made observable.**
|
||||
///
|
||||
/// Everything else in this file drives a no-gates path, so it can prove
|
||||
/// a step's name and its order and **nothing** about what the runner
|
||||
/// does when a step fails. `--self-test` closes that gap by handing the
|
||||
/// *real* runner loop a hardcoded three-line plan — a passing step, a
|
||||
/// failing one named `build-crdt`, and a passing sentinel after it.
|
||||
///
|
||||
/// **Why `build-crdt` must be its own step** is exactly what this
|
||||
/// witnesses: folded into the sweep as `cargo build … && cargo test …`,
|
||||
/// a *build* failure would be reported under the name `sweep-crdt` — a
|
||||
/// wrong attribution in the one place this script exists to be
|
||||
/// trustworthy about.
|
||||
///
|
||||
/// **The sentinel assertion is the load-bearing one.** With the failure
|
||||
/// last, a runner that aborts and one that continues produce identical
|
||||
/// output, so a two-line witness would pass on a runner doing the
|
||||
/// opposite of the stated `--no-fail-fast` policy. The sentinel's own
|
||||
/// log existing is the only thing that separates them — delete that
|
||||
/// assertion and this test stops testing continuation at all.
|
||||
///
|
||||
/// The plan is a literal inside the script on purpose. Making
|
||||
/// `PLAN_FILE` injectable would let this test supply its own commands,
|
||||
/// and would turn the runner's `eval` into a general command executor —
|
||||
/// the same defect the `--acceptance` refusal above exists to prevent.
|
||||
#[test]
|
||||
fn self_test_names_the_failing_gate_and_the_suite_continues_past_it() {
|
||||
let root = tempfile::tempdir().expect("tempdir");
|
||||
let (out, err, ok) = run(root.path(), &["--self-test"]);
|
||||
|
||||
assert!(
|
||||
!ok,
|
||||
"a plan containing a failing step must exit non-zero; stdout:\n{out}stderr:\n{err}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("build-crdt"),
|
||||
"the failing gate must be named as it runs; stdout:\n{out}"
|
||||
);
|
||||
assert!(
|
||||
err.contains("FAILED: build-crdt"),
|
||||
"the failing gate must be listed under FAILED: by its OWN name; stderr:\n{err}"
|
||||
);
|
||||
|
||||
// The runner claims a log path for the failure. Assert the file is
|
||||
// actually there: a tool that prints a path it did not write is
|
||||
// worse than one that prints nothing, because the absence is only
|
||||
// discovered while chasing a real failure.
|
||||
let claimed = err
|
||||
.lines()
|
||||
.find_map(|l| l.split_once("log: ").map(|(_, path)| path.trim()))
|
||||
.unwrap_or_else(|| panic!("the failing gate's log path must be printed; stderr:\n{err}"));
|
||||
assert!(
|
||||
claimed.ends_with("02-build-crdt.log"),
|
||||
"the log must be numbered and named for the gate that failed; was {claimed}"
|
||||
);
|
||||
assert!(
|
||||
Path::new(claimed).is_file(),
|
||||
"the runner must WRITE the log it claims at {claimed}"
|
||||
);
|
||||
|
||||
let logdir = Path::new(claimed)
|
||||
.parent()
|
||||
.expect("the log lives in a log directory");
|
||||
assert!(
|
||||
logdir.join("01-self-pass.log").is_file(),
|
||||
"the step before the failure must have its own log; dir was {}",
|
||||
logdir.display()
|
||||
);
|
||||
// THE ASSERTION THE WHOLE MODE EXISTS FOR.
|
||||
assert!(
|
||||
logdir.join("03-self-sentinel.log").is_file(),
|
||||
"the suite must CONTINUE past a failed gate — the sentinel after \
|
||||
build-crdt wrote no log, so this runner ABORTED. Stdout:\n{out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("self-sentinel"),
|
||||
"the sentinel must be reported like any other gate; stdout:\n{out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The seam handoff §3 keeps authority over: a script cannot infer
|
||||
/// which acceptance suites a change touched, so it runs what it is
|
||||
/// handed — each one, in order.
|
||||
|
|
|
|||
Loading…
Reference in New Issue