Repository navigation
Commit 7c7e76f
Fixes #18074
`scripts/pm/dispatch-gates.mjs --ran` had two channels for declaring a
gate family
unmeasured, and one silently ate the other. A recorded `exit 124` — the
number a
`timeout` wrapper reports when it had to signal the child — classified
as **run**,
and so did every code at or above the signal floor. A record that also
carried an
explicit `NOT-MEASURED ... :: reason` for the same command had that
declaration
discarded without a word. The most careful record produced the least
truthful total.
A kill is not a verdict. The process was ended before it produced a
result, so
counting the family as `run` asserts a measurement that does not exist —
the false
green the whole reconciliation exists to refuse.
## What changed — `scripts/pm/dispatch-gates.mjs` only
**One named constant, judged in one place.** `RUN_RECORD_KILL_EXITS`
holds the
timeout wrapper's `124` and the `128` signal floor, with a docblock
saying why each
member is in it; `runRecordKillLabel()` is the only expression that
compares a code
against it, so no reconciliation branch and no rendered line carries a
bare number of
its own. `124` is the member the set exists for: it sits *below* the
floor, so a
floor test alone never catches it.
**A killed family lands in one of two places, never `run`:**
- **NOT-MEASURED** when the record also carries a claim with a stated
reason for that
command. The claim **wins** over the run line beside it — the runner who
recorded
the code *and* declared the family has said everything there is to say,
and the two
lines do not actually disagree. It gets its own source, its own block
(`NOT-MEASURED · KILLED`) and a row naming both the code and the reason.
- **UNRUN** otherwise — the direction that costs a rerun. The row names
the recorded
code and prescribes the spelling that would declare it.
**It is counted once.** A killed-and-declared family arrives through two
doors and is
one family. It is already inside `coded`, so `killClaimed` is reported
beside the
other evidence counts and added to none of them; `accounted` stays equal
to the number
of families the record accounts for.
**A verdict plus a claim is still a genuine contradiction** — `0`, `1`,
`2` and every
other code below the floor — run still wins, unchanged, and the
contradiction line
still prints. The conflicts rendering now reads the class the
reconciliation actually
assigned instead of re-deciding it, so it can no longer report a reading
the totals
above it do not share.
## Premise readings, taken before writing
**P1 — reading D reproduces.** 2026-09-14T02:11Z, worktree at
`a26a114d7`, the card's
own reproduction (`--repo objectstack-ai/objectstack
packages/spec/src/system/metrics.zod.ts`,
73 derived families on this tree), exits captured by
redirect-then-capture:
```
Run reconciliation — 73 derived, 1 run, 0 NOT-MEASURED, 72 UNRUN.
```
The seat's open question, measured on the **full** output rather than
the card's
`reconciliation`-only filter: **the contradiction line does print.** It
is line 77 of
stdout, and it reads
```
⚠️ 'node packages/lint/scripts/check-reference-carrier-shape.mjs' is recorded BOTH as run and as NOT-MEASURED. Read as run; fix the record so it states one thing.
```
So `conflicts` was both computed and rendered; the defect was never that
the tool
failed to notice, it was which bucket the family landed in. Since the
line already
printed, it is pinned rather than repaired — by a self-test that reads
the **rendered
text**, not the `conflicts` array.
**P2 — a bare `exit 124` classified as run.** Same run, reading A:
`73 derived, 1 run, 0 NOT-MEASURED, 72 UNRUN` — identical to D. Reading
B (`exit 3`)
and reading C (the claim alone) both read `0 run, 1 NOT-MEASURED` on the
same tree,
which is what makes A and D readings of one instrument rather than four
instruments.
**P3 — the module argues the opposite way round.** Its own
`parseRunRecord` docblock:
> a marked line without one leaves its family UNRUN, which is the
direction that costs
> a rerun instead of a false green.
**P4 — hold #14290, declared rider, nothing folded.** 2026-09-14T02:20Z:
#14290 is
open, `domain:devx`, `priority:p3`; its hold comment `5556473243`
carries
`Restart-touch: scripts/pm/dispatch-gates.mjs` and
`Restart-when: closed #16132`, which has
fired. Its subject
is the STAGE-THEN-RUN edge on the **derivation** side —
`check:objectui-changeset`
inheriting no watch hints from `scripts/bump-objectui.sh`, an edge
neither follow
traverses. This PR touches only the `--ran` **record** reconciliation
and its
rendering: it adds no follow, reads no program text and changes no hint
extraction, so
it does not meet that edge and stops short of it. The hold's own
re-pricing stays in
the `domain:devx` lane. Riders #12797 and #12808 are closed.
## Before / after, same command, same derivation
| record | before | after |
|:--|:--|:--|
| **A** `CMD :: exit 124` | `73 derived, 1 run, 0 NOT-MEASURED, 72
UNRUN` | `73 derived, 0 run, 0 NOT-MEASURED, 73 UNRUN` |
| **B** `CMD :: exit 3` | `0 run, 1 NOT-MEASURED, 72 UNRUN` | unchanged
|
| **C** `NOT-MEASURED CMD :: reason` | `0 run, 1 NOT-MEASURED, 72 UNRUN`
| unchanged |
| **D** both A and C | `1 run, 0 NOT-MEASURED, 72 UNRUN` | `0 run, 1
NOT-MEASURED, 72 UNRUN` |
| **E** `CMD :: exit 0` + a claim | `1 run, 0 NOT-MEASURED, 72 UNRUN` |
unchanged |
Reading D's family row and its conflict line, after:
```
NOT-MEASURED · KILLED (1) — your record carries BOTH a kill code and a stated reason for these, so the run line beside it is NOT read as run: a kill is not a verdict. ...
- node packages/lint/scripts/check-reference-carrier-shape.mjs [recorded exit 124 on line 1 — a `timeout` wrapper fired and signalled the child, so no verdict was reached; declared on line 2: cap-killed at the foreground ceiling]
⚠️ 'node packages/lint/scripts/check-reference-carrier-shape.mjs' is recorded BOTH as run and as NOT-MEASURED. The two AGREE: exit 124 is a kill, not a verdict. Read as NOT-MEASURED with your stated reason — ⭐ this is the record shape to keep, not one to repair.
```
Reading A's, after:
```
⛔ UNRUN (73) — derived for these paths, and the record does not account for them:
- node packages/lint/scripts/check-reference-carrier-shape.mjs [recorded exit 124 on line 1 — a `timeout` wrapper fired and signalled the child, so no verdict was reached; declare it as `NOT-MEASURED CMD :: reason` beside the code to count it NOT-MEASURED]
```
## Tests
`node scripts/pm/dispatch-gates.mjs --self-test` — **before: `1682 cases
pass`**,
after: see the verification section below. Every existing case is kept.
The one
re-spelling is the both-ways case: its expectation is byte-for-byte what
it always
asserted, and the sentence now says which run lines it covers, because a
**bare** run
line carries no code for a kill to be read out of — it is the control
beside the new
branch, not a casualty of it.
New pins: reading A (bare kill → UNRUN, and the row names the code),
reading D (kill +
reasoned claim → NOT-MEASURED, and the row carries both halves), the
double-count
control on `accounted`, every member of the kill set driven both ways
round
(`124`, `130`, `137`, `141`, `143`, `128`, `149`), the verdict controls
(`0`, `1`, `2`
and `127` — unusual-looking, below the floor, and still run), a kill
code beside an
**unreasoned** claim staying UNRUN, the rendered contradiction line for
the
verdict-plus-claim case, and readings B and C unchanged.
## Verification
All readings below are from real output on this branch head `dda4d903e`
(`origin/main` merged in at `ca7886047`), exits captured by
redirect-then-capture.
**Self-test, before and after.** `node scripts/pm/dispatch-gates.mjs
--self-test`
```
before: ✓ dispatch-gates self-test: 1682 cases pass.
after: ✓ dispatch-gates self-test: 1712 cases pass. (exit 0)
```
**Falsifiability, one-shot.** With the classification reverted on disk —
the single call
site `const killed = runRecordKillLabel(recorded.code);` replaced by
`const killed = null;`,
which restores the pre-fix fall-through exactly — **16 of the new cases
fail BY NAME** and
every control stays green:
```
✗ READING A — a bare cap-kill code is UNRUN, never run: the run left no verdict to read
✗ ...and the unrun row NAMES the recorded code, so the runner can see what this tool read
✗ READING D — a kill code PLUS a reasoned claim is NOT-MEASURED: the honest runner's declaration WINS ...
✗ ...and its row carries BOTH halves of the record — the code this tool read and the reason the runner stated
✗ ...and the evidence counts that ONE family once: it is inside `coded`, and `killClaimed` is reported beside ...
✗ a recorded exit 124 / 130 / 137 / 141 / 143 / 128 / 149 is a kill: UNRUN bare, NOT-MEASURED when declared (7 cases)
✗ a kill code beside an UNREASONED claim stays UNRUN — the claim costs a reason here exactly as it does alone
✗ the NOT-MEASURED · KILLED block prints, and names the code and the reason on the family's own row
✗ ...and reading D is reported as the two channels AGREEING — ⛔ never as a record to repair
✗ reading A names its kill in the UNRUN block and prescribes the spelling that would declare it
```
Readings B and C, the verdict controls (`0`, `1`, `2`, `127`) and the
rendered
verdict-plus-claim contradiction line all stay ✓ under the ablation,
which is what says
the new branch sits **beside** the two existing channels rather than
over either of them.
The mutation was proven on disk before the run (injected text present
once, deleted text
absent, and the blob hash moved: `224b8a91` → `adf94009`), and the
restore was proven by
bytes and not by an exit code — `git hash-object` back to `224b8a91`,
`git diff HEAD`
empty, `git status --porcelain` empty. The script carried
`trap restore EXIT INT TERM` throughout, since a cap kill landing
mid-mutation is the
exact failure this card is about.
**Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
derives **31 families** for this diff on `dda4d903e` — the tool deriving
its own families,
since the changed path is the tool. All 31 were run with each exit
captured by
redirect-then-capture, recorded as `CMD :: exit CODE`, and handed back
through `--ran`,
so the tool judged its own record on the same run that exercises the new
classification:
```
Run reconciliation — 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.
EXIT CODES — all 31 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them. ⛔ This tool ran none of them; it read the codes you recorded.
✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED (a DERIVED zero — all 31 recorded an exit code and none of them is 3).
```
Every family exited **0**, `check:pm-dispatch-gates` included — the
family that was
cap-killed at the container ceiling on the round that surfaced this
card, and the reason
that round's ledger read `81 run` over a gate that never reached a
verdict.
`npx eslint --no-inline-config scripts/pm/dispatch-gates.mjs` — exit 0,
no output. The
repo-level `pnpm lint` scan is CI's run, not this PR's, and is not
claimed here.
The changed file is self-scanned for control bytes
(`grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'`): none, and
`check:nul-bytes` is
among the 31 green families.
## Acceptance notes
- **Rider #14290, not folded.** See P4. `scripts/pm/dispatch-gates.mjs`
is that hold's
`Restart-touch:` file, so this PR names it; the change does not meet the
STAGE-THEN-RUN edge it describes and stops short of it. Its re-pricing
belongs to
`domain:devx`.
- **`skip-changeset` is the declaration.** The `changeset-check` job in
`.github/workflows/pr-automation.yml` counts added `.changeset/*.md`
files with
`git diff --name-only --diff-filter=A MERGE_BASE HEAD --
'.changeset/*.md'` and has
**no path exemption** — its only two exemptions are the live
`skip-changeset` label
and the changesets release branch. `scripts/pm/dispatch-gates.mjs` sits
at the repo
root, whose package is `private: true`, and no released package's
`files[]` names
`scripts/pm`, so this diff publishes nothing.
- **Noted, not filed** — the evidence block's "a DERIVED zero" sentence
is reachable
only from the ✓ line, so it can never be printed over a killed family;
the comment
now says so, but the coupling between that sentence and the caller that
reaches it
is implicit rather than asserted. Observation, not a defect. Carrier:
the next PR to
touch `runRecordEvidenceLines` / `notMeasuredEvidenceTerm`.
Clause-②: no
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent c2cce8f commit 7c7e76f
1 file changed
Lines changed: 329 additions & 15 deletions
0 commit comments