fix(scripts): the changeset level axis reads a packed bin target as a published surface - #16988
Merged
Merged
Conversation
… published surface
`judgeLevel` decided a package was "grown" from `packages/*/src/**` alone, so a
diff confined to a package's `bin` target -- which npm packs REGARDLESS of
`files` -- was invisible to it. The same `Clause-② yes` + `patch` pairing went
red over `src/**` and green over `bin/**` on the same day, and the green was
read as "the axis looked and approved" when it meant "the axis did not look".
`publishedSourceOwners` becomes a two-legged reading:
1. compiled source -- the path reads `<dir>/src/**`. Shape only, byte for byte
the leg that already existed, and deliberately NOT rewritten to ask "is
this in the packed set?": `files` is `["dist", ...]`, so such a predicate
would stop counting `src/**` and gut the axis while printing ticks.
2. a packed `bin` target -- the directory's own manifest names the path in
`bin`, in either spelling npm accepts. Manifest-derived rather than a list
of root names, so a `bin` pointing outside `bin/` is read and a file beside
the target that `bin` does not name is not.
The manifest reader is a REQUIRED argument: a default would silently read
`src/**` alone at a call site that reads as if it asked the whole question. A
manifest that is present and will not parse makes its directory a candidate that
cannot be named, so the path lands in `unreadable` rather than in neither set.
Superset-only, measured over the whole tree: 73 package dirs matched before, 73
after, 0 lost; 5303 paths resolved before, 5305 after, 0 lost, the two gained
being the repository's only two real bin targets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
baozhoutao
marked this pull request as ready for review
September 9, 2026 00:16
baozhoutao
enabled auto-merge
September 9, 2026 00:16
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 #16692
The predicate, in words, before the diff
A package's published surface — for the purpose of the changeset LEVEL axis — is now read on two legs, and a directory owns a changed path when either holds:
DIR/src/**. Shape only, at any depth. Byte for byte the leg [finding] The changeset LEVEL axis is blind to every NESTED package:packages/*/src/**matches one segment, so 51 of 74 workspace packages (all drivers/services/adapters) can pairClause-②: yeswithpatchand stay green #16713 / PR fix(tooling): resolve the changeset LEVEL axis package from its manifest, so the 51 nested packages are candidates it can refuse #16969 landed, untouched.bintarget —DIR's ownpackage.jsonnames the path inbin, in either spelling npm accepts (a bare string, or an object of command names to paths). npm packs abintarget regardless offiles(The PUBLISHED CLI puts its own stdout/stderr on the blocking write path every time it spawns a child with inherited stdio #14874), which is what makes it published.Candidates are still enumerated shallowest first and the first readable manifest still wins.
⛔ Why leg 1 was NOT rewritten as "is this path in the packed set?"
Triage ruled option A — judge a package by what it packs. Taken literally that ruling is a trap, and this is the one thing worth reading twice:
packages/cli'sfilesis["dist","README.md","CHANGELOG.md"], sosrc/**is not packed — it is whatdistis compiled from. A predicate rewritten to ask "is this in the tarball?" would therefore stop countingsrc/**as growth, gut this axis outright, and print a tick while doing it — the exact failure shape this card and #16713 both document. So the packed reading is an additional leg, never a replacement, and A's mechanism is taken while A's literal wording is not.⛔ Why leg 2 is not "add
bin/**to a list of roots"Triage refused option B in this card's own words — «it is a list, and lists drift — this finding exists because of a list». The difference is measurable, not stylistic, and both directions are pinned:
packages/cli/bin/run-dev.jssits besidebin/run.jsin this repo.bindoes not name it andfilesexcludes it, so it does not ship — abin/**root list would count it. The reader does not.binpointing atdist/cli.jsorscripts/run.jsis read. A directory list is blind there.Superset-only — the safety property, measured
No path that was "grown" before may stop being "grown": this axis may over-include harmlessly and can never under-include harmlessly. Leg 1 runs first and unconditionally, so the two legs are a union in which leg 2 can only add — and that is measured over the whole tree rather than argued.
0 lost on both counts is the superset property. Every count carries its controls:
packages/carry abinfield; 2 targets resolve; both are tracked in the tree. The zero-gain on dirs is therefore a real reading and not a leg that never fired: both packages already matched throughsrc/**, so what the new leg gains is paths, not dirs.+2comes from the two real manifests, not from the walk having been widened.packages/cli/bin/run-dev.js(tracked) resolves to[];packages/cli/bin/run.js(tracked) resolves to["packages/cli"].(#16969 reported 72 package dirs; this tree reads 73 — one package dir has landed since, and none was lost.)
Acceptance
Run on synthetic git repos driving
scan→packagesTouched→judgeLevel→renderLevel, all underClause-②: yes.packages/cli/src/**+patchpackages/cli/bin/**+patch(objectbin)packages/cli/bin/**+patch(stringbin)packages/cli/bin/**+minor— level controlcontent/docs/**only +patch— negative control.github/**only +patch— negative controlpackages/drivers/driver-sql/src/**+patch(#16969's leg)The before leg was run first, on
origin/main, precisely to confirm the card still stood —src/**RED besidebin/**GREEN is the finding. The two negative controls are what make the reds readable: an implementation that simply always enforced would satisfy every positive above and fail only there.The control on
mainthat asserted this fix had not happened#16713 wrote a control at
check-changeset-no-major.mjs:2822holding this card's boundary open — «bin/**is still NOT read … a fix that reddened here would be answering a different card». It did its job: the boundary could not close by accident. It is inverted in place, not deleted — its reasoning kept, its direction flipped, and its message rewritten to name what still is not read.⛔ The 误判 half is NOT closed here, and is named
This PR closes the ROOT axis (which roots ship). The opposite failure recorded in comment
5576046189—packages/rest/src/rest-route-ledger.ts, one line of package-internal data, zero public face, counted as "grown" and pushed towardminor— is untouched. "Did thissrcchange grow the public face?" is not answerable from the path at all and needs a different instrument; it is a separate card the PM is filing. ⛔ Do not read this PR as having closed both.Two smaller decisions worth a reviewer's eye
publishedSourceOwnersthrows aTypeErrorwithout it. A default that quietly skipped leg 2 would restore exactly the blindness this card closes, at a call site that reads as if it asked the whole question — check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690's direction, applied to a collaborator rather than to an input.unreadable's invariant extends to the new leg. The manifest reader is tri-state: parsed object /false(present, will not parse) /null(absent).falsemakes the directory a candidate that cannot be named, so the path lands inunreadablerather than in neither set. Residual stated rather than implied: where a manifest is absent there is nobinfield to have named anything, so leg 2 contributes no candidate and none is owed — pinned with its own nonsense control.Verification
node scripts/check-changeset-no-major.mjs --self-test— 262 assertions, exit 0. New batteryTHE ROOT: a packed bin target is a published surface the axis can refuse (#16692)registers 37 cases;SELF_TEST_BATTERY_FLOORraised 16 → 17.node scripts/pm/dispatch-gates.mjs --ran→ «33 derived, 33 run, 0 NOT-MEASURED, 0 UNRUN».pnpm lint(eslint . --no-inline-config, whole repo, no narrowing) — exit 0.e6facf5240.验收备注
skip-changeset: nothing published moves. The root manifest isprivate: truewith nofiles, and every other manifest lives underapps/·examples/·packages/, whosefiles[]are package-relative and cannot name a repo-rootscripts/path. Positive control on that reading: the whole-tree run above resolved 2 realbintargets, so the manifest reader was live rather than silently empty.content/docs/protocol/backward-compatibility.mdxnames this script twice — both times about the major-bump axis, not the level axis. Not stale; no docs change owed.🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Generated by Claude Code