Uh oh!
There was an error while loading. Please reload this page.
feat(pm): dispatch-gates derivation follows first-party imports - #11512
Merged
Conversation
`resolveCheckToFiles` reads a family's script paths out of the npm script's command string and `discoverFamilies` scanned exactly those files, so a module a gate imports was never opened: a population declaration moved out of a gate and into a shared module stopped contributing hints to every gate that imports it, with every gate still green and nothing in the output saying so. `firstPartyImportTargets` resolves the relative static import specifiers of a gate script one level down, inside the repo's own scripts/ tree, never into node_modules and never into a module that is itself a discovered gate file. Hints from a followed module are appended after every own hint, so no existing match changes key or provenance, and `entry.hintOrigin` carries which module contributed each inherited hint into `coveringKey`'s via column. Measured over 140 discovered families x 6460 tracked files: watch-hint (gate, file) pairs 51848 -> 52741 (+893, ZERO lost) families gaining coverage 6 existing matches re-attributed 0 Every refusal carries its own measurement in the docblock: following gate modules too costs +4907 instead of +893 (3065 of the difference is one family inheriting a repo-wide declaration table from a gate it imports one string helper from), reaching outside scripts/ costs +2517 more, dynamic import() adds +0, and depth 2 and depth 3 add +0. The self-test's pin of the old constraint is inverted rather than deleted, and every case now names its specimen with a count: measured, the old pin would NOT have gone red on this change, because it picked its specimen by iteration order and landed on a family whose imported module declares no path at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
This was referenced Aug 24, 2026
os-steve
marked this pull request as ready for review
August 24, 2026 02:27
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 24, 2026
This was referenced Aug 24, 2026
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 freeto 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.
Fixes#11190
Step 1 only, as dispatched. The derivation now follows first-party imports. The thirteen private
pnpm-workspace.yamlparsers are not consolidated here — that is step 2, filed as #11510. This PR is what makes step 2 safe: doing 2 without 1 is a regression with no red gate.The mechanism
resolveCheckToFilesreads a family's script paths out of the npm script's command string, anddiscoverFamiliesscanned exactly those files. A module a gateimports was never opened, so a population declaration moved out of a gate and into a shared module stopped contributing hints to every gate that imports it — with every gate still green and nothing in the output saying so.firstPartyImportTargets(scriptPath, source)resolves a gate script's relative static import specifiers one level down, inside the repo's ownscripts/tree, never intonode_modules, and never into a module that is itself a discovered gate file.discoverFamiliesappends the hints of those modules after every own hint, and records which module contributed each one inentry.hintOrigin, whichcoveringKeyprints in theviacolumn:The measurement — the deliverable, not the code
Corpus: 140 discovered families × 6460 tracked files, swept before and after on this branch's tree, the way #9626 and #10540 did.
viachanged)undeterminedbucketFor scale,
hintCovers' docblock prices the bare-top-level-word admission it refuses at +139084 pairs on the same corpus. Following imports is 0.6% of that trade.Per-family matched counts, every family that moved:
check:i18nscripts/cli-build-prerequisite.mjs(packages/cli,packages/cli/bin/run.js)check:i18n-coveragecheck:merge-driverscripts/regen-artifacts.mjs— 20 hints, the artifacts the driver regeneratescheck:adr-anchorsscripts/adr-anchors.mjs(scripts/adr-anchors)check:slot-lookupscripts/eslint-fatal-guard.mjs,scripts/eslint-stack-headroom.mjscheck:query-options-erasureZero families gained or lost from the discovered set, and the
undeterminedbucket is unmoved — every family that gained already had hints of its own, so none left the honest bucket.Every narrowing is a measurement, not a preference
import()invoked-as.mjs,js-comment-mask.mjs,ts-parse.mjs) declares no path literal at all. One level is the mandate and the measured fixpoint.scripts/check:slot-lookupandcheck:query-options-erasureboth import../eslint.config.mjs, whose globs describe what the lint reads, not what either ratchet reads. Cost of the refusal today: zero — the only two family scripts outsidescripts/(packages/lint/scripts/*.mjs) carry no relative first-party import.node_modules(workspace links included); an installed dependency is not a repo source input.check:examples-live-importsinheriting the repo-wideCROSS_PACKAGE_TEST_INPUTSdeclaration table from a gate it imports one string helper (globToRegExp) from. That is a fabricated lead in the column a dispatch prompt pastes — refused on provenance, the way #9964 refused an admission worth 17 pairs because 8 of them were fabricated.The gate-module refusal costs recall in one direction and the PR states it rather than hiding it: where an importer really does read the imported gate's population (
check-ci-filter-parity.mjsimportsCROSS_PACKAGE_TEST_INPUTSitself, +595 pairs), that lead is now missing. A missing lead costs one card one CI round; a fabricated one is pasted into every prompt whose surface brushes it. The card is not blind either way — the imported gate's own family is discovered too and already matches those paths.The assertion PR #11189 pinned
The card is right that this change makes it false by design. It is inverted, not deleted: the same section now asserts that imports are followed, exactly one level, and never into a gate module — so a later author still measures the constraint instead of trusting a paragraph.
scripts/check-adr-links.mjs, importinginvoked-as.mjs, which declares no path at all — is not among the 3. A pin whose specimen is chosen by iteration order can be true of the tree and silent about the rule. Every replacement case therefore names its specimen with a count.Reverse verification
Three ablations, each proving the mutation landed on disk with anchored single-line greps (never an editor's exit code), each restored from the commit by an
EXIT INT TERMtrap:Reported honestly: under ablation 1 the reconstruction case stays green, because it recomputes the expected set from the same (ablated) function — it pins what
discoverFamiliesdoes with the follow, not the follow itself, which is why the fixture cases sit beside it. And under ablation 2 the reconstruction also stays green, because today's depth-2 modules declare nothing — which is exactly why the depth bound is pinned onentry.importsrather than on the hint set.Verification
All at
538517ab, exit codes captured before any pipe, each gate quoted by its own verdict line:pnpm check:pm-dispatch-gates→✓ dispatch-gates self-test: 564 cases pass.(557 before; +9 cases, −2 replaced)pnpm check:cross-package-test-inputs→OK: 14 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.pnpm check:entry-guard→✓ check:entry-guard: 139 scripts/ file(s) — every entry guard goes through invoked-as.mjs; …pnpm check:parse-guard→✓ check:parse-guard: 138 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.pnpm check:pnpm-filter-targets→✓ check:pnpm-filter-targets: 120/148 --filter occurrence(s) across 25 file(s) resolve against 78 workspace package(s); …node scripts/check-ci-filter-parity.mjs→OK: all 89 declared cross-package glob(s) (77 unique) are covered by core or crosspkg, …pnpm check:nul-bytes→check-nul-bytes: OK (scanned 6455 text file(s) …; no raw ASCII control bytes).Gate set derived, not recalled:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths — off the merge base) named exactly these seven;check:nul-byteswas added on top for the any-edit rule.Declared narrowing — lint. The repo-wide
eslint . --no-inline-configrun is CI's; here it is narrowed to the one changed file, and the narrowing is measured rather than asserted: (1) the population comes from eslint's own configuration, which this repo runs as a singleeslint.config.mjsthat never enables type-aware linting for any file (noparserOptions.project, no typed rules — measured in that file's own header with a positive control), so a one-file diff cannot move an untouched file's verdict; (2) the count is read from--format json: 1 file linted, 0 errors, 0 warnings, exit 0; (3)eslint.config.mjsimports nothing fromscripts/, so this diff cannot change the config or the linted set.No changeset: this PR touches one internal PM tool (
scripts/pm/dispatch-gates.mjs) and publishes nothing. Labelledskip-changeset.Adjacent, checked and NOT touched
The card's
check:cross-package-test-inputsdouble discovery (pnpm check:xandnode scripts/check-x.mjskeyed separately) is unchanged by this PR, measured: neither family gains a single hint, because that script's only first-party import isinvoked-as.mjs, which declares nothing. It stays on #11199.One finding the sweep produced, filed as #11511 and not fixed here:
check:ci-filter-parityimports theCROSS_PACKAGE_TEST_INPUTStable it genuinely reads out of a gate script, so the gate-module refusal costs it a real 595-pair lead. The remedy is the same module shape #11510 needs.Re-derived on today's tree, since both earlier figures were dated: 15 families across 13 distinct scripts name
pnpm-workspace.yaml(#10542 measured twelve at 119 families; the card measured fifteen at 137; the farm is 140 today).Generated by Claude Code