Skip to content

fix(scripts): the changeset level axis reads a packed bin target as a published surface - #16988

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-16692-published-bin-root-axis
Sep 9, 2026
Merged

fix(scripts): the changeset level axis reads a packed bin target as a published surface#16988
baozhoutao merged 1 commit into
mainfrom
claude/issue-16692-published-bin-root-axis

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

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:

  1. compiled source — the path reads 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 pair Clause-②: yes with patch and 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.
  2. a packed bin targetDIR's own package.json names the path in bin, in either spelling npm accepts (a bare string, or an object of command names to paths). npm packs a bin target regardless of files (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's files is ["dist","README.md","CHANGELOG.md"], so src/** is not packed — it is what dist is compiled from. A predicate rewritten to ask "is this in the tarball?" would therefore stop counting src/** 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.js sits beside bin/run.js in this repo. bin does not name it and files excludes it, so it does not ship — a bin/** root list would count it. The reader does not.
  • A bin pointing at dist/cli.js or scripts/run.js is 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.

tracked files scanned .......... 8410
package dirs matched  BEFORE ... 73
package dirs matched  AFTER .... 73   (gained 0, LOST 0)
paths resolving       BEFORE ... 5303
paths resolving       AFTER .... 5305   (gained 2, LOST 0)
newly-owned paths .............. ["packages/cli/bin/run.js",
                                  "packages/create-objectstack/bin/create-objectstack.js"]

0 lost on both counts is the superset property. Every count carries its controls:

  • FIRING control — 2 manifests under packages/ carry a bin field; 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 through src/**, so what the new leg gains is paths, not dirs.
  • NONSENSE control — re-run with a bin-bearing manifest injected under every ancestor of every path: 73 dirs / 5303 paths, i.e. identical to BEFORE. So the +2 comes from the two real manifests, not from the walk having been widened.
  • SIBLING controlpackages/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 scanpackagesTouchedjudgeLevelrenderLevel, all under Clause-②: yes.

leg before after
packages/cli/src/** + patch RED RED
packages/cli/bin/** + patch (object bin) GREEN RED
packages/cli/bin/** + patch (string bin) GREEN RED
packages/cli/bin/** + minor — level control GREEN GREEN (and still SEEN)
content/docs/** only + patchnegative control GREEN GREEN
.github/** only + patchnegative control GREEN GREEN
packages/drivers/driver-sql/src/** + patch (#16969's leg) RED RED

The before leg was run first, on origin/main, precisely to confirm the card still stood — src/** RED beside bin/** 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 main that asserted this fix had not happened

#16713 wrote a control at check-changeset-no-major.mjs:2822 holding 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 5576046189packages/rest/src/rest-route-ledger.ts, one line of package-internal data, zero public face, counted as "grown" and pushed toward minor — is untouched. "Did this src change 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

  • The manifest reader is a REQUIRED argument and publishedSourceOwners throws a TypeError without 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). false makes the directory a candidate that cannot be named, so the path lands in unreadable rather than in neither set. Residual stated rather than implied: where a manifest is absent there is no bin field 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-test262 assertions, exit 0. New battery THE ROOT: a packed bin target is a published surface the axis can refuse (#16692) registers 37 cases; SELF_TEST_BATTERY_FLOOR raised 16 → 17.
  • 33 of 33 derived gate families run, all exit 0; reconciled with 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.
  • Control-byte self-scan over the changed file — no hits.
  • All of the above at e6facf5240.

验收备注

  • skip-changeset: nothing published moves. The root manifest is private: true with no files, and every other manifest lives under apps/ · examples/ · packages/, whose files[] are package-relative and cannot name a repo-root scripts/ path. Positive control on that reading: the whole-tree run above resolved 2 real bin targets, so the manifest reader was live rather than silently empty.
  • content/docs/protocol/backward-compatibility.mdx names this script twice — both times about the major-bump axis, not the level axis. Not stale; no docs change owed.
  • Out of scope, noted, not filed: nothing beyond the 误判 half above.

🤖 Generated with Claude Code

https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU


Generated by Claude Code

… 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
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