Skip to content

fix(pm): the mixed-diff diversion on PR #15284 fired and lawfully found nothing to fork — the three report-side readings that made it look like a failure - #15650

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-15406-mixed-diff-diversion
Sep 5, 2026
Merged

fix(pm): the mixed-diff diversion on PR #15284 fired and lawfully found nothing to fork — the three report-side readings that made it look like a failure#15650
baozhoutao merged 2 commits into
mainfrom
claude/issue-15406-mixed-diff-diversion

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Part of #15406 — the MECHANISM half only. The governance ruling half (whether the merge was
compliant, whether anything is rolled back, whether os-justin is a principal) is the project
director seat's, and nothing here decides or implies it.

Root cause: the diversion evaluated PR #15284 and lawfully found nothing to fork

The queue guard ran on #15284's merge group and printed this, verbatim
(run 33874147578, job 101026960644, merge_group on gh-readonly-queue/main/pr-15284-dc46c4ec1,
head f502898a49530a1c85e58f3c4d2b340c0e1cb909, 2026-09-04T12:43:16Z):

merge group on refs/heads/main — 1 commit(s) in range
  ℹ️  generated-surface exception (#11705) LIFTED skills/objectstack-ui/references/react-blocks.md on f502898a4953: byte-equal to `pnpm --filter @objectstack/spec gen:react-blocks` recomputed on this tree — the generator declared 1 output(s), this path among them, and its own `check:react-blocks` reported no drift across all of them (no stored baseline consulted)
Governed Surface Queue Guard — merge_group — 0 governed pull request(s), 0 unattributed governed commit(s), 0 API read(s) (PR head + reviews).
  ✅  CLEAR — the diff touches no governed surface, so this guard has nothing to judge.
      Derived from GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs (5 surfaces),
      never from a restated list. ⛔ ZERO review lookups were made: the path test runs first and returns,
      so a GitHub API outage can never block a diff that touches nothing governed.

So the leg that owns the outcome is the register, not a broken predicate. #15284's single
skills/** path is skills/objectstack-ui/references/react-blocks.md, which matches the
spec-react-blocks row of GENERATED_SURFACE_EXCEPTIONS (ruling #11705). Its provenance was
recomputed on that tree, certified byte-exact, and the path was lifted — after which the diff
held no governed path and the mixed-diff rule had nothing to fire on. That is the ruled
behaviour under 2026-09-01 (「纯生成的指针行(spec 源变更后再生成的 references/_index.md) 不需要我审核吧」).

The hunk corroborates it — one table row whose description column is derived from the
grouping .describe() this same PR edited in packages/spec/src/ui/view.zod.ts, truncated by
the generator:

-| `grouping` | `object` | data |  | Group records by one or more fields |
+| `grouping` | `object` | data |  | Group records by one or more fields — server-side: the groups and their header numbers come from an aggregate query over the whole filtered set, rows within a … |

Reproduced live on today's tree, with #15284's real 11-path file list through the seat-side
predicate — the three tools agree, as #11705's "⛔ do not author a second mechanism" requires:

governed-surface predicate: 0 of 11 path(s) hit the register (5 surfaces, repo-agnostic).
  ✅  NOT governed — ordinary queue landing applies to a PR with exactly this file list.
  ℹ️  generated-surface exception (#11705): skills/objectstack-ui/references/react-blocks.md is a PURE REGENERATION —
      byte-equal to `pnpm --filter @objectstack/spec gen:react-blocks` recomputed on THIS tree (never a stored baseline),
      so this path does not govern the PR by itself. Any other governed hit still forks the whole PR.

Why the four existing mixed-diff assertions did not "cover" this: they do cover it. They
assert that a single live governed hit governs the whole PR. #15284's only governed hit was
not live at verdict time — it was lifted — and the self-tests already carried that case too
(with-the-toolchain-installed-a-PURE-REGENERATION-merge-group-CLEARS-with-zero-approvals-and-zero-api-calls).
No assertion was missing. ⇒ No predicate is patched here.

What IS broken, and it is the reporting

Three report-side statements denied the register hit they had just processed, and together they
are why a compliant landing reads back as a mechanism failure:

  1. check-governed-queue-guard.mjs — the clear rendering asserted "the diff touches no
    governed surface" and "the path test runs first and returns" one line under its own LIFTED
    note
    . Both are false for that run: the path test matched, and the recompute ran.
  2. check-governed-merges.mjsrenderTestVerdict's head counts the POST-lift set, so it
    printed "0 of 11 path(s) hit the register" directly above the exception line naming the hit.
  3. check-governed-merges.mjs — the sweep classifies with governedPathsIn alone and never
    consults the register (deliberately: provenance is a recompute against the tree a commit
    landed on, and the sweep holds no such tree). The row it renders for a certified regeneration
    is therefore indistinguishable from a row for a hand-authored governed merge.

All three fixes are report-only. No predicate, verdict, exit code, API cost or listing changes;
the zero-cost clear keeps its wording byte-for-byte on both legs, so the 2026-08-27
pull_request byte-identity constraint is untouched. registerCell lifts nothing and suppresses
nothing — the row is still listed and still counts as a governed merge — and it repeats the
register's own doctrine rather than softening it: a candidate earns the QUESTION, never the
answer, and certification is recorded in that landing's queue-guard log.

Timeline facts for #15284 (reported, not judged)

when (UTC) event actor
2026-09-04T11:37:28Z last commit 49f19218bchore(spec): regenerate api-surface / export-origins / references … os-justin
2026-09-04T12:40:46Z ready_for_review os-justin
2026-09-04T12:41:02Z auto_merge_enabled os-justin
2026-09-04T12:42:18Z added_to_merge_queue os-justin
2026-09-04T12:43:16Z queue guard on the merge group: CLEAR, 0 API reads, react-blocks.md LIFTED
2026-09-04T13:08:11Z merged as f502898a4 os-justin
2026-09-04T13:08:11Z removed_from_merge_queue github-merge-queue[bot]

Reviews: none. The timeline carries zero reviewed events of any state — no APPROVED,
no CHANGES_REQUESTED, no DISMISSED, by any account. The guard did not need one: with the only
governed path lifted it returned before any review lookup existed (0 API read(s)).

No late push. The skills/** file entered the diff at 49f19218b (11:37:28Z), 65 minutes
before the enqueue and 66 before the guard ran — so the classic "added after the guard ran"
bypass is ruled out. The guard also ran twice on the PR head (49f19218b) as the
pull_request early warning, 11:56:24Z and 12:40:51Z, both success.

Not done here, and why. The dispatch's optional report-only approval column is not
added: check-governed-merges.mjs reads only /pulls/{n} for merged_by/merged_at
(fetchPullAttribution), so an approval column would be a new API surface, which the brief
forbids.

Reproduction case

A new self-test battery in each script replays #15284's exact shape: its real file list (11
paths in check-governed-queue-guard.mjs, the 4-path core in check-governed-merges.mjs), one
commit, PR 15284, zero reviews of any kind, an approver set (GOVERNED_APPROVERS) that does
not contain the merging account, and a recompute that certifies react-blocks.md. The queue
battery drives main()'s own wiring — lift, derive what was lifted, judge — rather than the
existing endToEnd helper, because the defect lived in the step between those two.

Both directions are pinned: with the recompute certifying, the verdict is still clear/exit 0
with apiCalls === 0; with the recompute refusing (a hand edit to the same path), the identical
file list still REFUSES with EXIT_REFUSED_UNAPPROVED. The shape is not an exemption for
references/react-blocks.md; it is an exemption for a recompute.

Ablation

Each fix was reverted on a committed tree and the batteries re-run. Mutation and restore were
both confirmed on disk (injected-marker / removed-text counts, then git checkout HEAD -- path
verified against the HEAD blob hash, with git diff HEAD empty), and each script carried a
trap … EXIT INT TERM restore.

script mutation result
check-governed-queue-guard.mjs const lifted = verdict.lifted ?? []const lifted = [] (the pre-fix unconditional clear wording) ✗ 3 of 144 case(s) failed, exit 1 — including ⭐ the-CLEAR-line-does-NOT-claim-the-diff-touched-no-governed-surface and ⭐ the-CLEAR-line-does-NOT-claim-the-path-test-returned-before-matching
check-governed-merges.mjs registerCell returns '' immediately; the lift clause on the --test head forced off ✗ check-governed-merges --self-test — 7 failure(s), exit 1

Restore legs: RESTORE OK: … == HEAD blob 240297fb1f955c2eefaadb9c12389b763fe331ea and
RESTORE OK: … == HEAD blob 081df968d6900cdb636af2570727e03390e73778, git diff HEAD empty in
both.

Verification

Derived, not recalled: node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack
(exit 0) — change set read by the tool itself from the merge base d30ccb9bd, two paths,
scripts/pm/check-governed-merges.mjs and scripts/pm/check-governed-queue-guard.mjs. All 32
emitted commands were run; every one exited 0. The union was run on the final commit
9832369e9.

command exit verdict line
node scripts/pm/check-governed-queue-guard.mjs --self-test 0 ✓ check-governed-queue-guard self-test: 144 cases pass … (was 133)
pnpm check:pm-governed-merges (= check-governed-merges.mjs --self-test) 0 ✓ check-governed-merges --self-test: 274 assertions … + live: the real generator declared 9 output(s) and certified this tree (was 263)
pnpm check:pm-dispatch-gates 0 ✓ dispatch-gates self-test: 1445 cases pass. (no case added by this PR)
pnpm check:nul-bytes 0 clean (plus a direct grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over both edited files: no match)
pnpm check:pm-governed-prose 0 the register's surface list still matches AGENTS.md
the other 28 derived commands 0 see the full list in the PR discussion

Also run, because their rosters live under scripts/ or scripts/pm/ — the directories this
diff is in — where dispatch-gates scores them silent and says that silence is evidence in
neither direction: check-published-list-mirrors.mjs (+ --self-test), check:console-injection,
check:engine-double-contract, check:i18n-stale-fill, check:pm-label-desc-cap — all exit 0.

NOT MEASURED, by name:

  • pnpm check:published-readme-exports — exit 1, but a PREREQUISITE failure, not a finding:
    every message is … does not exist. Build first: pnpm build, over packages/** READMEs this
    diff does not touch. This worktree has no dist/. It is also a roster gate dispatch-gates
    did not derive for these paths.
  • The three families whose argv takes a value from the workflow (check-cross-package-test-inputs --union-into "$RUNNER_TEMP/…", check-shard-attestation --shard ${{ matrix.shard }},
    check-test-completeness "$RUNNER_TEMP/test-core.log") — no value outside a CI run.
  • The one family dispatch-gates reports as CI-MEASURED ONLY (it reads the workflow event
    payload).
  • The 16 families derived against a changeset path: none is written — scripts/pm/** publishes
    from no package, so this PR carries skip-changeset.
  • Whole-repo pnpm lint was not run. Narrowed instead, and the narrowing is NOT proved to the
    three-part standard, so it is reported as not run rather than as a measurement.

Scope

Two files, both scripts/pm/**: check-governed-queue-guard.mjs and
check-governed-merges.mjs. dispatch-gates.mjs is not touched — its skills/**
classification row is correct and was not the root cause. .github/workflows/**, .claude/**,
skills/**, AGENTS.md, CLAUDE.md and docs/adr/** are untouched. No changeset
(scripts/pm/** publishes from no package); skip-changeset applied.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…ct lift no longer reports itself as a clear that matched nothing (#15406)

The merge-queue log for PR #15284 printed, one line under its own
`LIFTED skills/objectstack-ui/references/react-blocks.md` note:

    ✅  CLEAR — the diff touches no governed surface, so this guard has nothing to judge.
    … ⛔ ZERO review lookups were made: the path test runs first and returns

Both sentences are false for that run. The path test MATCHED (the diff's
eleventh file is on the `skills/**` surface), and the register's own recompute
ran and certified it. Read back from the log, a compliant landing under the
2026-09-01 generated-artifact ruling is indistinguishable from a guard that
never saw the file.

Report-only: `guardVerdict` now carries the paths the register lifted (default
`[]`), and the `clear` rendering picks between the zero-cost clear — kept
BYTE-FOR-BYTE on both legs, so the 2026-08-27 pull_request byte-identity
constraint is untouched — and a clear reached through a lift, which names the
lifted paths and says the recompute ran. No predicate, verdict, exit code or
API cost changes.

`liftedPathsBetween` derives what was lifted from the row lists on either side
of `liftGeneratedExceptions`, not from its prose notes, and is deliberately
conservative across rows (the #11084 fence is per-row).

Self-test: 133 → 144 cases; new battery replays #15284's real 11-path file
list, one commit, PR 15284, zero reviews of any kind.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
… it does not recompute, and --test stops reporting a post-lift zero as a clean read (#15406)

Two report-side readings turned a compliant landing into an incident card.

1. `renderTestVerdict`'s head counts `hitPaths`, which is the POST-lift set.
   On PR #15284 it printed "0 of 11 path(s) hit the register" immediately above
   the exception line naming the path that hit it. The count keeps its meaning
   (what is STILL governed) and now says when the register lifted the
   difference. Byte-identical when nothing was lifted.

2. The sweep classifies with `governedPathsIn` alone and never consults the
   exception register — deliberately: provenance is a recompute against the
   tree a commit landed on, and this sweep holds no such tree. The row it
   rendered for #15284 was therefore indistinguishable from one for a
   hand-authored governed merge. `registerCell` adds the missing reading: which
   register row the governed path belongs to, that this sweep does NOT
   recompute, and that certification is recorded in that landing's queue-guard
   log. It lifts nothing and suppresses nothing — the row is still listed and
   still counts as a governed merge — and it repeats the register's own
   doctrine rather than softening it: a candidate earns the QUESTION, never the
   answer. Membership is the register's own `generatedExceptionFor`, so no
   second mechanism is authored (#11705's ruled constraint).

Self-test: 263 → 274 assertions, new battery replaying #15284's shape in both
directions (all-registered, mixed with hand-authored content, and none).

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

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

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

Reviewed against the three-dot diff at 9832369e9 (2 files, +373/−15), not the report. Part of #15406 — the mechanism half; the governance half stays with the director seat (#12708) and this PR decides nothing there.

Root cause accepted as measured: the queue guard ran on #15284's merge group (job 101026960644) and cleared through a certified spec-react-blocks lift; the predicate did not fail. What was wrong was the reporting, in three places, and the diff touches exactly those three:

  • check-governed-queue-guard.mjs: guardVerdict/runGuard carry lifted (default []); pure liftedPathsBetween(before, after) derives it from the row lists, conservative across rows (a path kept by any row is not listed), membership via the register's own generatedExceptionFor. The clear rendering forks: nothing lifted → the pre-existing text byte-for-byte (pinned on both legs, so the 2026-08-27 pull_request byte-identity constraint holds); lifted → names the paths and says the recompute ran. Exit codes, API cost and the predicate are asserted unchanged.
  • check-governed-merges.mjs: renderTestVerdict appends after N generated-artifact lift(s) only when N>0 (byte-identical otherwise); pure registerCell(entry) adds a report-only reading under a sweep row whose governed path is a register candidate, repeating the register's doctrine (a candidate earns the question, never the answer), naming where certification is recorded, and saying a mixed row is governed regardless. The row is still listed and still counted.
  • Battery floors: queue-guard 17→18 batteries (+10 cases), merges 23→24 (+10). dispatch-gates.mjs untouched, no governed surface touched.

Measured by this seat on the PR head (throwaway worktree, PM checkout's node_modules):

node scripts/pm/check-governed-queue-guard.mjs --self-test   EXIT=0   144 cases pass
node scripts/pm/check-governed-merges.mjs --self-test        EXIT=3   PREREQUISITE NOT MET — `tsx` is not installed in this checkout

The merges self-test is NOT MEASURED here (its live battery runs the real generator); the dev reports 274 assertions with the live line, and CI's Lint & Repo Gates is the arbiter.

Fences held: two files under scripts/pm/** only; no overlap with any other open PR; git merge-tree origin/main clean; skip-changeset (nothing published).

The dev's two open questions are recorded here, not decided: the sweep-side recompute (options A/B/C, C shipped) and the ruling-intersection reading both belong to #15406's governance half. Flipping ready + enabling auto-merge; the card stays open (Part of).


Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

2 participants