Skip to content

fix(devx): declare check:merge-driver's manifest population, and re-measure the bounded-subtree walk specimen - #15538

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-15501-merge-driver-derivation
Sep 4, 2026
Merged

fix(devx): declare check:merge-driver's manifest population, and re-measure the bounded-subtree walk specimen#15538
baozhoutao merged 2 commits into
mainfrom
claude/issue-15501-merge-driver-derivation

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #15501
Fixes #15510

Two folded cards, both on scripts/pm/dispatch-gates.mjs. Branched from 615fac3a0; current origin/main merged in, so every reading below is from the tree at the head this PR carries.

#15501check:merge-driver could not be routed to a card that ADDS a generator

git-merge-regen --self-test refuses a generator with no recorded merge disposition, and the population that refusal sweeps is the MANIFESTS: the root package.json plus every workspace member's, read for their gen: / check: script rows (reconcileGenerators, which builds [{ dir: '.' }, ...workspacePackages(REPO_ROOT)]). What the family declared to the derivation was the artifact paths scripts/regen-artifacts.mjs carries, imported one level down — the generators ALREADY routed. A card that adds a generator touches a manifest and a new scripts/*.mjs, and neither is in that population until the card lands the very row it is being asked to add. So the one class of change the refusal exists to catch was the one class the derivation never named, and the gate fired a cycle late, in CI.

The repair

The population declaration, in the idiom the derivation already supports, beside the read it describes — never a rule inside dispatch-gates.mjs, and never a hand list of generator names (that ledger is regen-artifacts.mjs's, and this gate already reconciles it in both directions):

  • ROOT_FILE_WATCH_HINTS = ['package.json/**'] in scripts/git-merge-regen.mjs — the root manifest in the subtree spelling, because hintCovers refuses a bare single-segment literal as too generic and the glob suffix collapses back to the filename and matches it exactly. Same idiom as check-workspace-manifest-cycles.mjs's pnpm-workspace.yaml/** and check-turbo-task-graph.mjs's turbo.json/**.
  • DECLARED_WATCH_HINTS = ['packages/**/package.json', 'apps/**/package.json', 'examples/**/package.json'] — the member manifests as full-path glob literals. Each carries its glob in a NON-FINAL segment, so hintCovers judges it as a PATTERN reaching manifests only, never the thousands of files under those roots (checked directly: packages/**/package.json covers packages/spec/package.json and packages/adapters/hono/package.json, and does NOT cover packages/spec/src/index.ts or the root package.json).

A twelfth self-test callee in that file, reconcileManifestPopulation, holds both declarations against the enumerator's LIVE answer in both directions — every member manifest covered, no declared pattern covering zero, no member pattern reaching the root, and the root literal's collapsed form equal to manifestFor('.'), the path the gate really opens for the root owner. Its roster entry and floor came with it (SELF_TEST_BATTERY_FLOOR 11 to 12, registerCase as the first statement), since nothing else in this repo can redden when a declaration another tool reads goes stale.

Two self-test cases per direction in dispatch-gates.mjs, pinned against the LIVE tree through the same discovery pass derive runs, with the probe table pinned to its own length so an emptied table cannot run zero cases and read like a pass. No existing case was edited.

Measurements

(a) The derivation, before and after, on a package.json change set

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -- package.json

BEFORE, at 615fac3a0: 9 commands, 0 lines naming merge-driver.
AFTER: 10 commands. The full diff of the two command lists is one line:

5a6
> pnpm check:merge-driver

An owner manifest routes too — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -- packages/plugins/plugin-auth/package.json lists pnpm check:merge-driver; a brand-new unrelated scripts/*.mjs does not (0 lines).

Note the delta is exactly ONE family. check:generated and check:docs import check-regen-pending.mjs and did not gain the manifests: the hint-import widening is one level, and these declarations live in git-merge-regen.mjs.

(b) The false-positive cost, over every currently OPEN PR

29 open PRs, file lists enumerated via REST, the derivation run per change set, before and after:

PRs deriving check:merge-driver
before 11 of 29
after 12 of 29

Exactly one PR newly derives the gate: #15392, which edits the root package.json. That is 1 of 29, 3.4% — far below the third that would have made the manifest-wide hint the wrong trade and sent this back as needs_decision. Only 3 open PRs touch any manifest at all (#15492, #15392, #15334); the other two already derived the gate through the artifact population.

Price when it does fire, measured on this (shared, contended) box: node scripts/git-merge-regen.mjs --self-test at 1.92 s / 1.94 s / 1.96 s over three runs, and the whole pnpm check:merge-driver — both self-tests — at 6.5 s.

(c) The two population gates, before and after

pnpm check:declared-population-live — unchanged, and that is the honest reading: this family already declared a population that reached the tree, so the count cannot move.

before  ✓ check:declared-population-live — 207 of 254 famil(ies) declare a path population, and every one of them reaches this tree's 7522 tracked file(s).
after   ✓ check:declared-population-live — 207 of 254 famil(ies) declare a path population, and every one of them reaches this tree's 7522 tracked file(s).

pnpm check:watch-hint-literal — two declarations added, on two rostered names:

before  ✓ check-watch-hint-literal: 52 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 33, ROOT_FILE_WATCH_HINTS 9, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 7 -- ...
after   ✓ check-watch-hint-literal: 54 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 33, ROOT_FILE_WATCH_HINTS 10, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- ...

pnpm check:pm-dispatch-gates — the case count RISES by the added cases and no existing case was touched: 1402 to 1415 (+13).

The ablation, both legs

Reword the root declaration back to the bare 'package.json' the pin exists to catch, and the new callee reds; restore, and it is green again. The mutation was proved on disk by grepping for the injected and the deleted text (1 / 0) and by git hash-object differing from the HEAD blob; the restore was proved by git hash-object equal to the HEAD blob AND git diff HEAD empty. Absolute paths and an EXIT INT TERM trap throughout.

HEAD blob: 3aad5b89b65902e5732fbabc6ed484fa3f2092b3
mutation on disk: injected-text count=1  original-text count=0
mutated blob: d55fb10acff07cf655f14f215b6b75ea2ad66036 (differs from HEAD)
mutated run exit=1
✗ watch-hint declaration(s) out of step with the manifest population:
  the root manifest is declared in the SUBTREE spelling (hintCovers refuses the bare filename as too generic)
  every declared entry carries a path separator
✗ merge driver wiring is inconsistent — 1 failure(s) (cases and floor); see above.
restored blob: 3aad5b89b65902e5732fbabc6ed484fa3f2092b3
restore proved: hash == HEAD blob AND `git diff HEAD` empty
restored run exit=0

#15510 — the bounded-subtree-walk specimen, re-measured

Two sentences cited check-self-test-workflow-commands.mjs as the LIVE specimen for repoRootWalkSpelling's negative direction.

The outcome sentences are re-verified and stand. Running repoRootWalkSpelling over the six candidate gates on this tree: five are selected, that gate is selected by none, so the census of six is still a census of five.

One outcome detail was stale and is corrected, measured. The prose said "limbs A/B select five". It is three by limb A (check-nul-bytes, check-refd-timer-probe, check-closing-keyword-parity), one by limb B (check-watch-hint-literal) and one by limb C (check-comment-mask-corpus) — the last moved from B to C when limb B was tightened to refuse a path BUILD, and the census sentence was left behind.

The REASON is repaired at the level that does not rot. The census sentence now gives the sixth gate's POPULATION (a bounded subtree, scripts/), which is what its remedy turns on, rather than the SHAPE of a walk it can stop holding. On this tree it does still hold that walk; on PR #15506's head it holds none, and the sentence is true either way.

The fixture-side comment now names two MEASURED live specimens at two different walk roots, so one file's repair cannot empty the claim again:

file walk seeded at
scripts/check-self-test-wired.mjs walkScripts(scriptsDir) join(ROOT, 'scripts')
scripts/check-spec-parsed-alias.mjs walkZodFiles(SPEC_SRC) join(ROOT, 'packages/spec/src')

Both read null from repoRootWalkSpelling here and on PR #15506's head, and two new LIVE self-test cases read them off their real source rather than off a string typed in the file — a file that has gone is NOT MEASURED, never a quiet pass. The hand-written fixture case beside them is unchanged.

Verification — every exit code captured before any pipe

All 25 other commands dispatch-gates derives for this change set at head 4cfa319a7 were run; all exited 0. Verdict lines from the load-bearing ones:

✓ merge driver wiring is consistent (24 path(s) deliberately excluded).
✓ check-regen-pending self-test passed.
✓ the declared manifest population covers the root manifest and all 79 member manifest(s)
✓ all 31 generator(s) across 80 manifest(s) have a recorded disposition
✓ dispatch-gates self-test: 1415 cases pass.
✓ check:declared-population-live — 207 of 254 famil(ies) declare a path population, and every one of them reaches this tree's 7522 tracked file(s).
✓ check-watch-hint-literal: 54 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 33, ROOT_FILE_WATCH_HINTS 10, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -- ...
check-nul-bytes: OK (scanned 7518 text file(s) -- 7518 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).
✓ check:entry-guard: 211 scripts/ file(s) — every entry guard goes through invoked-as.mjs
✓ check:pnpm-filter-targets: 142/181 `--filter` occurrence(s) across 33 file(s) resolve against 79 workspace package(s)
OK  check-ratchet-remedy-authority: 202 scripts swept
check-self-test-wired --self-test: 4 live ledger row(s) verified ... 9 declared batteries, 59 cases registered, every battery at or above its pinned floor.
pnpm lint  -> exit 0, no findings (eslint . --no-inline-config, whole repo)

The dispatch-gates self-test was run DETACHED, as scripts/pm/check-dispatch-gates.mjs's own header instructs for an agent container, and both long runs went through scripts/pm/os-verify-lock.sh; the lock's verdict lines: VERDICT command-exit 0 · held the lock 391s (6m31s) · waited 41s for the first self-test run, and VERDICT command-exit 0 · held the lock 490s (8m10s) · waited 112s (1m52s) for the final-head run that carried pnpm lint and the self-test together. Both pnpm lint and pnpm check:pm-dispatch-gates were run AFTER the last commit, on head 4cfa319a7, so the greens above are greens on the tree this PR carries.

scripts/-only change, publishing nothing from any package, so skip-changeset.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…easure the bounded-subtree walk specimen

Two cards on the dispatch derivation, folded because both land in
`scripts/pm/dispatch-gates.mjs`. Card relations are declared once in the PR
body; this branch squashes, so no commit here carries a closing keyword.

## The manifest population (#15501)

`git-merge-regen --self-test` refuses a generator with no recorded merge
disposition, and the population that refusal sweeps is the MANIFESTS: the root
`package.json` plus every workspace member's, read for their `gen:` / `check:`
rows. What the family declared to `scripts/pm/dispatch-gates.mjs` was the
artifact paths `scripts/regen-artifacts.mjs` carries, imported one level down —
the generators ALREADY routed. So the one class of card the refusal exists to
catch (a card that ADDS a generator, touching a manifest and a new
`scripts/*.mjs`) was the one class the derivation could not name, and the gate
fired a cycle late, in CI, on every card of that shape.

Measured on 615fac3, before:

    node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -- package.json
    → 0 lines naming check:merge-driver

and after: `pnpm check:merge-driver` is listed, for the root manifest and for a
member manifest alike.

The repair is the population declaration, in the idiom the derivation already
supports and beside the read it describes — `ROOT_FILE_WATCH_HINTS` for the root
manifest in the subtree spelling `hintCovers` accepts, `DECLARED_WATCH_HINTS`
for the member manifests as full-path glob literals, both following
`check-workspace-manifest-cycles.mjs`, which declares the identical population.
No hand list of generator names (that ledger is `regen-artifacts.mjs`'s, and
this gate already reconciles it two ways), and no self-test case edited to pass.

A twelfth self-test callee, `reconcileManifestPopulation`, holds both
declarations against the enumerator's LIVE answer in both directions — every
member manifest covered, no declared pattern covering zero, the root literal's
collapsed form equal to the path the gate opens for the root owner — with its
roster entry and floor (11 → 12), since nothing else in the repo can redden when
a declaration read by another tool goes stale.

Two dispatch-gates self-test cases per direction, pinned against the LIVE tree
through the same discovery pass `derive` runs: a change set touching the root or
a member manifest derives the gate, an unrelated brand-new `scripts/*.mjs` does
not, and an in-place ablation shows it is the manifest declaration doing it.

## The bounded-subtree walk specimen (#15510)

Two sentences cited `check-self-test-workflow-commands.mjs` as the LIVE specimen
for `repoRootWalkSpelling`'s negative direction. The outcome they report is
re-measured and unchanged — the limbs select five of the six candidates, and
that gate is selected by none — but the REASON was written as the shape of that
gate's walk, and a gate can stop holding a walk without the census moving. The
census sentence now states the population (a bounded subtree, `scripts/`), which
is what the remedy turns on; its limb attribution is corrected too, measured:
three by limb A, one by B, one by C, not "limbs A/B".

The fixture-side comment now names two measured specimens at two different walk
roots — `check-self-test-wired.mjs` (`walkScripts`, seeded at `scripts/`) and
`check-spec-parsed-alias.mjs` (`walkZodFiles`, seeded at `packages/spec/src`) —
and two LIVE cases read them off their real source, so the claim cannot rot back
into prose pointing at a file with no walk in it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff origin/main...claude/issue-15501-merge-driver-derivation (merge base ed9d87653, head 4cfa319a7): 2 files, +324/−16, matching the PR file list; the claim's ruling (5545388370) taken as written.

Fixes #15501 / Fixes #15510 close both cards on merge. #15539 (t() drops a third argument at six call sites) is graded on the card. Flipping ready and arming auto-merge now; #15441 (p1) is the next dispatch on dispatch-gates.mjs once this merges.


Generated by Claude Code

@baozhoutao
baozhoutao enabled auto-merge September 4, 2026 20:17
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit bbf4a52 Sep 4, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15501-merge-driver-derivation branch September 4, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants