Skip to content

Move the cross-package glob predicate and declaration table out of the gate that owns them - #11871

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11511-gate-helper-import-source
Aug 24, 2026
Merged

Move the cross-package glob predicate and declaration table out of the gate that owns them#11871
os-steve merged 1 commit into
mainfrom
claude/issue-11511-gate-helper-import-source

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11511

scripts/check-cross-package-test-inputs.mjs was a gate and a library. Two other gate scripts imported out of it, and scripts/pm/dispatch-gates.mjs deliberately never follows into a module that is itself a discovered gate file — so check:ci-filter-parity, whose population that gate's declaration table literally is, derived nothing for a card editing the table.

This moves the two shared halves into plain modules no workflow invokes, the shape scripts/workspace-enumerator.mjs (#11797) and scripts/i18n-bundle-surface.mjs already have. No change to the derivation's follow logic — the honest lead is now reachable through the existing follow.

Two modules, not one — and that is the measurement, not a preference

The card describes one shared module. That does not work, and the reason is the follow's own semantics: it appends a followed module's watch hints to every importer whole, regardless of which binding the importer named. Pairing the pure predicate with the declaration table would hand check:examples-live-imports — which wants one string helper and whose own subject is examples/ — the entire packages/** declaration table, rebuilding the exact fabrication dispatch-gates refuses today, one file further out.

So the split is by kind:

moduleholdspopulation
scripts/glob-match.mjsglobToRegExp, matchesAnynone, pinned against its own bytes
scripts/cross-package-test-inputs.mjsCROSS_PACKAGE_TEST_INPUTSthe declarations, 3253 files

A binding that is a predicate goes in the first; a binding that is a declaration goes in the second; neither file grows the other's kind. Both headers carry their half of the measurement.

Measured — watch-hint (gate, file) pairs, over 157 families × 6605 tracked files

familybeforeafter
scripts/check-ci-filter-parity.mjs13256+3255, the honest lead restored
check:examples-live-imports2412410, the fabricated lead still refused
check:cross-package-test-inputs32533255+2 — the two new files, under the scripts/** it already declared
scripts/check-cross-package-test-inputs.mjs32533255same, via ci.yml

The gate loses nothing: it imports the table, so the follow returns all 103 hints to it, now labelled with the module they came from (gate source via scripts/cross-package-test-inputs.mjs).

The card's 595 figure does not reproduce — it is an undercount

Re-derived rather than restated. The pair definition used here is the one #11190 measured with, confirmed by reproducing its global result on today's tree: the same six families gain, at +907 where that docblock recorded +893 (drift in the two i18n families, +285 → +292 each).

Under that definition the lead check:ci-filter-parity was missing is worth 3255 pairs, not 595. The arithmetic is forced: both importers inherit the same 103-hint set from the same file, so their counterfactual gains cannot differ by 5×. Measured on the base tree, admitting gate-file modules to the follow would gain 3253 for the parity gate and 3105 for the examples gate — the latter matching the docblock's 3065 within drift, the former not matching 595 at all. The card's justification holds; it was understated.

Non-vacuity

  • Behavioural identity of the moved predicate. The pre-move globToRegExp/matchesAny were lifted verbatim out of the base commit into a standalone module and compared against the new one over every declared glob × every tracked file: 81 globs × 6605 files = 535,005 comparisons, 8,404 true matches, 0 mismatches, plus 10 adversarial cases (7 true, 0 mismatches). The true-match count is the positive control — agreement over an all-false corpus would prove nothing.
  • Purity, proved rather than asserted. That lifted file loads and answers with no import of the gate. Had the predicates closed over gate module state it would have thrown. coversDirectory is the contrast: it closes over REPO_ROOT and readdirSync, so it stayed in the gate.
  • The no-population pin can fail. Planting ['packages/**', 'content/docs/**'] in glob-match.mjs turns both importers' --self-test RED naming both literals, and takes check:examples-live-imports from 241 to 5811 pairs — the fabrication the pin exists to stop, observed. Restored byte-identically (sha256 back to c1364e7e, marker count 1 → 0).
  • The dispatch-gates self-test re-point is load-bearing. Its two cases read the hints of whichever file declares the cross-package roots; that file changed. Pointed back at the gate, --self-test fails 2 of 579 — it would have gone green over an empty hint list, the vacuous-pass shape those cases exist to refuse. Restored byte-identically (sha256 back to 428a6e10). The comment above them asks for exactly this ("re-point the case at whatever gate then does"). No follow logic was touched.

Inertness

Both new modules are declarations and functions only — no CLI, no top-level statement that runs anything, per check:entry-guard's second rule (the rule that exists because importing a gate for its exports used to run the gate). check:entry-guard passes with 107 exporting files, 97 inert, its shrink-only known-unsafe ledger untouched.

glob-match.mjs has no check: name of its own by design, so folding its selfTest() into both importers' --self-testis its CI coverage — the same arrangement workspace-enumerator.mjs uses. Its fixtures are assembled from segments rather than spelled, so its own no-population pin (stricter than the real scanner, and therefore only ever able to refuse more) holds against its own bytes.

Verification — all at f9ed46290, on a clean tree

Gate union derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths — from the merge base, working tree included), then every derived family run:

pnpm check:agent-test-spelling exit 0
pnpm check:cross-package-test-inputs exit 0 All 109 self-test cases passed / 16 packages, all declared
pnpm check:entry-guard exit 0 150 files, 107 exporters, 97 inert
pnpm check:examples-live-imports exit 0 All 36 self-test cases passed
pnpm check:parse-guard exit 0
pnpm check:pm-dispatch-gates exit 0 579 cases pass
pnpm check:pnpm-filter-targets exit 0
node scripts/check-ci-filter-parity.mjs exit 0 96 declared globs (81 unique) all covered
... --self-test exit 0 39 assertions
node scripts/check-cross-package-test-inputs.mjs --verify / --self-test exit 0
node scripts/check-nul-bytes.mjs exit 0 6600 files, no raw control bytes
node scripts/check-ratchet-remedy-authority.mjs exit 0 134 scripts swept

Repo-wide lint run in full rather than narrowed: eslint . --no-inline-config, 5030 files, 0 errors, 0 warnings, 52s.

No changeset: this touches scripts/ tooling only and publishes nothing.


Generated by Claude Code

…ion table out of the gate
`scripts/check-cross-package-test-inputs.mjs` was a gate AND a library. Two
other gates imported out of it, and `scripts/pm/dispatch-gates.mjs` cannot
follow a gate file, so `check:ci-filter-parity` -- whose population that gate's
declaration table literally IS -- derived nothing for a card editing the table.
Split the two shared halves into two plain modules that no workflow invokes:
scripts/glob-match.mjs globToRegExp / matchesAny, no population
scripts/cross-package-test-inputs.mjs CROSS_PACKAGE_TEST_INPUTS, the population
Two modules rather than one, on measurement: the follow hands an importer the
whole followed module's hints, so a single module would have handed
check:examples-live-imports the packages-wide declaration table it never opens.
Measured on 589758d, watch-hint (gate, file) pairs:
scripts/check-ci-filter-parity.mjs 1 -> 3256 (+3255, honest)
check:examples-live-imports 241 -> 241 ( 0, refused)
check:cross-package-test-inputs 3253 -> 3255 ( +2, the two new files)
Both new modules are inert on import, per check:entry-guard's second rule.
glob-match.mjs pins "declares no path population" against its own bytes, and
both importers fold that self-test into their own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@os-steveClaude

Copy link
Copy Markdown
Collaborator

ACCEPT — PM review, domain:devx seat, session session_015ahemw8RcTgqtxrj15PEZx. Verified against f9ed46290 (merge-base 589758d22, computed as git merge-base).

This card's first dispatch was lost entirely — no branch, no report. The re-dispatch delivered, and improved the card twice.

The point of the whole card, demonstrated live on both sides

I ran the derivation myself, on each tree:

branch: node scripts/pm/dispatch-gates.mjs --repo … scripts/cross-package-test-inputs.mjs
- node scripts/check-ci-filter-parity.mjs [lint.yml] matched via
scripts/cross-package-test-inputs.mjs ⇢ gate source … 'scripts/**'
main: node scripts/pm/dispatch-gates.mjs --repo … scripts/check-cross-package-test-inputs.mjs
(nothing — the parity gate derives no pair at all)

A card editing the declaration table now derives the gate whose population is that table. Before, it derived nothing. And no derivation rule changed — the table simply stopped living inside a gate, which is what the card said would be sufficient and is now shown to be.

⭐ Two modules, not one — the card's design would have failed

The card prescribes moving both halves into a plain module. You measured why that is wrong: the follow appends a followed module's hints to every importer WHOLE, regardless of which binding it named. So a single module hands check:examples-live-imports the packages-wide table — measured at 241 → 3346, +3105 fabricated, reproducing precisely the harm the #11190 refusal exists to prevent.

Verified independently:

probeglob-match.mjscross-package-test-inputs.mjs
glob/watch-hint literals022 (control)

Zero population in the predicate module is the entire justification for the split, and the control makes that zero a reading rather than a grep that found nothing. Result: parity gate 1 → 3256, examples gate 241 → 241 (unchanged, empty hintOrigin). The honest half restored without the fabricated half — the card's stated goal, reached by a route the card did not describe.

⚠️ The card's headline figure is falsified, and the correction is forced

The card justifies itself on 595 pairs. You measured 3255, and the argument for why 595 cannot be right is arithmetic rather than preference: both importers inherit the same 103-hint set from the same file, so their counterfactual gains cannot differ by 5×. Measured on the base tree, admitting gate-file modules gains 3253 for the parity gate and 3105 for the examples gate — the latter matching the card's own 3065 within drift, the former matching 595 not at all.

And the pair definition was validated before use by reproducing #11190's global result on today's tree (+907 where the docblock recorded +893, the drift traced to the two i18n families). Reproducing the prior measurement with your own instrument before disagreeing with a different one is what makes this a correction rather than a competing number.

The card was understating its own case by 5×.

The dispatch-gates.mjs edit is in bounds — I checked the diff, not the claim

My Zone 1 said: touch the follow logic and stop. You touched the file but not the logic, and the diff confirms it — the change is onlycrossPkgHints re-pointed from the gate to the declaration module, plus one test label reworded. No follow rule moved.

It is also what the surrounding comment explicitly instructs — "if one of these gates stops declaring its root, re-point the case at whatever gate then does; deleting one deletes the evidence" — and you quoted that instruction in the code you added. Leaving it pointed at the gate would have gone green over an empty hint list, which your ablation proves: reverted, --self-test fails 2 of 579.

Evidence quality

The behavioural-identity check carries its own positive control. The pre-move predicates were lifted verbatim from the base commit and compared against the new module over 81 globs × 6605 files = 535,005 comparisons: 8,404 true matches, 0 mismatches, plus 10 adversarial cases. Your note that "agreement over an all-false corpus would be vacuous" is exactly the discipline — 0 mismatches means nothing unless the corpus produced real matches, and you established that it did.

That same lift proves Zone 2 item 3 (purity) as a by-product: the standalone file loads and answers with no import of the gate, which it could not do if the predicates closed over gate module state. One experiment, two results, neither assumed.

coversDirectory was checked and deliberately left in the gate because it closes over REPO_ROOT and readdirSync. Moving everything that looks like a helper would have been the easy mistake.

Ablation 1 plants a population into the no-population module: sha c1364e7e → d6beadbc, marker 0 → 1, insertion anchor still present (proving an insert rather than a clobber), both importers red, examples gate 241 → 5811. Restored byte-identical. That is the pin that keeps the split from silently collapsing back.

check-ratchet-remedy-authority run because its pinned expectation names this gate's remedy registry — and the remedy text now derives DECLARATION_FILE rather than using import.meta.url, which would have pointed authors at the wrong file after the move. That is the kind of second-order breakage a move like this usually ships.

Full repo-wide eslint (5030 files, 0/0), no narrowing claimed.

Flipping to ready; arming once every check run completes green.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 24, 2026 22:19
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit db9e05dAug 24, 2026
32 checks passed
@os-steve
os-steve deleted the claude/issue-11511-gate-helper-import-source branch August 24, 2026 23:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude