Skip to content

Commit 625db0e

Browse files
os-elon-muskclaude
andauthored
docs(audits): the verdict/exit-code audit of check:merge-driver's three self-tests — all three immune (#18896)
Fixes #18166 Clause-②: no This is an **audit** card, and the audit's answer is that **all three self-tests chained in `check:merge-driver` are immune** to the verdict/exit-code split. Nothing was repaired, because nothing is broken. No script in the population changed; the diff is one audit record under `docs/audits/`. Written by session `session_019srGWGCBBCBHqcDoRZpQRh`. ## 1. The population, re-derived from the script `package.json:19` on this branch, read rather than copied from the card, which said its own list is a hint: ``` "check:merge-driver": "node scripts/git-env.mjs --self-test && node scripts/git-merge-regen.mjs --self-test && node scripts/check-regen-pending.mjs --self-test" ``` Exactly three files, in this order — `scripts/git-env.mjs`, `scripts/git-merge-regen.mjs`, `scripts/check-regen-pending.mjs`. The re-derivation agrees with the card's hint. They are chained with `&&`, which is the whole reason the audit matters: a zero from any one of them **is** the gate passing. ## 2. The gate's own exit code, before and after | run | tree | exit | |:--|:--|--:| | `pnpm check:merge-driver` **before** | `0b31d90fb` (the branch point) | **0** | | `pnpm check:merge-driver` **after** | `e18f4d2a7` (this head) | **0** | Unchanged, and it has to be: no file the gate runs was edited. ## 3. The reading — exit code, never output Per file: is there any path on which the dispatch prints a failing verdict, or counts a failure, while the process still exits 0? Thirteen ablations, one mutation each, landed and verified **on disk** by `scripts/ablation-replace.mjs` (anchor hit exactly once, occurrence counts and blob hash before and after), then restored from `HEAD` with the restore proven by `blob == HEAD` and an empty `git diff HEAD`. No editor of the `-i` family was used and no mutation was left in the tree. | # | file | the mutation | reading | direction | |:--|:--|:--|--:|:--| | A1 | `git-env.mjs` | one registered case reports failure | **1** | fires | | A2 | `git-env.mjs` | failing verdict line kept, exit decoupled from the count | **0** | discriminates | | A3 | `git-env.mjs` | case floor raised, so the floor summand fires | **1** | fires | | A4 | `git-env.mjs` | `selfTest()` returns a value that is not the verdict | **1** | fires | | B1 | `git-merge-regen.mjs` | `hookIsExecutable` reports failure by `return false` **alone** — no `fail()`, and it still prints its own tick | **1** | fires | | B2 | `git-merge-regen.mjs` | a failure counted **and** PR #18165's `process.exitCode` line removed | **0** | discriminates | | B3 | `git-merge-regen.mjs` | same forced count, that line left in place | **1** | fires | | B4 | `git-merge-regen.mjs` | a `registerCase()` deleted, so the floor breaches through `fail()` | **1** | fires | | C1 | `check-regen-pending.mjs` | `decisionTableSelfTest` returns false while all of its own rows print a tick | **1** | fires | | C2 | `check-regen-pending.mjs` | a failure counted, verdict line still printed, exit decoupled | **0** | discriminates | | C3 | `check-regen-pending.mjs` | same forced count, exit branch intact | **1** | fires | | C4 | `check-regen-pending.mjs` | the inline `noDist` assertion forced false | **1** | fires | | C5 | `check-regen-pending.mjs` | a `registerCase()` deleted, so the floor summand fires | **1** | fires | **Ten firing controls, and they fire.** Every exit sink of every file is covered: git-env has two sinks and three failure signals (A1/A3/A4), git-merge-regen has two (B1/B3 for the count, B4 for `fail()`), check-regen-pending has one sink fed by three summands (C1/C4/C5). The record lists each sink with its line number. **Three discrimination controls, in the other direction.** Ten non-zero readings on their own are also what an instrument that cannot see exit 0 produces. So each file additionally carries the same forced failure with the count-to-exit coupling removed, and all three printed a failing verdict and exited **0** — the card's defect, reproduced on each of the three files, by this exact reading: ```text A2 git-env --self-test: 1 failure(s) [ABLATED: exit decoupled from the count] exit 0 B2 merge driver wiring is inconsistent - 1 failure(s) (cases and floor); see above. exit 0 C2 self-test failed -- 1 failure(s) [ABLATED: exit decoupled] exit 0 ``` B3/C3 pair with B2/C2 on the same forced count and differ only in whether the coupling is present, which isolates the coupling as the thing that makes the difference. B2 in particular measures that PR #18165's repair line is load-bearing rather than decorative. ## 4. The judgement the card left open The card raises one shared verdict/exit helper against three copies of `if (failures > 0)`, expressly as a judgement on measurements. The measurement puts the repair population at **zero**, so neither route earns itself: rewriting the verdict site of three working instruments would add risk with no defect to remove, and `check:merge-driver` is itself the gate that would have to judge that rewrite. **No gate weakened, no exemption added** — acceptance clause 2 holds trivially, since no file the gate reads was touched. ## 5. Gates Derived from the actual changed path, never from a named list: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, re-derived after the `origin/main` merge so the stale-tree warning cleared. **17 derived families, 17 run, all exit 0, 0 NOT MEASURED** — reconciled with `--ran`, which confirms the zero is derived from recorded exit codes rather than claimed: > dispatch-gates --ran: 17 derived famil(ies) accounted for — 17 run, 0 NOT-MEASURED (a DERIVED zero — all 17 recorded an exit code and none of them is 3). Every exit code was captured by redirecting first and reading `$?` second, never after a pipe. One family needed a second pass: `check:doc-formula-expressions` first answered **3**, its own `PREREQUISITE NOT MET` code ("Nothing was measured"), because `@objectstack/formula` and `@objectstack/lint` were unbuilt in a fresh worktree; after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` it answered **0**. That 3 is recorded as not-a-finding, not as a failed measurement. `check:issue-citations --base origin/main` was also run defensively, since the diff adds four issue citations: exit 0, and it read **0 files** — `docs/audits/` is outside its declared surface, so that is a not-applicable rather than a pass. **Repo-wide eslint, narrowed — and the narrowing is measured, not assumed.** Taken at `e18f4d2a7`, this branch's final commit: 1. **Population, read from eslint's own config:** every config block in `eslint.config.mjs` scopes its `files` glob to `ts,tsx,mts,cts,js,jsx,mjs,cjs`. No block names a Markdown extension. 2. **Count, from `--format json`:** 1 result for the changed path, carrying exactly one message — `File ignored because no matching configuration was supplied.` Zero rules were evaluated on it. 3. **Invariance for untouched files:** `eslint.config.mjs` states, with its own positive control, that this repo "never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file". A one-file Markdown diff therefore cannot move the verdict of any file it does not contain. So the narrowing excludes nothing: the only changed path is not in the examined population at all. CI still runs the full farm. ## Acceptance notes Observations from the audit, noted and deliberately **not** filed — none is a reproducible defect, a declared-contract violation, or a metadata-authoring trap: - `check-regen-pending.mjs`'s two inline dispatch assertions (`noDist`, `noTree`) sit outside the battery roster. That is a **declared** gap, stated at the roster with its cost and its reason, and this audit measures that both feed the same exit sink (C4). Not a finding. Successor: the reshape card the roster comment already points at. - `fixtureSelfTest()` reduces with `results.every(Boolean)`, which is `true` over an empty array. It cannot be reached empty — every path that would skip the pushes throws out of a `try` with no `catch`, which exits non-zero — so it is not this card's class either. Noted for whoever next edits that function. Successor: none. - `main()` in `check-regen-pending.mjs` is the production `pre-commit` / `pre-push` path and is outside `check:merge-driver`, which runs `--self-test` only. Its own verdict/exit coupling was read statically (its stale printers increment the same `blocked` count that `decide()` turns into the returned status) and deliberately **not** ablated. Successor: none. - Linux only. macOS was not measured; #16717 records `check:merge-driver` staying red there for unrelated reasons, and #13799 is not addressed here. ## 维护者速读(草稿) **改了什么** —— 一个文件:`docs/audits/2026-09-merge-driver-verdict-exit-split.md`。三个被审计的 脚本一个字节都没动。 **为什么改** —— 卡面要的是一次测量,不是一次修复:`check:merge-driver` 用 `&&` 串起三个 `--self-test`,任何一个「打印失败判决却退出 0」就让整条电池悄悄变成 advisory。测量结果是三个都 免疫,所以本 PR 的产物是那份可复核的审计记录本身,而不是代码改动。留在 GitHub 评论里的读数下一 个人复核不到,所以它落进仓库。 **风险与代价(含回滚)** —— 风险接近零:无代码改动、无门禁改动、无豁免、不发布任何包 (`skip-changeset`)。回滚 = 删掉这一个文件。唯一的真实代价是 161 行仓内文档的维护面。 **席位意见** —— **你要做的** —— 判断这份审计记录该不该留在 `docs/audits/`。如果认为「审计结论写进卡面评论就够 了」,直接关掉本 PR 即可,卡面报告评论已独立承载同一组读数;如果认为该留,照常合并。另:卡面 的验收第 1、3 条(每个文件带点火对照 + 重新推导 population)由这份记录与本正文同时满足。 --- _Generated by [Claude Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 156792e commit 625db0e

1 file changed

Lines changed: 161 additions & 0 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Audit: the verdict/exit-code split in the self-tests `check:merge-driver` chains
2+
3+
**Date**: 2026-09-18 · **Tree**: `objectstack-ai/objectstack` at
4+
`0b31d90fb37d490d7da12c7acd4b14c4feb22501` · **Card**: #18166 · **Instrument**:
5+
`scripts/ablation-replace.mjs`, unmodified, driving `node scripts/<file>.mjs --self-test` and
6+
reading the child's **exit code** — the line it prints as `command exited N`.
7+
8+
**Answer: all three chained files are immune. Nothing was repaired, because nothing is broken.**
9+
Ten firing controls fire and three discrimination controls reproduce the defect shape on the same
10+
three files, so the immunity reading discriminates rather than merely failing to find anything.
11+
12+
## The population, re-derived from the script
13+
14+
`package.json:19` — not copied from the card, which said its own list is a hint:
15+
16+
```json
17+
"check:merge-driver": "node scripts/git-env.mjs --self-test && node scripts/git-merge-regen.mjs --self-test && node scripts/check-regen-pending.mjs --self-test",
18+
```
19+
20+
Exactly three files, in this order:
21+
22+
1. `scripts/git-env.mjs`
23+
2. `scripts/git-merge-regen.mjs`
24+
3. `scripts/check-regen-pending.mjs`
25+
26+
The re-derivation agrees with the card's hint. It is chained with `&&`, which is why the audit
27+
matters at all: a zero from any one of them **is** the gate passing, so a file in the split makes
28+
the whole battery advisory without saying so.
29+
30+
## The question, and why the exit code is the only reading
31+
32+
Per file: **is there any path on which the dispatch prints a failing verdict, or counts a failure,
33+
while the process still exits 0?**
34+
35+
The output cannot answer it. That is the whole shape of the defect PR #18165 repaired in
36+
`scripts/git-merge-regen.mjs`: the run printed `✗ merge driver wiring is inconsistent — N
37+
failure(s)` and exited 0, because the code was set only inside `fail()` while the verdict line was
38+
printed from a count of returned booleans. A reading taken from the text would have called that
39+
run red. Only the exit code distinguishes it, and only the exit code is what `&&` consults.
40+
41+
## Baseline, before any mutation
42+
43+
| run | exit |
44+
|:--|--:|
45+
| `pnpm check:merge-driver` | **0** |
46+
| `node scripts/git-env.mjs --self-test` | **0** |
47+
| `node scripts/git-merge-regen.mjs --self-test` | **0** |
48+
| `node scripts/check-regen-pending.mjs --self-test` | **0** |
49+
50+
## The exit sinks, enumerated per file
51+
52+
Immunity is a property of every sink, not of one, so each was ablated separately.
53+
54+
### `scripts/git-env.mjs` — two sinks, three failure signals
55+
56+
| sink | line | what feeds it | ablated as |
57+
|:--|--:|:--|:--|
58+
| `if (failures) { print ❌; process.exit(1) }` | 444–446 | the `t()` case registrar (`failures += 1` at 286) | **A1** |
59+
| the same sink | 444–446 | the case floor `SELF_TEST_CASE_FLOOR` (`failures += 1` at 442) | **A3** |
60+
| the dispatch's handshake refusal `process.exit(1)` | 462 | `selfTest()` returning anything but `SELF_TEST_VERDICT` (449) | **A4** |
61+
62+
The file has **0** `process.exitCode` sites. On a site count alone that reads as the defect; it is
63+
not. `t()` prints the per-case line and increments the count in the same statement pair, and the
64+
verdict line at 445 and the `process.exit(1)` at 446 are inside one `if (failures)` — one count,
65+
one verdict, one code.
66+
67+
### `scripts/git-merge-regen.mjs` — two sinks (repaired by PR #18165)
68+
69+
| sink | line | what feeds it | ablated as |
70+
|:--|--:|:--|:--|
71+
| `fail()` → `process.exitCode = 1` | 624 | every callee's failure path; the floor-breach loop at 2010 | **B4** |
72+
| `if (failures > 0) process.exitCode = 1;` | 2019 | the count of returned booleans at 2012 — the same count the verdict line is printed from | **B1**, **B3** |
73+
74+
Line 2019 is PR #18165's repair, and **B2** measures that it is load-bearing rather than
75+
decorative: removing it restores exit 0 under a forced failure.
76+
77+
### `scripts/check-regen-pending.mjs` — one sink, three summands
78+
79+
| sink | line | what feeds it | ablated as |
80+
|:--|--:|:--|:--|
81+
| `if (failures > 0) { print ✗; process.exit(1) }` | 1859–1862 | the four declared callees' returned booleans | **C1** |
82+
| the same sink | 1859–1862 | the two assertions written **inline** in the dispatch (`noDist` 1810, `noTree` 1812), which the battery roster deliberately does not cover | **C4** |
83+
| the same sink | 1859–1862 | `floorBreaches` from `batteryFloorFailures()` (1850) | **C5** |
84+
85+
Six `process.exit(` spellings appear in the file; only three are executable statements
86+
(1862, 1865, 1867) and one of those is inside a fixture string. The per-case printer at 1263 is
87+
`check(label, cond)`, which pushes into `results` **and** prints from the same `cond` — so a
88+
callee reporting failure by `return false` alone is counted, not merely printed.
89+
90+
## The matrix — 13 ablations, one mutation each
91+
92+
Every row: the mutation is landed and verified **on disk** (anchor hit exactly once, occurrence
93+
counts and blob hash before/after), the dispatch is run, the child's exit code is read, and the
94+
file is restored from `HEAD` with the restore proven by `blob == HEAD` and an empty
95+
`git diff HEAD`. No `-i`-family editor was used, and no mutation was left in the tree.
96+
97+
| # | file | the mutation | reading | expected |
98+
|:--|:--|:--|--:|:--|
99+
| **A1** | `git-env.mjs` | one registered case reports failure | **1** | non-zero — fires |
100+
| **A2** | `git-env.mjs` | the failing verdict line kept, the exit decoupled from the count | **0** | zero — discriminates |
101+
| **A3** | `git-env.mjs` | case floor raised to 9999, so the floor summand fires | **1** | non-zero — fires |
102+
| **A4** | `git-env.mjs` | `selfTest()` returns a value that is not the verdict | **1** | non-zero — fires |
103+
| **B1** | `git-merge-regen.mjs` | `hookIsExecutable` reports failure by `return false` **alone** — no `fail()`, and it still prints its own `✓` | **1** | non-zero — fires |
104+
| **B2** | `git-merge-regen.mjs` | a failure counted **and** the 2019 repair line removed | **0** | zero — discriminates |
105+
| **B3** | `git-merge-regen.mjs` | the same forced count, 2019 left in place | **1** | non-zero — fires |
106+
| **B4** | `git-merge-regen.mjs` | `registerCase('hookIsExecutable')` deleted, so the floor breaches through `fail()` | **1** | non-zero — fires |
107+
| **C1** | `check-regen-pending.mjs` | `decisionTableSelfTest` returns false while every one of its own rows prints `✓` | **1** | non-zero — fires |
108+
| **C2** | `check-regen-pending.mjs` | a failure counted, the failing verdict line still printed, the exit decoupled | **0** | zero — discriminates |
109+
| **C3** | `check-regen-pending.mjs` | the same forced count, the exit branch intact | **1** | non-zero — fires |
110+
| **C4** | `check-regen-pending.mjs` | the inline `noDist` assertion forced false | **1** | non-zero — fires |
111+
| **C5** | `check-regen-pending.mjs` | `registerCase('decisionTableSelfTest')` deleted, so the floor summand fires | **1** | non-zero — fires |
112+
113+
B1 and C1 each read **2** failures rather than 1: both files' ambient-git-env battery re-enters the
114+
whole `--self-test` in a child, and the mutated child fails too. That inflates the count, never the
115+
colour, and the colour is the reading.
116+
117+
## Why the firing controls alone would not have settled it
118+
119+
"Exited non-zero" and "this harness cannot see an exit 0" produce the same ten rows. So each file
120+
also carries a control in the **other** direction — the same forced failure with the count-to-exit
121+
coupling removed — and all three printed a failing verdict and exited **0**:
122+
123+
```text
124+
A2 ❌ git-env --self-test: 1 failure(s) [ABLATED: exit decoupled from the count] exit 0
125+
B2 ✗ merge driver wiring is inconsistent — 1 failure(s) (cases and floor); see above. exit 0
126+
C2 ✗ self-test failed -- 1 failure(s) [ABLATED: exit decoupled] exit 0
127+
```
128+
129+
That is the card's defect, reproduced on each of the three files by this exact reading. So the ten
130+
non-zero readings are an immunity measurement and not an instrument that cannot fail. B3/C3 pair
131+
with B2/C2 on the same forced count and differ only in whether the coupling is present, which
132+
isolates the coupling as the thing that makes the difference.
133+
134+
## What was deliberately not changed
135+
136+
- **No gate weakened and no exemption added.** Nothing in the three scripts changed at all. The
137+
card's acceptance holds trivially: there is no change that could turn a previously-red run green.
138+
- **No shared verdict/exit helper.** The card raises one shared helper against three copies of
139+
`if (failures > 0)`, expressly as a judgement on measurements. The measurement says **zero** files
140+
need repair, so neither route earns itself: a refactor rewriting the verdict site of three working
141+
instruments would add risk with no defect to remove, and `check:merge-driver` is the gate that
142+
judges its own repair.
143+
- **No permanent test file.** The ablation harness is throwaway by construction; this record is the
144+
durable artifact.
145+
146+
## Boundaries — what this audit does not say
147+
148+
- It is **not** the assertion-floor class. "A battery that never ran is indistinguishable from one
149+
that passed" is a different property, measured elsewhere
150+
(`docs/audits/2026-09-self-test-shape-census.md`, `scripts/measure-self-test-floor.mjs`).
151+
#13799 is not addressed here. A3/C5/B4 touch a floor only because each file's floor is a summand
152+
of the very count that sets the exit code, which is inside this card's question.
153+
- It is **not** the handshake class either, except where a handshake *is* one of the exit sinks —
154+
`git-env.mjs` 462, ablated as A4. The other two files have no `selfTest()` function to hand back
155+
a verdict: their dispatch block **is** the verdict site.
156+
- `main()` — the production `pre-commit` / `pre-push` path in `check-regen-pending.mjs` — is
157+
outside this population: `check:merge-driver` runs `--self-test` only. Its `✗` printers at 951
158+
and 957 increment the same `blocked` count that `decide()` turns into the returned exit status,
159+
read statically here and **not** ablated.
160+
- **Linux only.** macOS was not measured. #16717 records `check:merge-driver` staying red there for
161+
unrelated reasons.

0 commit comments

Comments
 (0)