Skip to content

Commit 5f8e2b4

Browse files
committed
fix(pm): declare the runtime-enumerated gate populations dispatch-gates could never name (#10542)
Seven of the discovered gate families compute their population at RUNTIME by parsing pnpm-workspace.yaml, so they spell no path literal and the dispatch derivation named them for no card in the tree. Re-measured on current main the class is 8 unrescued families across 137 discovered, not the 5 of 119 the card recorded — the instrument moved when the tree did. Read each one against its source rather than declaring a blanket population, and the readings disagree with the card in one place that matters: check:published-files DECLARES the workspace globs. It walks and judges every non-build file of every publishable member — 4803 of the 5263 files the declaration names, 91.3%. Reconciled against the live parse in its self-test, so a workspace root that moves fails here. check:published-readme-exports REFUSES the same declaration, against the card's own triage. It narrows the same walk to published markdown and manifests: 149 of 5263, 2.8% — the `filtered` shape check-examples-live-imports refuses by name at 1.6%. Recorded with the measurement and pinned, so a later author meets an assertion. check:pnpm-filter-targets DECLARES scripts/** (228 of 235, 97.0%); the per-member manifest half has no truthful spelling and stays a documented blind spot. check:cross-package-test-inputs moves to CHANGE_KIND_GATES instead: its judged population IS the test-file kind, and a subtree declaration would name it at 49.6%. check:release-body declares no path population, with a reason. check:prerelease-pins likewise — its population is one repo-root file its own workflow already names in `paths:`, so the CI-trigger key reaches it. check:dev-prereqs needs nothing: its one hint is honest. The instrument grows the half that made the count unreadable: a gate may carry `dispatch-gates: no-path-population -- <reason>` in its own source, mirroring the workflow-level marker one level down, and the residue reports those apart from the families nobody has examined. A declaration is not an escape from having a population, and the self-test holds that line against the live tree. The shared workspace-enumerator consolidation the card proposed is NOT done, and the reason is now an assertion rather than a paragraph: resolveCheckToFiles reads the script paths out of the npm script's COMMAND STRING, so a module a gate imports is never opened. Moving a population declaration into a shared enumerator would DELETE it from every gate that imports it — undoing #10540's declarations and this card's. The consolidation is blocked on teaching the derivation to follow first-party imports, not on the gates. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 584fe2e commit 5f8e2b4

7 files changed

Lines changed: 522 additions & 22 deletions

scripts/check-dev-prereqs.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
#!/usr/bin/env node
22
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3+
//
4+
// Note on gate derivation (#10542), because this file looks like it needs a
5+
// population declaration and does not. It enumerates the workspace at runtime
6+
// out of pnpm-workspace.yaml, which is the shape that card is about — but two
7+
// facts make a declaration the wrong move here, and both were read off the
8+
// source rather than assumed:
9+
//
10+
// - lint.yml runs `--self-test` ONLY, and says so in the step name. The
11+
// EXISTENCE and FRESHNESS scans below are for `pnpm dev`, a local preflight
12+
// no pull request schedules. A workspace-wide declaration would name this
13+
// gate for every packages/ card in the tree for a scan CI never runs.
14+
// - the one hint this file does contribute is honest and load-bearing: the
15+
// FRESHNESS half really does read the spec package's dist, so a card there
16+
// names this gate for a read it genuinely performs. It scores `silent` for
17+
// cards elsewhere, and that is the correct verdict rather than a blind spot.
18+
//
19+
// So there is nothing to declare and no marker to carry: a `no-path-population`
20+
// declaration would contradict the hint above, and dispatch-gates' self-test
21+
// asserts exactly that pair cannot coexist.
322

423
/**
524
* check:dev-prereqs — confirm the workspace is BUILT, and that the one artifact

scripts/check-pnpm-filter-targets.mjs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,44 @@ const JS_EXTENSIONS = ['.mjs', '.mts', '.cjs', '.js', '.ts'];
110110
const HASH_COMMENT_EXTENSIONS = ['.sh', '.bash', '.yml', '.yaml'];
111111
const SCANNED_EXTENSIONS = [...JS_EXTENSIONS, ...HASH_COMMENT_EXTENSIONS, '.json'];
112112

113+
/**
114+
* The half of `scannedFiles`' population that `scripts/pm/dispatch-gates.mjs`
115+
* could not see, written in the syntax that derivation CAN read (#10542).
116+
*
117+
* ── The defect this repairs ─────────────────────────────────────────────────
118+
*
119+
* `scannedFiles` walks THREE carriers, and the derivation saw only one of them.
120+
* `.github/workflows/**` is already declared below in a spelling that carries a
121+
* separator, so a workflow card names this gate. The scripts/ walk is spelled
122+
* `join(root, 'scripts')` — a bare single-segment word, which `extractWatchHints`
123+
* drops before `hintCovers` is ever consulted — so a card editing any script in
124+
* the tree named this gate NOWHERE, including the cards most likely to add the
125+
* very `--filter` spelling it exists to judge.
126+
*
127+
* ── Why `scripts/**` is honest here, with the measurement ───────────────────
128+
*
129+
* This is the `subtree` case: the walk descends the whole of scripts/ and every
130+
* file carrying a scanned extension is judged. Measured on this tree, the
131+
* declaration names 235 tracked files under scripts/ and this gate reads 228 of
132+
* them — 97.0%. The 7 it skips are the non-code files the extension filter
133+
* drops, not a subtree it never opens.
134+
*
135+
* ── Why the workspace manifests stay UNDECLARED ─────────────────────────────
136+
*
137+
* `scannedFiles` also reads every workspace member's `package.json` — a real
138+
* read, and one this declaration deliberately does not reach. The instrument
139+
* cannot express it: a root hint covers a whole SUBTREE, so declaring the
140+
* workspace globs (the shape check-published-files.mjs legitimately takes,
141+
* because it walks every file of every member) would name this gate for all
142+
* 5263 tracked files under packages/, apps/ and examples/ in order to reach the
143+
* ~78 manifests it actually opens — 1.5% precision, pasted into every card
144+
* whose surface brushes a package. `hintCovers`' docblock prices a fabricated
145+
* lead above a missing one, so the manifest half stays a documented blind spot
146+
* rather than a wholesale claim. The refusal is pinned below, so a later author
147+
* who adds the workspace globs meets an assertion instead of this paragraph.
148+
*/
149+
const ROOT_DIR_WATCH_HINTS = ['scripts/**'];
150+
113151
/**
114152
* Blank the regions whose `--filter` spellings are prose rather than commands.
115153
*
@@ -455,6 +493,33 @@ export function selfTest() {
455493
),
456494
);
457495

496+
// ---- the dispatch-gates declaration (#10542) -----------------------------
497+
//
498+
// Enforcement cannot hold any of these: ROOT_DIR_WATCH_HINTS is read by
499+
// another tool entirely, so a wrong or stale one runs green here forever and
500+
// pays itself out as a dev dispatched on a scripts/ card with this gate
501+
// missing from the brief. Reconciled against the LIVE sweep rather than
502+
// re-spelled, so a carrier that moves cannot leave the declaration behind.
503+
const sweptRoots = new Set(
504+
scannedFiles(root).map((f) => f.split('/')[0]).filter((s) => s.length > 0),
505+
);
506+
ok(
507+
'the declared subtree is one this gate really walks',
508+
ROOT_DIR_WATCH_HINTS.every((h) => sweptRoots.has(h.replace(/\/\*+$/, ''))),
509+
);
510+
ok(
511+
'scripts/ is declared in the subtree spelling (hintCovers refuses the bare word, so a tidy-up back to a directory name re-opens the blind spot silently)',
512+
ROOT_DIR_WATCH_HINTS.includes('scripts/**'),
513+
);
514+
ok(
515+
'every declared entry carries a path separator',
516+
ROOT_DIR_WATCH_HINTS.every((h) => h.includes('/')),
517+
);
518+
ok(
519+
'the workspace globs stay UNDECLARED (they would name 5263 files to reach ~78 manifests — the measurement is in the docblock)',
520+
!ROOT_DIR_WATCH_HINTS.some((h) => /^(packages|apps|examples)(\/|$)/.test(h)),
521+
);
522+
458523
if (failures.length === 0) {
459524
console.log(
460525
`✓ check-pnpm-filter-targets --self-test: ${checked} assertions — a dead filter observed RED in all `

scripts/check-prerelease-pin-watch.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ import { dirname, join, resolve } from 'node:path';
114114
import { fileURLToPath } from 'node:url';
115115
import { isEntrypoint } from './invoked-as.mjs';
116116

117+
// This gate's whole population is ONE repo-root file, and the derivation
118+
// already reaches it — through the trigger key, not through a hint. Read from
119+
// the source rather than assumed: `prerelease-pin-watch.yml` declares
120+
// pnpm-workspace.yaml in its `paths:` filter, so a card that edits the pins
121+
// gets this gate named with `CI trigger in prerelease-pin-watch.yml` as its
122+
// provenance. What it carries no hint for is a bare repo-root FILENAME, which
123+
// `hintCovers` refuses as too generic; the subtree-spelling escape would be a
124+
// second, weaker route to a lead the workflow already delivers, and a second
125+
// spelling of one population is what drifts. Nothing else in the tree moves
126+
// this gate: it reads the `overrides` block and npm, and nothing more.
127+
//
128+
// dispatch-gates: no-path-population -- population is pnpm-workspace.yaml alone, and prerelease-pin-watch.yml already names it in `paths:` — the CI-trigger key reaches it, so a hint would be a second spelling of one population
117129
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
118130
const DEFAULT_WORKSPACE = join(REPO_ROOT, 'pnpm-workspace.yaml');
119131
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';

scripts/check-published-files.mjs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,71 @@ function matcher(pattern) {
190190
return (rel) => rx.test(rel) || rel.startsWith(prefix);
191191
}
192192

193+
/**
194+
* This gate's population, written in the syntax `scripts/pm/dispatch-gates.mjs`
195+
* can read (#10542).
196+
*
197+
* ── The defect this repairs ─────────────────────────────────────────────────
198+
*
199+
* The dispatch derivation names a gate for a card by scanning the gate's own
200+
* module body for the path literals it operates on. This gate computes its
201+
* population at RUNTIME instead — `workspaceGlobs()` below parses
202+
* pnpm-workspace.yaml — so it spelled no workspace path literal anywhere, and
203+
* the derivation therefore named it for NO card in the tree. Measured against
204+
* the four layout specimens #10542 uses (a flat package, a nested package, an
205+
* app manifest, an example manifest), `coveringKey` returned null for all four.
206+
*
207+
* That is a strictly worse failure than a gate with a stale hardcoded list: a
208+
* runtime-computed population is invisible rather than wrong, so nothing in
209+
* the output says the gate was ever considered.
210+
*
211+
* ── Why the glob spelling, and why it is not a second source of truth ───────
212+
*
213+
* `hintCovers` refuses a literal with no path separator (`packages`, `apps`,
214+
* `examples`) as too generic — measured, at +139084 fabricated (gate, file)
215+
* pairs if bare top-level words were admitted, because those words are path
216+
* COMPONENTS in dozens of gates that never read the root. The sanctioned escape
217+
* is for a gate to declare its own subtree in a spelling that carries a
218+
* separator, which is what these entries do. They are the workspace globs
219+
* VERBATIM, so the glob collapse reduces each back to the root it names and to
220+
* nothing else.
221+
*
222+
* Nothing in this gate reads this array — `workspaceGlobs()` still parses the
223+
* YAML, and remains the only thing the scan walks. The self-test reconciles the
224+
* two in BOTH directions against that live parse, so a workspace root added to
225+
* or removed from pnpm-workspace.yaml fails here rather than leaving this
226+
* declaration describing a workspace that moved. A declaration that can drift
227+
* from the scan is worse than none: it replaces a silent gate with a lying one.
228+
*
229+
* ── Why the WHOLE workspace is honest here, with the measurement ────────────
230+
*
231+
* This is the `subtree` case, not the `filtered` one check-examples-live-imports
232+
* refuses. `walk()` below enumerates EVERY non-build file of every publishable
233+
* member and MINIMAL judges each of them against FORBIDDEN, so the declaration
234+
* names files this gate really opens. Measured on this tree: the declaration
235+
* names 5263 tracked files and the gate judges 4803 of them — 91.3%. The 460 it
236+
* does not judge are the members whose OWN manifests this gate read in order to
237+
* exclude them (`private`), which is itself a read of the declared subtree, so
238+
* a manifest card there is a true lead rather than a fabricated one.
239+
*
240+
* The contrast that sets the boundary is in check-published-readme-exports.mjs,
241+
* which enumerates the same members and scores 2.8% — its refusal docblock
242+
* carries that measurement and declines the same declaration.
243+
*/
244+
const ROOT_DIR_WATCH_HINTS = [
245+
'packages/*',
246+
'packages/adapters/*',
247+
'packages/apps/*',
248+
'packages/connectors/*',
249+
'packages/drivers/*',
250+
'packages/plugins/*',
251+
'packages/qa/*',
252+
'packages/services/*',
253+
'packages/triggers/*',
254+
'apps/*',
255+
'examples/*',
256+
];
257+
193258
/**
194259
* The `packages:` globs from pnpm-workspace.yaml. Blank lines and comments are
195260
* skipped rather than treated as the end of the list: stopping early would drop
@@ -330,14 +395,49 @@ function selfTest() {
330395
failures.push(`FORBIDDEN("${path}") === ${actual}, expected ${expected}`);
331396
}
332397
}
398+
399+
// ── the dispatch-gates declaration (#10542) ───────────────────────────────
400+
//
401+
// Enforcement cannot hold any of these: ROOT_DIR_WATCH_HINTS is read by
402+
// another tool entirely, so a wrong or stale one runs green here forever and
403+
// pays itself out as a dev dispatched on a packaging card with this gate
404+
// missing from the brief. Both directions are reconciled against the LIVE
405+
// parse rather than re-spelled, so a workspace root that moves cannot leave
406+
// the declaration describing the old one.
407+
const declaredRoots = ROOT_DIR_WATCH_HINTS.map((h) => h.replace(/\/\*+$/, ''));
408+
const liveGlobs = workspaceGlobs();
409+
const liveRoots = liveGlobs.map((g) => g.replace(/\/\*+$/, ''));
410+
const declarationCases = [
411+
[
412+
'every workspace glob this gate walks is declared (a root with no path separator is refused as too generic, so the population needs the glob spelling)',
413+
liveRoots.every((r) => declaredRoots.includes(r)),
414+
],
415+
[
416+
'and it declares no root the workspace does not have (a declaration that can drift from the scan is worse than none — it replaces a silent gate with a lying one)',
417+
declaredRoots.every((r) => liveRoots.includes(r)),
418+
],
419+
[
420+
'every declared entry carries a path separator (the whole point of the spelling: hintCovers refuses a bare top-level word, so a tidy-up back to directory names re-opens the blind spot silently)',
421+
ROOT_DIR_WATCH_HINTS.every((h) => h.includes('/')),
422+
],
423+
[
424+
'no declared entry is the bare root itself (provenance, never a lookup key)',
425+
ROOT_DIR_WATCH_HINTS.every((h) => !liveRoots.includes(h)),
426+
],
427+
];
428+
for (const [name, ok] of declarationCases) {
429+
if (!ok) failures.push(`ROOT_DIR_WATCH_HINTS: ${name}`);
430+
}
431+
333432
if (failures.length > 0) {
334433
console.error(`✗ check:published-files --self-test — ${failures.length} failure(s)\n`);
335434
for (const f of failures) console.error(` ${f}`);
336435
process.exit(1);
337436
}
338437
console.log(
339-
`✓ check:published-files --self-test — ${cases.length} pattern case(s) and ` +
340-
`${forbidden.length} classification case(s).`,
438+
`✓ check:published-files --self-test — ${cases.length} pattern case(s), ` +
439+
`${forbidden.length} classification case(s) and ${declarationCases.length} ` +
440+
`population-declaration case(s) over ${liveGlobs.length} live workspace glob(s).`,
341441
);
342442
}
343443

scripts/check-published-readme-exports.mjs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,60 @@ const WORKSPACE_FILE = 'pnpm-workspace.yaml';
300300
const SELF = 'scripts/check-published-readme-exports.mjs';
301301
const BASELINE_REL = 'scripts/published-readme-exports.baseline.json';
302302

303+
/**
304+
* ⛔ THIS GATE DECLARES NO WORKSPACE POPULATION, DELIBERATELY (#10542).
305+
*
306+
* There is no `ROOT_DIR_WATCH_HINTS` array below, and adding one would be a
307+
* regression rather than a fix. This docblock is that decision, with the
308+
* measurement that made it, because the card that dispatched the work assumed
309+
* the opposite and the source says otherwise.
310+
*
311+
* ── What #10542 expected ────────────────────────────────────────────────────
312+
*
313+
* `scripts/pm/dispatch-gates.mjs` names a gate for a card by scanning the
314+
* gate's module body for path literals. This gate computes its population at
315+
* RUNTIME (it parses pnpm-workspace.yaml, exactly as check-published-files.mjs
316+
* does), so it names no workspace path and the derivation named it for no card.
317+
* #10542 grouped it with check-published-files.mjs and said of the pair that
318+
* they "really do read every published package's manifest, so their population
319+
* genuinely is the workspace and the #10114 escape applies directly".
320+
*
321+
* ── What the source actually does, and the measurement that decides it ──────
322+
*
323+
* The two gates enumerate the same members and then diverge completely.
324+
* check-published-files.mjs JUDGES every non-build file it walks — MINIMAL
325+
* tests each one against FORBIDDEN — so a declaration of the workspace globs
326+
* names files it really opens: 4803 of the 5263 tracked files the declaration
327+
* would name, 91.3%.
328+
*
329+
* This gate walks the same trees and then narrows twice, to published markdown
330+
* and to the manifests: `publishedMarkdown` keeps only `.md` paths the `files`
331+
* whitelist admits, and the rest of the read surface is `<member>/package.json`
332+
* plus the built type entry under `<member>/dist/`. Measured on the same tree,
333+
* the same declaration would name 5263 tracked files to reach 149 — 2.8%.
334+
*
335+
* That is the `filtered` shape check-examples-live-imports.mjs refuses by name
336+
* at 1.6%, and `hintCovers`' docblock prices a fabricated lead above a missing
337+
* one: a `packages/**`-class declaration here would paste this gate into every
338+
* dispatch prompt whose surface brushes any package source file, and 97 of
339+
* every 100 of those leads would name a gate that never opens the file. The
340+
* "22 leads is the same as none" failure, bought at a worse ratio than the
341+
* wholesale admission the derivation already refuses.
342+
*
343+
* ── What this gate is left with, and why that is the honest state ───────────
344+
*
345+
* A card editing this script names it by identity. A card editing a published
346+
* README or a manifest names it by nothing — a real blind spot, and one the
347+
* instrument cannot close: a root hint covers a whole SUBTREE, and there is no
348+
* spelling for "the README of each workspace member". Recorded here rather than
349+
* repaired falsely; if the derivation ever grows a narrower key than a subtree
350+
* root, this is the gate to revisit first.
351+
*
352+
* The refusal is pinned in `--self-test` rather than left in this paragraph, so
353+
* a later author who adds the workspace globs meets an assertion.
354+
*/
355+
const DECLARED_WORKSPACE_POPULATION = [];
356+
303357
// ⛔ SHRINK-ONLY. The authority token the #8435 convention requires; the
304358
// baseline is a maintainer's registry, never an author's escape hatch.
305359
const RATCHET_AUTHORITY_MARKER = '⛔ MAINTAINER-ONLY';
@@ -3400,6 +3454,20 @@ function selfTest() {
34003454
);
34013455
}
34023456

3457+
// The dispatch-gates population refusal (#10542). Enforcement cannot hold
3458+
// this: the declaration is read by another tool entirely, so a wrongly-added
3459+
// one runs green here forever and pays itself out as a fabricated lead in
3460+
// every packages/** dispatch prompt. See DECLARED_WORKSPACE_POPULATION's
3461+
// docblock for the 2.8% measurement that refused it.
3462+
if (DECLARED_WORKSPACE_POPULATION.length !== 0) {
3463+
failures.push(
3464+
'this gate must declare NO workspace population: it narrows the walk to published ' +
3465+
'markdown and manifests, so the workspace globs would name 5263 tracked files to ' +
3466+
'reach 149 (2.8%). check-published-files.mjs walks the same members and judges every ' +
3467+
'file it finds (91.3%), which is why the declaration is honest THERE and not here.',
3468+
);
3469+
}
3470+
34033471
if (failures.length > 0) {
34043472
console.error(`✗ check:published-readme-exports --self-test — ${failures.length} failure(s)\n`);
34053473
for (const f of failures) console.error(` ${f}`);

0 commit comments

Comments
 (0)