pm gates: refuse a widening diff whose claim declares Clause-②: no (the mechanical half of the directional ruling) - #16604
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX
`splitUnifiedDiff` wrote `additions: addedLines(patch).length` for every row, and `addedLines(null)` is empty — so a BINARY change stamped a count nobody took. `unreadFiles` skips a row that added nothing, so a binary edit to `packages/spec/api-surface/*.json` arriving through the local path reported `state: 'clean'`: the gate's own contract, declared and not enforced, inside the gate. The three states are now told apart by what the diff SAYS — a hunk gives the count, a `Binary files`/`GIT binary patch` marker gives `null` (UNKNOWN), and neither gives a real `0` (a mode-only change or a pure rename adds no line). `addedNothing` interprets both input paths in one place, so a MISSING count can never become a zero; GitHub's own `additions: 0` on a binary row is kept, because that reading was taken by something that can see the blob. The self-test pinned the two halves separately and never composed them, which is why it stayed green. Seven composed cases now drive splitUnifiedDiff into wideningRefusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX
This was referenced Sep 7, 2026
…nnot honour The gate's CI command is its own --self-test, so on the marker's own second listed cause the declaration read true. dispatch-gates' live guard refuses it anyway the moment a family NAMES paths, and this one names 59: 9 from its module body (the registry table, the objectui mirror glob, two repo slugs, four fixture filenames) and 50 inherited from the two registers it imports on purpose, SUSPECT_TIER_GLOBS and REGEN_ARTIFACTS. Getting those out of a scanned position would mean hand-copying two registers this file imports precisely so it can never disagree with them - the drift check:pm-governed-prose exists to stop, and this gate's strongest property. So the marker goes and the derivation stands. The header now records what the derivation says, which part of it is right (packages/spec/src/**, api-surface/**, the three registries - the surfaces this gate polices) and which part is noise inherited from REGEN_ARTIFACTS, and why one cheap self-test in a MATCHED column is the smaller error than a marker sitting above a live population. The case that catches this sits at ~1534 of dispatch-gates' self-test and needs well over 540s to reach, which is why three local runs missed it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dxq7YqsLDMeZDZ5AzsgJX
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.
Part of #16448
The mechanical half of the directional clause-② ruling (#16349): a diff that
adds a schema key, a closed-set member, a published export row or a registry
entry, while its card's claim declares
Clause-②: no, is refused at enqueue withthe
file:lineof every tell.What lands
scripts/pm/check-widening-tells.mjs(new) — the tells, the surfaces, therefusal sentence and a 131-case
--self-test. Pure over its input; its wholeinput is a diff, supplied by its caller.
scripts/pm/check-clause2-carriers.mjs— a fifth row, C5, on the--pairpredicate. That is the enqueue path:references/contract-review.mdlanding pre-check ② already runs
--pair PR-NUMBERbefore a seat may hand apair to the queue. C5 joins the declaration this file already reads to the diff
the sibling judges; the tells themselves are stated once, in the sibling.
package.json/.github/workflows/lint.yml—check:pm-widening-tells,the self-test wired the way this directory's other gates wire theirs
(
check:self-test-wiredgreen over the new step).The four tells, calibrated against the tree rather than guessed
SUSPECT_TIER_GLOBS, imported fromdispatch-gates.mjsz.enum/z.union/ anas constarrayREGEN_ARTIFACTSrows whosecheckischeck:api-surfaceT1's recogniser was measured over
packages/spec/src/**before it was written:8,102 property lines take a
z.value, and the whole non-z.schema vocabularyunder them is
retiredKey((235), the*Schemaidentifiers (about 300),strictObject((46) andlazySchema(. Requiring a schema-shaped value is whatkeeps the tell off the 1,655
x: trueand 1,170x: stringlines in the samefiles, which are object literals and type annotations, not accept-set members.
Two boundaries this PR keeps
existing
Clause-②:reader; this gate writes nothing and hangs nothing —hanging a review gate from a checker would be issuing the verdict.
--paironly, never the sweep. Anopair costs one extra request (itschanged-file listing). Paying that per sweep pair would push a 29-PR sweep past
the anonymous hourly budget it already sits on, and a widening tell on somebody
else's pair is a board fact, not a verdict about the PR that runs CI next —
the same split every other row in that file already makes.
Where judgement was exercised, along the four axes
Home: a sibling script, not a function inside
dispatch-gates.mjs.Real need — measured:
--pairhas no workflow caller at all; it is a seat'spre-arm predicate, and
dispatch-gates.mjsis 20k lines thatcheck-clause2-carriersdoes not import today. Long-term — a leaf module keeps the dependency acyclic
(the sibling imports the leaf; the exit-code pin is written on the importing
side). AI-error — the tells get their own self-test and their own battery
floors, so a weakened recogniser reddens something. Startup focus — no new
surface: one file, one package script, one CI step.
Exit 4 reused, not a new code. Rows already carry codes (C1..C5); the exit
carries one bit, "a verdict about this pair, adverse". A fifth exit would make a
seat reading
$?maintain two tables for one decision.A registry table declared by hand, with an existence guard. No register in
the tree answers "which files are closed-vocabulary registries", so this one is
declared here — and pinned to paths that exist, the way
MANDATORY_TIER_GLOBSis,because a declared-but-absent glob is dead data that guards nothing while reading
as protection.
Patch round 1 — an unread diff was reading as a narrow one
The seat's contract-tier review found the gate breaking its own contract. The
local diff splitter wrote
additions: addedLines(patch).lengthfor everyrow, and
addedLines(null)is empty — so a binary change stamped a countnobody had taken.
unreadFilesskips a row that added nothing, so a binary editto
packages/spec/api-surface/kernel.jsonarriving through the local pathreported
state: 'clean'. Declared and not enforced, inside the gate whose wholecontract is an unread diff is not a narrow diff.
The three states are now told apart by what the diff says:
additionsBinary files … differ/GIT binary patchnull0addedNothinginterprets both input paths in one place, so a missing countcan never become a zero. The API path keeps GitHub's own
additions: 0on abinary row — that reading was taken by something that can see the blob, so the
two paths differ by information available, not by drift; the splitter's
docblock says so.
The self-test had pinned the two halves separately (
patch: null, and "null isa gap" on a hand-built row carrying no
additions) and never composed them,which is why it stayed green. Seven composed cases now drive
splitUnifiedDiffintowideningRefusal, and four more pin theadditionsreadings themselves.
Measured
At head
9f33c3a3:node scripts/pm/check-widening-tells.mjs --self-test— 0, 131 cases(120 before the patch round).
node scripts/pm/check-clause2-carriers.mjs --self-test— 0, 209 cases(was 190; +16 for the C5 join, +3 for the exit-register pin).
expression replaced by the original
addedLines(patch).length. On disk: blobd951ee5bto70da08e1, marker present 1, fixed expression present 0.Mutated self-test exit 1, 5 failing cases — the two
additionspins andthe three composed ones, including
a BINARY change to a tell surface reads INCOMPLETE, never clean. Restored tod951ee5b,git diff HEADempty,self-test back to 0.
SCHEMA_PROPERTYreplaced by anever-matching pattern. On disk: blob
40decab9tob49d87cf, marker present1, original opener present 0. Mutated self-test exit 1, 14 failing cases.
Restored to
40decab9,git diff HEADempty, self-test back to 0.needsWideningReadforced tofalse.On disk: blob
71d7d605to74687b76, marker present 1, original guardpresent 0. Mutated sibling self-test exit 1, 7 failing cases. Restored to
71d7d605,git diff HEADempty, self-test back to 0.Ablation 1 deliberately leaves the sibling green: its C5 battery drives a T2
tell, so ablating T1 must not move it. Two instruments, two populations.
OS_VERIFY_LOCK_SLOT=issue-16448 bash scripts/pm/os-verify-lock.sh -c "pnpm lint"printed
VERDICT command-exit 0, 76s held.eslintover the two touched fileswith
--format json— 0 errors, 0 warnings; a full-treeeslint . --no-inline-config --format jsonearlier in the round counted6,286 files, 0 errors, 0 warnings, i.e. the whole population eslint's own
config selects.
9f33c3a3,node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(61 commands): 58 exit 0. Two exit 3, PREREQUISITE NOT MET and are NOT
MEASURED —
check:dual-build-cjs-loadsandcheck:type-check-debtread builtdist/and refuse withoutpnpm build; this diff touches no file underpackages/, so neither can move, and CI runs them on a built tree.check:pm-dispatch-gatesdid not finish in a 300s foreground budget — anddoes not finish on a pristine checkout either (control run with none of
these changes: 540s wall, 1,522 assertions printed, zero failures). A
pre-existing runtime on this box, not a regression; CI owns it.
node scripts/pm/dispatch-gates.mjs --ranat9f33c3a3—61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN.node scripts/pm/check-governed-merges.mjs --testover this PR's exact filelist — exit 0,
0 of 4 path(s) hit the register: ordinary queue landing.pnpm check:nul-bytes— 0.git merge-tree --write-tree --name-only origin/main HEAD—exit 0, clean against
a7c14cdf.skip-changeset: this PR publishes nothing from any package —scripts/pm/**,two
package.jsonscript entries and one workflow step.验收备注
The one line in
.claude/skills/pm-dispatch/references/contract-review.mdthatnames this gate ships as a separate PR (#16605) — that file is on the
governed surface (
check-governed-merges --testreturns exit 3 for it), so ittakes the human-merge route and must not ride a queue-landing PR.
Out-of-scope observations, noted and not filed:
RendererRegistryappears only in ADR-0012's notification-platform table. Thelive shape nearest to it is
METADATA_FORM_REGISTRY, whose own docblock callsit the registry the generic SchemaForm renderer reads, so that is the row
declared. Recorded in the gate's header. Successor: whoever ports this gate to
a repo that has one. Not filed — there is no defect here, only an absent
subject.
packages/types/src/zod/**. Therow is declared and repo-keyed so a port is a data edit, but nothing runs
this gate in objectui today. Successor: none today; the row is inert by
construction and its inertness is asserted in the self-test.