Skip to content

Commit 798fdbf

Browse files
claude[bot]claude
andauthored
fix(scripts): let dispatch-gates name check:examples-live-imports for an examples card (#10727)
The dispatch derivation scans a gate's module body for path-ish string literals. This gate's only literal describing its example-app population was the bare word `examples` (from `'examples/'`, trailing slash trimmed), and `hintCovers` refuses a separator-less literal as too generic -- a measured refusal, priced at +139084 fabricated pairs if relaxed. So the gate scored `unreachable BY CONSTRUCTION` for every card in the tree, and no dispatch brief named it, including a brief for an `examples/**` edit -- the one change most likely to break it (#8754). Declare the subtree spelling the derivation CAN read, following PR #10114: - `SCAN_ROOTS` makes the two walked roots data, each carrying WHICH PART of it is the gate's population (`examples` wholesale, `packages` filtered to test files that live-import an example app). - `ROOT_DIR_WATCH_HINTS = ['examples/**']` -- provenance only; nothing in the gate reads it. - The self-test pins the coupling in BOTH directions, derived from SCAN_ROOTS rather than re-spelled, plus the refusal of the filtered root. Measured answer to the card's open half -- the `packages/**` side stays undeclared. It is unreachable too (`'packages'` is dropped before `hintCovers` is consulted, and the gate is not in `CHANGE_KIND_GATES`), but a root hint covers a whole subtree: `packages/**` would name this gate for all 4861 tracked files under `packages/` to reach the 76 that carry a coupling. Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt Co-authored-by: Claude <noreply@anthropic.com>
1 parent fb3b0f7 commit 798fdbf

1 file changed

Lines changed: 133 additions & 2 deletions

File tree

scripts/check-examples-live-imports.mjs

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,107 @@ import { isEntrypoint } from './invoked-as.mjs';
127127
const HERE = dirname(fileURLToPath(import.meta.url));
128128
const REPO_ROOT = resolve(HERE, '..');
129129

130+
const EXAMPLES_ROOT = 'examples';
131+
const PACKAGES_ROOT = 'packages';
132+
133+
/**
134+
* The two trees this gate walks, and WHICH PART of each is its population.
135+
*
136+
* The asymmetry is the whole content of this table, and it is why the roots are
137+
* written as records rather than as a flat list of directory names:
138+
*
139+
* `subtree` -- read WHOLESALE. Every file under an example app is something
140+
* a coupling can target, and `couplingTarget()` resolves to
141+
* arbitrary paths beneath it, so an edit anywhere in the tree
142+
* can move this gate's verdict. 239 tracked files, effectively
143+
* all of them in the population.
144+
*
145+
* `filtered` -- walked, then narrowed twice: to test files
146+
* (`TEST_FILE_RE` / `TEST_DIR_RE`), and then to the handful of
147+
* those that actually live-import an example app. Measured on
148+
* this tree: 76 coupled files across 3 packages
149+
* (`@objectstack/dogfood` 72, `@objectstack/cli` 2,
150+
* `@objectstack/lint` 2) out of 4861 tracked files under
151+
* `packages/`, of which 2507 are test files.
152+
*
153+
* `population` is not decoration: `--self-test` reads it to decide which roots
154+
* MUST appear in ROOT_DIR_WATCH_HINTS below and which must NOT. Both directions
155+
* are derived from this table rather than re-spelled, so renaming a root or
156+
* re-scoping what it contributes cannot leave the declaration describing a
157+
* population the gate stopped walking.
158+
*/
159+
const SCAN_ROOTS = [
160+
{ dir: EXAMPLES_ROOT, population: 'subtree' },
161+
{ dir: PACKAGES_ROOT, population: 'filtered' },
162+
];
163+
164+
/**
165+
* The half of SCAN_ROOTS that `scripts/pm/dispatch-gates.mjs` cannot see,
166+
* written in the syntax that derivation CAN read.
167+
*
168+
* ── The defect this repairs ─────────────────────────────────────────────────
169+
*
170+
* The dispatch derivation scans a gate's module body for path-ish string
171+
* literals. The only literal describing this gate's example-app population was
172+
* the bare single-segment word `examples` (from `'examples/'`, whose trailing
173+
* slash the extractor trims), and `hintCovers` refuses a separator-less literal
174+
* as too generic. So this gate scored, for EVERY card in the tree:
175+
*
176+
* pnpm check:examples-live-imports [lint.yml] dead: 'examples' -- the tree
177+
* HAS it; the covering rule refuses the literal as too generic (no path
178+
* separator)
179+
*
180+
* A gate in that state is named by no dispatch brief -- including a brief for
181+
* the one edit most likely to break it. That is the exact cost shape #8754
182+
* records: an examples-only diff runs neither of CI's scoping layers, and the
183+
* shared merge queue is the first signal.
184+
*
185+
* ── Why the subtree spelling, and not a wider extractor ─────────────────────
186+
*
187+
* The refusal is measured, not incidental, and it is not this file's to relax:
188+
* `hintCovers`' docblock prices teaching the extractor to accept bare top-level
189+
* directory words at +139084 fabricated (gate, file) pairs, precisely because
190+
* `packages`, `apps` and `examples` are path COMPONENTS in dozens of gates that
191+
* never read those roots. A declared subtree is a different claim -- an author
192+
* stating what this gate reads -- and the glob collapse reduces `examples/**`
193+
* back to this root and to nothing else. One gate pays for its own precision
194+
* instead of every gate paying for one gate's.
195+
*
196+
* ── Why `packages/**` is deliberately NOT declared ──────────────────────────
197+
*
198+
* The `packages/**` test-inventory side is unreachable too, and by a wider
199+
* margin: `'packages'` carries no separator either, so `extractWatchHints`
200+
* drops it BEFORE `hintCovers` is ever consulted -- it is not even a dead hint.
201+
* It is also not reached by the test-file convention trigger
202+
* (`CHANGE_KIND_GATES`), which this gate is not listed in. Measured: a
203+
* dispatch derivation for `packages/cli/test/i18n-section-coverage.test.ts`
204+
* -- the very file #8754 went red on -- names this gate nowhere.
205+
*
206+
* It stays undeclared anyway, because the instrument cannot express this side's
207+
* population. A root hint covers a whole subtree, so `packages/**` would name
208+
* this gate for all 4861 tracked files under `packages/` in order to reach the
209+
* 76 that carry a coupling: 1.6% precision, pasted into every `packages/**`
210+
* dispatch prompt. That is the "22 leads is the same as none" failure the
211+
* derivation's header prices a fabricated lead against, bought at a worse ratio
212+
* than the wholesale admission it already refuses. The two sides are also not
213+
* symmetric in what they cost a dev: a `packages/**` test that couples to an
214+
* example app spells `examples/` in its own diff and gets a failure text naming
215+
* itself and what to write, while an example-app edit gets no signal at all.
216+
*
217+
* The refusal is pinned below rather than left in this paragraph, so a later
218+
* author who adds `packages/**` meets an assertion instead of prose.
219+
*
220+
* ── Provenance, never a lookup key ──────────────────────────────────────────
221+
*
222+
* Nothing in this gate reads this array. The glob form appearing in SCAN_ROOTS
223+
* would send `walk()` at a directory that does not exist -- and both walks here
224+
* fail SOFT (`existsSync` guard in `exampleApps`/`testFiles`, a swallowed
225+
* `readdirSync` throw in `walk`), so the mistake would quietly shrink the
226+
* population to nothing while every gate stayed green. The self-test pins that
227+
* apart too.
228+
*/
229+
const ROOT_DIR_WATCH_HINTS = ['examples/**'];
230+
130231
/**
131232
* Every `packages/**` test coupling to `examples/**` that CI's scoping layers
132233
* CANNOT see -- the #8754 population.
@@ -252,7 +353,7 @@ function walk(dir, out = []) {
252353

253354
/** Test files (and the helpers under a test dir that carry couplings into them). */
254355
function testFiles() {
255-
const pkgRoot = join(REPO_ROOT, 'packages');
356+
const pkgRoot = join(REPO_ROOT, PACKAGES_ROOT);
256357
if (!existsSync(pkgRoot)) return [];
257358
return walk(pkgRoot).filter((abs) => {
258359
if (!/\.[mc]?[jt]sx?$/.test(abs)) return false;
@@ -263,7 +364,7 @@ function testFiles() {
263364

264365
/** The example apps, by directory and by published package name. */
265366
function exampleApps() {
266-
const root = join(REPO_ROOT, 'examples');
367+
const root = join(REPO_ROOT, EXAMPLES_ROOT);
267368
const apps = [];
268369
if (!existsSync(root)) return apps;
269370
for (const e of readdirSync(root, { withFileTypes: true })) {
@@ -741,6 +842,36 @@ function selfTest() {
741842
'* does not span segments',
742843
!globCoversTarget('examples/app-showcase/src/*.ts', 'examples/app-showcase/src/ui/x.ts', false),
743844
],
845+
846+
// ── the dispatch-gates declaration (#9964's pattern, #10114's coupling) ──
847+
//
848+
// Enforcement cannot hold any of these: ROOT_DIR_WATCH_HINTS is read by
849+
// another tool entirely, so a wrong or stale one runs green here forever and
850+
// pays itself out as a dev dispatched on an examples card with this gate
851+
// missing from the brief. Both directions are derived from SCAN_ROOTS rather
852+
// than re-spelled, so widening or renaming a root cannot leave the
853+
// declaration describing the old population.
854+
[
855+
'every WHOLESALE root is declared (a root with no path separator is refused as too generic, so it needs the subtree spelling)',
856+
SCAN_ROOTS.filter((r) => r.population === 'subtree' && !r.dir.includes('/')).every((r) =>
857+
ROOT_DIR_WATCH_HINTS.includes(`${r.dir}/**`),
858+
),
859+
],
860+
[
861+
'and it declares no root this gate does not walk (a declaration that can drift from the scan is worse than none -- it replaces a silent gate with a lying one)',
862+
ROOT_DIR_WATCH_HINTS.every((h) => SCAN_ROOTS.some((r) => r.dir === h.replace(/\/\*+$/, ''))),
863+
],
864+
[
865+
'a FILTERED root stays undeclared (packages/** would name this gate for 4861 files to reach 76; the measurement is in the docblock)',
866+
SCAN_ROOTS.filter((r) => r.population !== 'subtree').every(
867+
(r) => !ROOT_DIR_WATCH_HINTS.some((h) => h.replace(/\/\*+$/, '') === r.dir),
868+
),
869+
],
870+
['examples is the root it declares (the #8754 population)', ROOT_DIR_WATCH_HINTS.includes('examples/**')],
871+
[
872+
'the declared form is NOT a SCAN_ROOTS dir (provenance, never a lookup key: the glob form would send the walk at a directory that does not exist)',
873+
!SCAN_ROOTS.some((r) => ROOT_DIR_WATCH_HINTS.includes(r.dir)),
874+
],
744875
];
745876

746877
let failed = 0;

0 commit comments

Comments
 (0)