Repository navigation
Commit 625db0e
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
0 commit comments