Repository navigation
test(merge-driver): pin the os-regen deferral refusal on the linked-worktree path, and make the self-test exit non-zero when it fails - #18165
Merged
Conversation
… path `git-merge-regen.mjs` ends every deferral by printing "The pre-commit hook will not let this commit through until you do." Nothing tested that sentence: `endToEnd()` asserts the marker was written and never commits, and `check-regen-pending.mjs`'s fixture sets `core.hooksPath=/dev/null` and writes the marker by hand, so it grades the checker's verdict rather than git's willingness to invoke it. The joint property therefore had no coverage, and the one arrangement in which it can break is the one this repo mandates: a linked worktree. The driver's `markPending()` and the checker's `gitDirPath()` are two independent `--absolute-git-dir` call sites; in a primary checkout that directory and `--git-common-dir` are the same directory, so a regression to the common dir stays green in every single-tree fixture while every real merge loses its refusal. `endToEndWorktreeRefusal()` builds a throwaway repo with a linked worktree, reaches the REAL `.githooks` and the REAL checker through untracked symlinks (so the relative `core.hooksPath` resolves per worktree, as registered), merges a routed path inside the linked worktree and asserts: the marker lands in the linked git dir and not the common one; git's own pre-commit and pre-push refuse; and — the firing control — with the gate clean the same commit and push go through and the marker is gone. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
…verdict The `--self-test` dispatch computed `failures` from the booleans its callees return, printed "✗ merge driver wiring is inconsistent — N failure(s)" from that count, and then exited 0: the only thing that ever set a non-zero code was `fail()`, which every callee happened to call. The two agreed by convention, not by construction. `pnpm check:merge-driver` chains this file with `&&`, so the first callee to report a failure by returning `false` alone would have made the whole battery advisory — a failed gate reading as a pass, which is the shape this file exists to prevent. Measured on the ablation for the case landed alongside this: with `markPending()` regressed to the shared common git dir, the run printed nine failing legs and "✗ … 1 failure(s)" and exited 0. The verdict line and the exit code now come from the same count, and the new linked-worktree case reports through `fail()` like every other callee. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 14, 2026
This was referenced Sep 14, 2026
This was referenced Sep 14, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…ee self-tests — all three immune (objectstack-ai#18896) Fixes objectstack-ai#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 objectstack-ai#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 objectstack-ai#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; objectstack-ai#16717 records `check:merge-driver` staying red there for unrelated reasons, and objectstack-ai#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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #17602
PREMISE P was measured first, and it HOLDS — so the driver is not the defect
The card asks whether a zero exit from
merge=os-regencan be trusted. Thedomain:devxseat's mechanism reading (comment
5631161285) narrowed that to one falsifiable premise:is
content/docs/references/**really "generated whole", as.gitattributes:84declares?If yes, the unconditional deferral is the design and
markPending+ regeneration closesthe loop; if no, the row is MIXED without a
mixedfield and the deferral is a silentdeletion — and the defect would be in the declaration.
Measured on
origin/main394ccf549, with the post-driver state reproduced exactly (theartifact missing main's side while the source still carries it), the generator re-run, and
both controls lit:
The note is not hand-written: it is a
.describe()atpackages/spec/src/ui/view.zod.ts:917,rendered into the page by
gen:docs. The firing control proves the generator rewrites thisfile wholesale rather than skipping or appending — the injected marker is gone. Regeneration
reproduces the page byte for byte.
⇒
.gitattributes:84is true for this family.scripts/git-merge-regen.mjsbehaved exactlyas its own comment says it should, and ⛔ nothing in this PR changes the driver's merge
behaviour, weakens a gate, adds an exemption, or makes the driver prefer a side in a new case.
So the work is ask 2 — and the safety net DOES hold, but nothing proved it
The remaining question is the one the ruling names: on the path this incident took — a
human or agent hand-repairing a merge in a linked worktree — does the refusal actually
fire? Measured live, in this worktree, with a real merge of a real routed path:
The net holds. What is unguarded is that no test says so. The driver ends every deferral
by printing "The pre-commit hook will not let this commit through until you do", and the two
cases that come closest each stop one step short in the same direction:
endToEnd()performs a real merge, asserts the marker exists, and never commits — so gitnever runs a hook.
check-regen-pending.mjs's deferred-merge fixture setscore.hooksPath=/dev/null("thefixture drives the script itself") and writes the marker by hand — it grades the checker's
verdict, never git's willingness to invoke it, and never the path the driver wrote to.
The joint property had no coverage at all, and the one arrangement in which it can break is
the one AGENTS.md Prime Directive #11 makes mandatory.
markPending()in the driver andgitDirPath()in the checker are two independent--absolute-git-dircall sites; in aprimary checkout that directory and
--git-common-dirare the same directory, so aregression to the common dir keeps every single-tree fixture green while every real merge in
every agent's worktree silently loses its refusal.
What landed
1.
endToEndWorktreeRefusal()— a sixteenth self-test battery. It builds a throwawayrepo with a linked worktree, reaches the REAL
.githooksand the REAL checker throughuntracked symlinks (so the relative
core.hooksPathresolves per worktree, assetup-git-hooks.mjsregisters it — a hook this file wrote out would test an imitation),merges a routed path inside the linked worktree, and asserts nine legs:
pre-commitpre-push, via its os-regen legLegs 7-9 are what legs 1-6 rest on: a hook that can only fail — a missing interpreter, a
typo'd path, node erroring — satisfies every refusal assertion while proving nothing, and
from the outside that reads exactly like "the net holds".
2. The self-test's exit code now agrees with its own verdict. Found by the ablation below,
not by reading. The
--self-testdispatch computedfailuresfrom the booleans its calleesreturn and printed
✗ merge driver wiring is inconsistent — N failure(s)from that count —then exited 0. The only thing that ever set a non-zero code was
fail(), which everycallee happened to call. The two agreed by convention, not by construction, and
pnpm check:merge-driverchains this file with&&: the first callee to report a failure byreturning
falsealone would have made the whole battery advisory. The verdict line and theexit code now come from the same count.
This is an in-place fix under the bounded exemption — same defect class as the card (a failed
check reporting as a pass), mechanical, on a file this PR already declares, in the same gate
family, adding no new verification surface.
Ablation — the battery can fail, and the existing cases cannot catch what it catches
markPending()regressed to the shared common git dir (the exact regression class the caseexists for), run from the committed state, mutation proven on disk before the run and the
restore proven byte-identical after:
Three pre-existing end-to-end cases stay green through a regression that disarms the refusal
for every worktree in the container. That asymmetry is the case's whole justification.
Which of the three unreliable signals this makes trustworthy
Triage
5628590957records three cards describing one apparatus whose every signal isunreliable, and asks which one a change repairs. This PR repairs the driver's exit code
being the only evidence the merge was safe (#17602): after it, the claim "the hook refuses
until regeneration has run" is a test rather than a sentence, on the path real merges take.
It also, incidentally, moves #13799's signal for this one member: the
os-regen-mergeself-test (
.github/workflows/lint.yml) is named in #13799's population, and fix 2 means thatbattery can no longer report a failure at exit 0. ⛔ That is one member, ⛔ not #13799.
It does not touch #16717 —
check:merge-driverremains red on macOS for therunner-missing classifier's reasons, so a green local run of this battery is not evidence
there. The new case is platform-neutral (git, symlinks,
pnpm -son a stub), but it inherits#16717's blind spot like every other case in the file.
Effect on #16612 (shares this file, serialized behind this card)
Neither easier nor harder — but it is now instrumented. #16612 proposes retiring the only
mixedrow, which would dropsystem-context.mdxonto the unconditional-deferral branch thiscard measured. Nothing here changes that branch. What changes is that whoever takes #16612 now
has a case that proves the deferral's refusal actually fires end to end, so the question
"is the safety net behind the deferral real?" is answerable by running
pnpm check:merge-driverinstead of by argument. ⛔ Not fixed in passing; ⛔ not re-priced here.
Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived32 commands from the real diff (1 path,
scripts/git-merge-regen.mjs, vs merge base394ccf549). All 32 run and exit 0, reconciled with--ran. Exit codes landed to disk percommand.
pnpm check:merge-driver— the gate that owns this battery — is among them.pnpm lint's scan was not narrowed and needs no narrowing argument: the whole set ran at3d8e4c63e—eslint . --no-inline-config --format jsonover 6752 files, 0 errors,0 warnings. (The config declares it never enables type-aware linting for any file, so a
one-file diff could not have moved another file's verdict in any case.)
changeset:
skip-changeset, measured rather than assumed. The diff is one rootscripts/**file; the root package isprivate: true; all 70 published packages declarefiles[]and none can reach rootscripts/; the new symbol occurs in zero published packagesources, against a lit positive control (
defineStackhits inpackages/spec/src).Acceptance notes
SELF_TEST_BATTERY_FLOORmoved 15 to 16 and the roster/dispatch gained the new callee, asthat mechanism requires. The callee-count prose in the same block moved with it; one number
there ("the other thirteen") was already off by one against a roster of fifteen and is now
correct at fifteen against sixteen. Mechanical, in the block being edited.
check-regen-pending.mjs's deferred-merge fixture still setscore.hooksPath=/dev/nulland writes its marker by hand. That is deliberate and correct forwhat it grades (the checker's verdict machine), and the new case covers the half it cannot;
a second end-to-end fixture there would duplicate it. Successor: whoever next widens that
fixture's scenario set.
--self-testprints its verdict and, before this PR, derivedits exit code elsewhere. The sibling self-tests chained beside it in
check:merge-driver(
git-env.mjs,check-regen-pending.mjs) were not audited for the same shape — out of scopehere, and neither is known to have it. Successor: none currently queued.
Generated by Claude Code