Skip to content

fix(pm): decide an inherited population per CALLER, off what the import binds - #18157

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-17991-inherited-population-value-import
Sep 14, 2026
Merged

fix(pm): decide an inherited population per CALLER, off what the import binds#18157
claude[bot] merged 1 commit into
mainfrom
claude/issue-17991-inherited-population-value-import

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #17991

inherited-population is keyed on the MODULE, while whether a contribution is fabricated is a property of the CALLER. scripts/pm/check-clause2-carriers.mjs imports one exported string constant (CONTRACT_REVIEW_TIER) from scripts/pm/dispatch-gates.mjs and thereby inherited that module's whole declared watch surface, so every card touching .github/workflows/** was told to run pnpm check:pm-clause2-carriers — a gate that never opens that directory. No per-module declaration can express the refusal, because the same module's globs ARE a real population for check:pm-widening-tells, which binds and reads them.

What changed

One file: scripts/pm/dispatch-gates.mjs.

  • firstPartyImportBindings is the resolver firstPartyImportTargets already was, carrying the one thing that function dropped: WHAT the importer binds from each module it reaches. The resolution, its refusals and its sort order are unchanged to the byte — firstPartyImportTargets is now a keys() of it, and the whole-tree derivation was proved identical across that refactor before any rule was added.
  • exportedValueConstant reads the exporter half: is NAME declared in that module as a const initialised to one primitive literal? It is read in ONE direction only (a positive answer removes leads), so every shape it cannot read answers negative — an array, an object, a function, a class, a computed or multi-line initialiser, a re-exported binding and an undeclared name are all "not a value".
  • importBindsNoPopulation is the per-caller rule: an importer binding only value constants reaches none of the module's reads and inherits nothing; a table, a function, a class, a namespace, a default, a re-export and every clause this reading cannot parse inherit exactly what they did. One exception, and it is a live specimen rather than a hypothetical: a string constant whose own text is one of the module's paths stays population-bearing — scripts/adr-anchors.mjs exports its anchor directory that way, and binding a one-path population is reading it.
  • discoverFamilies keeps entry.imports whole (the edge is real) and records the contributing subset as entry.populationImports, unioned across the family's files.

The two docblocks the card quotes are extended in place rather than a new marker being added: declaredInheritedPopulation now states what it is keyed on and what reaches the caller instead, and the follow's docblock states that reaching a module and inheriting from it are two questions. #11556's semantics for table importers are untouched, and .github/workflows/** is untouched.

Acceptance

All readings on this branch at 1b75a853a0, base 295eae57d.

The card's ablation, reproduced on the committed base (mutate, measure, restore, restore proved by blob hash e59aaff8e0698af16cc20bd2128b81b6bc211c5a with git status --porcelain empty):

base, import present -> check:pm-clause2-carriers hints: [".github/workflows"]
base, import removed -> check:pm-clause2-carriers hints: []

And reversed on the head — the same ablation, same restore proof, now with no effect at all:

head, import present -> check:pm-clause2-carriers hints: []   (imports: ["scripts/pm/dispatch-gates.mjs"])
head, import removed -> check:pm-clause2-carriers hints: []   (imports: [])

The derivation a dispatch prompt pastes. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack .github/workflows/lint.yml, base against head, is exactly one line shorter:

26d25
< pnpm check:pm-clause2-carriers

and for scripts/pm/check-clause2-carriers.mjs the two command lists are byte-identical — the gate's own identity key is untouched, and a card editing the carrier checker still runs it.

The control population (check:pm-widening-tells) still inherits. It binds SUSPECT_TIER_GLOBS (a table) and hintCovers (a function) from the same module, keeps .github/workflows with hintOrigin scripts/pm/dispatch-gates.mjs, and keeps all 50 of its inherited hints (49 of them from scripts/regen-artifacts.mjs). The derivation's own line, from the self-test:

✓ the answer is a property of the CALLER and not of the module — the live tree has 1 module(s) answering
  BOTH ways (scripts/pm/dispatch-gates.mjs), which is the shape no per-module declaration can express
✓ and the caller that BINDS the population still inherits every path of it

The family-population diff for the whole tree. Every family's hints, imports and hintOrigin dumped before and after:

families with a changed derived population: 1 of 307
CHANGED check:pm-clause2-carriers
  imports  base: ["scripts/pm/dispatch-gates.mjs"]  head: ["scripts/pm/dispatch-gates.mjs"]
  inherit  base: [".github/workflows <= scripts/pm/dispatch-gates.mjs (import)"]  head: []
  hints    base: [".github/workflows"]  head: []

Measured the same way over every live import edge the derivation follows: 1 of 52 edges binds only value constants, and it is the card's. The other four importers of this module are unmoved — check:pm-widening-tells binds a table, and check:declared-population-live, check:watch-hint-literal and the two-hop cases bind functions. Note the card's suggested enumeration (git grep "from './dispatch-gates.mjs'") is incomplete: it misses the ./pm/dispatch-gates.mjs spelling two of those importers use, so the enumeration here is taken from the derivation rather than from the grep.

Gates. Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths, three-dot against merge base 295eae57d, change set = 1 path), every one run in the foreground with $? captured before any pipe, then reconciled:

✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED
  (a DERIVED zero — all 31 recorded an exit code and none of them is 3).

All 31 exited 0, plus two run beyond the union because this change moves them: pnpm check:pm-clause2-carriers and pnpm check:pm-widening-tells, both exit 0. pnpm check:declared-population-live (its self-test and then the live sweep) exits 0 — the #18138 lesson was measured rather than assumed: the wording added here adds no path-shaped literal any family declares, and the whole-tree dump above is the check.

The self-test. pnpm check:pm-dispatch-gates: 1723 cases pass, 0 failures, 0 NOT MEASURED subjects. Eleven t( call sites added and zero removed (git diff counts), so the count strictly increased; 1712 before is arithmetic off that diff, 1723 after is the measured run. The new cases pin both directions on fixtures (the clause reader, the exporter recogniser, the rule) and then the live shape:

✓ the narrowing is NOT vacuous on this tree — 1 of 52 followed import edge(s) bind only value constants
  (check:pm-clause2-carriers -> scripts/pm/dispatch-gates.mjs {CONTRACT_REVIEW_TIER})
✓ and not one of those edges contributes a hint to the family that binds it

The self-test's own reconstruction of a family's hints now models the binding rule too, so a future widening of the recogniser reds there rather than silently agreeing with itself.

Restart-touch

Hold #14290 (pm:on-hold, "dispatch-gates: STAGE-THEN-RUN reaches a program by an edge neither follow traverses") carries a Restart-touch: on scripts/pm/dispatch-gates.mjs, so this landing fires its re-verification. This change does not touch STAGE-THEN-RUN edges: it changes only what an IMPORT edge contributes, and leaves spawnedProgramTargets, readProgramTargetsInSource and packageManifestTargets — the follows #14290 is about — untouched in both traversal and population.

Changeset

None. scripts/pm/** publishes nothing — the skip-changeset label is applied.

Acceptance notes

  • Observed, not filed: a card editing scripts/pm/dispatch-gates.mjs does not derive check:pm-clause2-carriers today either, measured on the base — the import edge's only contribution to that family was the fabricated workflow lead. An IDENTITY key over import edges is deliberately refused ([finding] dispatch-gates never names a family for an edit to a first-party module its gate script IMPORTS — 228 (family, module) pairs unreached, measured #13126, priced in the derivation's own self-test), so this is a recorded disposition rather than a gap this PR opens. Successor: none.
  • Observed, not filed: entry.selfTest is still read off the WORKFLOW argv while a --self-test in a package.json script body is invisible to it, so the cheaper guard beside the fix stays silent for every pnpm check:* family whose self-test-only-ness is spelled in the manifest. The card offers it as a separable shape and the machinery for it already exists next door (selfTestOnlyInvocation resolves a row one hop through the manifest that defines it). It is left out here because it would move other families' derivation, which is the one thing this PR's blast radius is pinned against. Successor: the card, which stays open on this half if the seat wants it.

Generated by Claude Code

…rt binds

`inherited-population` is keyed on the MODULE, while whether a contribution
is fabricated is a property of the CALLER: a gate that binds one exported
string constant out of `scripts/pm/dispatch-gates.mjs` inherited that
module's whole declared watch surface and was told to run for every card
touching the workflow directory it never opens.

`firstPartyImportBindings` carries the binding beside each resolved target
(the resolution and its order are unchanged to the byte), and
`importBindsNoPopulation` reads it: an importer binding only primitive value
constants reaches none of the module's reads and inherits nothing, while a
table, a function, a class, a namespace, a default, a re-export and every
clause this reading cannot parse inherit exactly what they did. A string
constant whose own text is one of the module's paths stays population-
bearing — binding a one-path population is reading it.

Measured over the live tree: 1 of 52 followed import edges answers inert,
and exactly 1 of 307 families' derived population moves.

Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
Co-authored-by: Claude <noreply@anthropic.com>
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 2433/2444, then CONTRACT_REVIEW_TIER
Head-sha: 1b75a853a0fac468f69e1b6e0ef28bbae0bdd213

In-seat record (non-governed surface: scripts/pm/dispatch-gates.mjs only, one file, +390/−20 against merge base 295eae57d), domain:skills execution PM seat, 2026-09-14T08:17Z. Record spelled from node scripts/pm/check-clause2-carriers.mjs --template on origin/main. The dev's os-dev-report is comment 5660863440 on #17991; every judgment below is the seat's own reading of the diff and of commands the seat ran in its own worktrees (wt-18157 checked out at the head, wt-main-full at origin/main), not an adoption of the report.

① Derived judgments

  • Resolution unchanged, inheritance re-keyed per caller. firstPartyImportTargets is now the keys() of firstPartyImportBindings; the comparator (a < b ? -1 : a > b ? 1 : 0) on string keys is the default string sort the old [...targets].sort() used, so the follow order is unmoved. IMPORT_FROM_SPECIFIER regrouped to (import|export)(clause)(quote)(specifier) with the backreference moved to \3; its single consumer is :5384 and SIDE_EFFECT_IMPORT is untouched.
  • Both new readers refuse toward inheritance. clauseBinding answers whole for namespace, default, mixed, re-export, side-effect and any braced entry that is not an identifier; exportedValueConstant answers null for arrays, objects, functions, classes, computed or wrapped initialisers, re-exported names, template literals, trailing comments and undeclared names. A positive answer is the only lead-removing direction and it is reached only through a fully read shape.
  • The card's fabrication is gone and the control still inherits, re-derived in the seat's worktrees: --commands --repo objectstack-ai/objectstack .github/workflows/lint.yml on the head derives pnpm check:pm-widening-tells alone; on origin/main it derives pnpm check:pm-clause2-carriers and pnpm check:pm-widening-tells. For scripts/pm/check-clause2-carriers.mjs the head and origin/main command lists are byte-identical (35 lines, diff empty) — the gate's own identity key is untouched.
  • pnpm check:declared-population-live on the head: exit 0 (251 of 307 families declare a path population) — the feat(pm): check-clause2-carriers emits the contract-review record as a copyable template #18138 lesson measured, not assumed.
  • Self-test on the head. In the dev's worktree at 1b75a853a (clean tree, git status --porcelain empty): pnpm check:pm-dispatch-gates exit 0, 1723 cases pass. In the seat's own worktree wt-18157 at the same head (clean tree): node scripts/pm/dispatch-gates.mjs --self-test exit 0, 1723 cases pass, 0 lines (log w14/selftest-18157-seat.log, run finished 2026-09-14T08:15Z).
  • Hold dispatch-gates: STAGE-THEN-RUN reaches a program by an edge neither follow traverses — check:objectui-changeset inherits nothing from scripts/bump-objectui.sh #14290's Restart-touch: scripts/pm/dispatch-gates.mjs (hold comment 5556473243) fires on this landing. The diff names none of spawnedProgramTargets, readProgramTargetsInSource or packageManifestTargets (0 hits in the patch); it changes what an IMPORT edge contributes and nothing about the STAGE-THEN-RUN follows the hold is about. The hold's own restart lines read today: Restart-when: closed objectstack-ai/objectstack#16132[finding] extractWatchHints masks // but not # comments, so a shell target's inherited population is prose — latent until the first family follows a .sh #16132 is closed completed since 2026-09-08T03:40Z; the specimen census on origin/main reads 1 (scripts/objectui-changeset-digest.mjs). The first trigger has fired; that re-verification belongs to the domain:devx lane's unlock scan, not to this PR.

② Semver level

None. scripts/pm/** publishes nothing; skip-changeset is on the PR and no .changeset/* file is in the diff.

③ Boundary flags

  • Governed rules-layer surface: none touched (.claude/**, skills/**, docs/adr/**, AGENTS.md, CLAUDE.md, .claude/settings.json absent from the diff). In-seat record, no needs-user-decision, no reviewer request.
  • Blast radius as measured: 1 of 307 families changes its derived population (check:pm-clause2-carriers loses .github/workflows, inherited origin scripts/pm/dispatch-gates.mjs); 1 of 52 followed import edges is inert and it is the card's; check:pm-widening-tells keeps all 50 inherited hints.
  • One boundary observed, not blocking: importBindsNoPopulation compares a bound string constant's text with the module's hint list by equality, not by glob coverage, so a constant naming a file under a declared glob reads inert. That is the file's declared error direction (a missing lead, never a fabricated one); no live specimen, no successor.
  • The two ## Acceptance notes dispositions in the PR body stand as written: the identity-key refusal is [finding] dispatch-gates never names a family for an edit to a first-party module its gate script IMPORTS — 228 (family, module) pairs unreached, measured #13126's recorded ruling; the manifest-spelled --self-test reading is one of the three shapes the card offered 「⛔ not asserted」, not a residual work item, so Fixes #17991 closes the card whole.

Implemented-by: claude/issue-17991-inherited-population-value-import
Reviewed-by: session_01DAcomhvR9kKizeYgg89Vo8

VERDICT: PASS


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

1 participant