Skip to content

Commit d6ae0d5

Browse files
claude[bot]claude
andauthored
fix(devx): check-declaration-mirrors declares the .d.mts/.mjs population it discovers, so a mirror edit derives it (#15601)
* fix(devx): check-declaration-mirrors declares the .d.mts/.mjs population it discovers, so a mirror edit derives it (#15553) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk * docs(devx): dispatch-gates' probe note reads the landed anchor, not the pre-#14963 one (#15553) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c01b3a6 commit d6ae0d5

2 files changed

Lines changed: 131 additions & 7 deletions

File tree

scripts/check-declaration-mirrors.mjs

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,61 @@ const HERE = dirname(fileURLToPath(import.meta.url));
118118
const REPO_ROOT = resolve(HERE, '..');
119119
const SCRIPTS_DIR = join(REPO_ROOT, 'scripts');
120120

121+
/**
122+
* POPULATION DECLARATION -- the corpus this gate's verdict is ABOUT, in the
123+
* subtree spelling `scripts/pm/dispatch-gates.mjs` compares in.
124+
*
125+
* Nothing here reads it; `mirrorFiles()` and `checkPair()` do. It is declared
126+
* anyway, because the dispatch derivation reads SOURCE TEXT and this gate's
127+
* population is discovered by EXTENSION with no path literal anywhere: the walk
128+
* is rooted at `SCRIPTS_DIR`, whose only spelled component is the bare
129+
* single-segment word `scripts` -- no separator, so `extractWatchHints`
130+
* recovers nothing from it, and `hintCovers` would refuse a bare word anyway.
131+
*
132+
* Measured on `ca46f8f12` before this constant existed, on the exact change set
133+
* the defect was filed from:
134+
*
135+
* dispatch-gates --commands -- scripts/js-comment-mask.d.mts \
136+
* scripts/js-comment-mask.mjs
137+
* -> 31 commands, ZERO of them this gate
138+
*
139+
* So the one class of change this gate exists for -- a hand-written `.d.mts`
140+
* moving out of step with the module it mirrors -- was the class the derivation
141+
* never sent here, and the red arrived in CI a cycle late (#15553; the specimen
142+
* is PR #15532, whose 32 derived commands were all green while this gate went
143+
* red on an arity mismatch).
144+
*
145+
* BOTH SIDES are declared, because either side moving breaks the mirror: the
146+
* declaration side is what `mirrorFiles()` walks, and the module side is what
147+
* `checkPair()` imports to read `Function.length` off. A change set naming only
148+
* `js-comment-mask.mjs` can turn this gate red without touching a `.d.mts` at
149+
* all.
150+
*
151+
* ⛔ NOT a hand list of the four mirror pairs: the corpus is DISCOVERED on
152+
* purpose (see this file's header), and a hint list that enumerated today's
153+
* pairs would go quiet on the fifth exactly as the walk would not.
154+
*
155+
* ## The precision this costs, measured rather than asserted
156+
*
157+
* `scripts/**\/*.d.mts` reaches 4 tracked files and this gate reads all 4 --
158+
* 100% precise. `scripts/**\/*.mjs` reaches 214 and this gate reads 4 of them
159+
* -- 1.9%. That second hint is the price of keeping the module side declared
160+
* without a hand list, and it is small in the only currency that matters here:
161+
* the pair of commands `lint.yml` runs costs ~0.18 s of wall clock, and over
162+
* the 36 open PRs on the day this landed it newly named this gate on 10 of them
163+
* (1 through the `.d.mts` hint, 9 through the `.mjs` one). ~1.6 s of fleet
164+
* compute per 36 cards, against a defect class whose alternative is a CI red a
165+
* cycle late. Under `hintCovers`' recorded ruling -- over-naming is loud and
166+
* self-limiting, under-naming is silent -- that trade runs the right way.
167+
*
168+
* Spelled as a LITERAL array, never computed from the walk's extension test:
169+
* the extractor reads SOURCE TEXT, so a built spelling keeps this value
170+
* identical at runtime, keeps every assertion about it green, and contributes
171+
* ZERO hints. `check-watch-hint-literal.mjs` holds that rule fleet-wide; the
172+
* self-test below holds the own-source half.
173+
*/
174+
const ROOT_DIR_WATCH_HINTS = ['scripts/**/*.d.mts', 'scripts/**/*.mjs'];
175+
121176
/**
122177
* The export spellings this parser recognises, in the words a declaration
123178
* author would write them. Published for the same reason the cross-package
@@ -429,7 +484,7 @@ const SELF_TEST_VERDICT = 'check-declaration-mirrors self-test reached its verdi
429484
// not red. A battery BELOW its floor means cases stopped running; the remedy is
430485
// to find what stopped registering.
431486
const SELF_TEST_BATTERIES = Object.freeze({
432-
'check-declaration-mirrors self-test': 23,
487+
'check-declaration-mirrors self-test': 29,
433488
});
434489

435490
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -603,6 +658,70 @@ async function selfTest() {
603658
mirrorFiles().every((f) => f.endsWith('.d.mts')),
604659
);
605660

661+
// ── the population this gate DECLARES to the dispatch derivation (#15553) ──
662+
//
663+
// The walk above is discovered by EXTENSION and spells no path, so before the
664+
// declaration beside `SCRIPTS_DIR` existed a change set naming a mirror --
665+
// either half of one -- derived every other `scripts/` family and not this
666+
// one. Nothing in THIS file can ENFORCE the declaration: `extractWatchHints`
667+
// and `hintCovers` live in another tool entirely, so a wrong or missing one
668+
// runs green here forever. What CAN be held here are the properties a wrong
669+
// one breaks -- it is a live literal the extractor can read, it is not the
670+
// bare root the consumer refuses or over-names on, and its extensions still
671+
// admit every file the walk really opens, on BOTH sides of a mirror.
672+
const declaredRoots = ROOT_DIR_WATCH_HINTS.map((h) => h.split('/')[0]);
673+
const declaredSuffixes = ROOT_DIR_WATCH_HINTS.map((h) => h.slice(h.lastIndexOf('*') + 1));
674+
const admits = (repoRelative) =>
675+
ROOT_DIR_WATCH_HINTS.some((h, i) =>
676+
repoRelative.split('/')[0] === declaredRoots[i] && repoRelative.endsWith(declaredSuffixes[i]));
677+
ok(
678+
'the gate declares a population at all',
679+
Array.isArray(ROOT_DIR_WATCH_HINTS) && ROOT_DIR_WATCH_HINTS.length > 0,
680+
);
681+
ok(
682+
'every declared hint is multi-segment, so the consumer does not refuse it as a bare word',
683+
ROOT_DIR_WATCH_HINTS.every((h) => h.split('/').filter(Boolean).length > 1),
684+
);
685+
ok(
686+
'and none of them is the bare subtree or the repo root, which would name this gate for every '
687+
+ 'JSON, Markdown and text file the walk never opens',
688+
ROOT_DIR_WATCH_HINTS.every((h) => !h.endsWith('/**') && h !== '.' && h !== '**'),
689+
);
690+
// The card's own point, held against the LIVE walk rather than a fixture: the
691+
// declaration side of every mirror this tree really has is admitted by the
692+
// extensions declared. A fifth pair added tomorrow is walked by `mirrorFiles`
693+
// and covered by the same hint, which is the property a hand list would lose.
694+
ok(
695+
'every declaration the walk discovers is admitted by a declared hint',
696+
mirrorFiles().length > 0
697+
&& mirrorFiles().every((f) => admits(relative(REPO_ROOT, f).split(sep).join('/'))),
698+
);
699+
// And the MODULE side, which `checkPair` imports to read `Function.length`
700+
// off: an arity change there reds this gate with no `.d.mts` edited at all,
701+
// so a declaration naming only the declarations would miss half the class.
702+
ok(
703+
'and so is the module each declaration mirrors, whose arity the gate reads',
704+
mirrorFiles().length > 0
705+
&& mirrorFiles().every((f) =>
706+
admits(relative(REPO_ROOT, f.replace(/\.d\.mts$/, '.mjs')).split(sep).join('/'))),
707+
);
708+
// Read from THIS file's own source, comment-masked and scoped to the
709+
// declaration STATEMENT: a whole-file search finds the spellings the docblock
710+
// above writes and passes on a computed declaration, which is the one
711+
// spelling that keeps every case above green while contributing ZERO hints.
712+
const ownDeclaration = (() => {
713+
const code = maskComments(readFileSync(fileURLToPath(import.meta.url), 'utf8'));
714+
const sites = [...code.matchAll(/\bconst\s+ROOT_DIR_WATCH_HINTS\s*=\s*([^;]*);/g)];
715+
return sites.length === 1 ? sites[0][1] : null;
716+
})();
717+
ok(
718+
'the declaration is ONE literal array of quoted strings — a computed spelling keeps this value '
719+
+ 'identical at runtime and contributes ZERO hints to the derivation',
720+
ownDeclaration !== null
721+
&& ROOT_DIR_WATCH_HINTS.every((h) => ownDeclaration.includes(`'${h}'`))
722+
&& /^\s*\[\s*(?:'[^'\\]*'\s*,\s*)*'[^'\\]*'\s*,?\s*\]\s*$/.test(ownDeclaration),
723+
);
724+
606725
// ── The floor: every declared battery RAN, and ran its cases (#13489) ────
607726
//
608727
// Evaluated after every battery has had its chance and BEFORE the verdict, so

scripts/pm/dispatch-gates.mjs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11805,12 +11805,17 @@ export function bannerLines({ identity, paths = [], drift = null }) {
1180511805
* "1288 cases pass" to zero bytes of output and exit 0 — a self-test that never
1180611806
* finished, reported as one that passed.
1180711807
*
11808-
* The mechanical probe in `scripts/measure-self-test-floor.mjs` cannot read this
11809-
* file (its anchor matches the first `function selfTest() {` in the source, which
11810-
* here is a FIXTURE STRING, so the injection lands inside a template literal and
11811-
* only ever produces a SyntaxError). That is a limit of the instrument, not a
11812-
* property of this file, and it is why the entry is hand-read there. Anchoring an
11813-
* early return on the real definition below measures it in one run.
11808+
* The mechanical probe in `scripts/measure-self-test-floor.mjs` READS this file
11809+
* since #14963: its anchor is taken over a comment-and-literal MASK and must
11810+
* begin a line, so it lands on the real definition below rather than on either
11811+
* decoy ahead of it — that phrase quoted in a docblock, then a FIXTURE STRING.
11812+
* Injected there the copy PARSES and RUNS (re-measured on the merge base of
11813+
* this change: exit 1, `selfTest() returned without reaching its verdict`),
11814+
* where the unmasked anchor could only ever produce a SyntaxError. The entry is
11815+
* still hand-read in `ENTRY_BY_HAND` — four self-test-shaped names stand in raw
11816+
* source — and the NOT MEASURED its row keeps is the probe's own artefact
11817+
* (#15515: it writes the copy under `scripts/`, where a single-site sweep
11818+
* refuses the near-duplicate), not a property of this file.
1181411819
*/
1181511820
/**
1181611821
* The lines ONE self-test case prints — a pure renderer, so both directions can

0 commit comments

Comments
 (0)