Skip to content

Commit 83863b2

Browse files
hotlongclaude
andauthored
fix(check-watch-hint-literal): red on a declared watch-hint literal the extractor drops at admission (#16799)
* wip(check-watch-hint-literal): known-positive self-test case, run against the unfixed implementation Records acceptance item 1's first half: with the case present and the finding class absent, the self-test reports the case as failing -- the drop is not reported by the unfixed checker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY * fix(check-watch-hint-literal): red on a declared literal the hint extractor drops A watch-hint declaration can be a perfect literal array and still contribute nothing: extractWatchHints admits a literal only when it starts with a word character, a dot or an @, so one opening with a glob never becomes a hint and places its gate on no card. Four instruments passed that drop because each asked its own question and none asked whether the literals were admissible. The sweep now asks the second question, of the extractor rather than of a copy of its rule, one literal at a time -- a whole-module diff falsely accuses a literal spelled inside a self-test the extractor blanks, and one written module-relative whose extracted hint is the resolved path. The remedy names the dropped literal, prescribes the enumerable root-prefixed spelling, and carries the +139084-pair measurement behind the refusal so widening admission does not read as the obvious repair. The admission rule itself is untouched. The manifest gate's docblock stops calling this drop silent, because it no longer is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8c7cca1 commit 83863b2

2 files changed

Lines changed: 198 additions & 13 deletions

File tree

scripts/check-manifest-repository-directory.mjs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,10 +306,17 @@ const EXIT_PREREQ = 3;
306306
* retries it: `**\/package.json` is judged correctly by `hintCovers` and is
307307
* never SEEN, because `extractWatchHints` admits a literal only if it starts
308308
* with a word character, a dot or an `@`. A leading glob is dropped at
309-
* admission, so that declaration extracts to zero hints -- the exact silent
310-
* drop `check:watch-hint-literal` exists for, arrived at through the value
311-
* rather than through the spelling. Measured while writing this gate: the
312-
* one-line form extracted `[]`; the four below extract all four.
309+
* admission, so that declaration extracts to zero hints -- the exact drop
310+
* `check:watch-hint-literal` exists for, arrived at through the value rather
311+
* than through the spelling. Measured while writing this gate: the one-line
312+
* form extracted `[]`; the four below extract all four.
313+
*
314+
* ⚠️ That drop was SILENT when this gate was written, and is not any more:
315+
* `check:watch-hint-literal` now puts every declared literal through the
316+
* extractor and reds on one that yields no hint, naming this remedy. So the
317+
* one-line form is refused loudly rather than dropped quietly -- retry it and
318+
* a gate tells you to enumerate, which is the only part of the cost this
319+
* enumeration ever paid twice.
313320
*
314321
* ⛔ NOT the workspace-root spelling `check-published-files.mjs` uses
315322
* (`packages/*`, `apps/*`, …). That declaration is honest THERE -- it walks

scripts/check-watch-hint-literal.mjs

Lines changed: 187 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,69 @@
107107
* masked before the statement is located, through the repo's one comment
108108
* scanner (`scripts/js-comment-mask.mjs`).
109109
*
110+
* ## The SECOND way a declaration is invisible: the VALUE, not the spelling
111+
*
112+
* Everything above is about the SPELLING -- a declaration computed instead of
113+
* written out. A declaration can be a perfect literal array and STILL
114+
* contribute nothing, because `scripts/pm/dispatch-gates.mjs#extractWatchHints`
115+
* admits a literal only when it STARTS with a word character, a dot or an `@`.
116+
* A literal opening with a glob never reaches the resolve step, never becomes a
117+
* hint, and places its gate on NO card:
118+
*
119+
* ['scripts/**'] -> ["scripts/**"]
120+
* ['**\/package.json'] -> [] <- literal, and dropped anyway
121+
*
122+
* Four instruments passed that drop, because each asked its own question and
123+
* none asked whether the literals were ADMISSIBLE: this gate saw a literal
124+
* array, the gate's own self-test saw its own constant,
125+
* `check:declared-population-live` was satisfied by nothing (a family
126+
* declaring zero literals "declares nothing", a legitimate state), and it scored
127+
* `undetermined` or `silent` for every card in the tree. A declaring-zero gate
128+
* and a declared-but-eaten gate are the same colour on all of them.
129+
*
130+
* So the sweep asks the second question too, and it asks it OF THE EXTRACTOR
131+
* rather than of a copy of its rule -- a second copy of an admission regex is
132+
* a thing that drifts, and the drift would be silent in the same direction.
133+
* Each declared literal is put through `extractWatchHints` on its own, and a
134+
* literal that yields NO hint is the finding.
135+
*
136+
* ⛔ Admission is NOT widened to accept a leading glob, and this gate exists
137+
* BECAUSE it is not: that refusal is measured (the admission comment inside
138+
* `extractWatchHints` prices bare-top-level-word admission at +139084
139+
* fabricated pairs, and records a re-measured refusal of the resolved-form
140+
* widening), and relaxing it is a change to every gate in the farm rather than
141+
* a repair to one declaration. The remedy this gate prints is therefore the
142+
* enumerable root-prefixed spelling, never a request to change the rule.
143+
*
144+
* ⭐ What this buys is NOT cheaper enumeration -- the author of a file-kind
145+
* population still has to name every root that holds the kind, and still has to
146+
* pin that enumeration against the gate's own scan. It solves the other half:
147+
* it does not solve the trouble of enumerating, it solves not knowing that you
148+
* need to enumerate.
149+
*
150+
* ## Why ONE literal at a time, and never the module's whole extraction
151+
*
152+
* The tempting spelling is to extract the whole module and diff the declared
153+
* literals against the result. Measured on this tree, that spelling is wrong in
154+
* both directions. It FALSELY accuses: `scripts/pm/dispatch-gates.mjs` spells a
155+
* rostered declaration inside its own self-test as a fixture string, which
156+
* `extractWatchHints` blanks along with the rest of the self-test, so a
157+
* perfectly admissible literal is absent from that module's extraction. And it
158+
* would falsely accuse again on any declaration written module-relative, whose
159+
* extracted hint is the RESOLVED path and not the literal as spelled. Probing
160+
* one literal at a time asks exactly the question the finding is about -- does
161+
* this literal produce a hint at all -- and neither residue reaches it.
162+
*
110163
* ## What is asserted, and what is deliberately NOT
111164
*
112165
* Asserted: the right-hand side of the declaration is an ARRAY OF QUOTED STRING
113-
* LITERALS and nothing else. That is stronger than "each declared hint appears
114-
* quoted inside the statement", and it needs no runtime value, so this gate
115-
* never imports the files it judges -- a gate that imported 14 modules to read
116-
* one constant would run their module bodies to do it.
166+
* LITERALS and nothing else, and every literal in it is one the hint extractor
167+
* admits. The first is stronger than "each declared hint appears quoted inside
168+
* the statement"; neither needs a runtime value, so this gate never imports the
169+
* files it judges -- a gate that imported 14 modules to read one constant would
170+
* run their module bodies to do it. The extractor itself IS imported, and that
171+
* is the opposite case: it is the authority being consulted, not a subject
172+
* being read, and it is a pure string function with no module-scope work.
117173
*
118174
* NOT asserted: that a declaration is CORRECT -- that it names the roots the
119175
* gate really walks, and only those. That claim is local to each gate and each
@@ -147,6 +203,7 @@ import { fileURLToPath } from 'node:url';
147203

148204
import { isEntrypoint } from './invoked-as.mjs';
149205
import { maskComments } from './js-comment-mask.mjs';
206+
import { extractWatchHints } from './pm/dispatch-gates.mjs';
150207

151208
// ── The self-test's own battery roster and floor (#13489) ──────────────────
152209
//
@@ -181,7 +238,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
181238
'the per-name floor': 5,
182239
'discovery of an UNROSTERED spelling of the idiom': 5,
183240
'the empty population is refused, not passed': 1,
184-
'the live tree': 14,
241+
"literals the extractor's admission DROPS": 14,
242+
'the live tree': 15,
185243
});
186244

187245
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -396,6 +454,57 @@ export function literalHints(rhs) {
396454
return rest.trim() === '' ? hints : null;
397455
}
398456

457+
/**
458+
* The hints `extractWatchHints` builds from ONE declared literal, standing
459+
* alone under `name`.
460+
*
461+
* The probe source is a declaration and not a bare string on purpose: the
462+
* extractor's exclusion channel keys on the DECLARATION NAME, so a future
463+
* rostered name that happened to read as an exclusion would drop its literals
464+
* here and be reported LOUDLY rather than judged under a rule that does not
465+
* apply to it.
466+
*
467+
* A literal carrying both quote characters cannot be delimited, and the
468+
* extractor cannot read such a span out of the real source either -- so it
469+
* yields nothing here, which is the same verdict for the same reason.
470+
*/
471+
function admittedHints(name, literal) {
472+
const quote = literal.includes("'") ? (literal.includes('"') ? null : '"') : "'";
473+
if (quote === null) return [];
474+
return extractWatchHints(`const ${name} = [${quote}${literal}${quote}];\n`);
475+
}
476+
477+
/**
478+
* The declared literals that produce NO hint at all -- the value-side drop.
479+
*
480+
* Pure over the list, and split out of `auditSourceName` for the same reason
481+
* `missingNames` is split out of `main`: the self-test drives it directly, and
482+
* the shape it guards against is one a healthy live tree cannot exhibit.
483+
*/
484+
export function droppedByAdmission(name, hints) {
485+
return hints.filter((h) => admittedHints(name, h).length === 0);
486+
}
487+
488+
/**
489+
* What a dropped literal's author is told: what happened, what to write
490+
* instead, and why the rule that dropped it is not the thing being changed.
491+
*/
492+
function admissionRemedy(name, dropped) {
493+
return `the ${name} declaration is a literal array, and ${dropped.length} of its literal(s) build NO `
494+
+ `hint at all: ${dropped.map((h) => JSON.stringify(h)).join(', ')}. The hint extractor admits a `
495+
+ 'literal only when it STARTS with a word character, a dot or an @, so one opening with a glob '
496+
+ 'never reaches the resolve step, never becomes a hint, and places this gate on NO card -- the '
497+
+ 'same silent drop this gate exists for, arrived at through the VALUE instead of the spelling. '
498+
+ 'Remedy: spell the population as one enumerable root-prefixed entry per root that holds the '
499+
+ 'kind, and pin that enumeration against the walk this gate really performs so it cannot go '
500+
+ 'stale quietly. Admission is deliberately NOT widened to accept a leading glob: see the '
501+
+ 'admission comment inside extractWatchHints, which prices bare-top-level-word admission at '
502+
+ '+139084 fabricated pairs and records a re-measured refusal of the resolved-form widening -- '
503+
+ 'relaxing it is a change to every gate in the farm, not a repair to one declaration. This '
504+
+ 'finding does not solve the trouble of enumerating; it solves not knowing that you need to '
505+
+ 'enumerate.';
506+
}
507+
399508
/**
400509
* One file's verdict for ONE rostered name. `null` means the file does not
401510
* declare that name at all -- it mentions it in prose, or reads someone else's.
@@ -426,6 +535,13 @@ export function auditSourceName(rel, source, name) {
426535
if (hints.length === 0) {
427536
return { rel, name, ok: false, why: `the ${name} declaration is EMPTY -- it names no subtree at all` };
428537
}
538+
// The value-side question, asked only once the spelling-side one has passed:
539+
// an empty or computed declaration has no literals to judge, and naming the
540+
// same declaration under two remedies would leave its author choosing.
541+
const dropped = droppedByAdmission(name, hints);
542+
if (dropped.length > 0) {
543+
return { rel, name, ok: false, why: admissionRemedy(name, dropped) };
544+
}
429545
return { rel, name, ok: true, hints };
430546
}
431547

@@ -530,18 +646,21 @@ function main() {
530646
if (bad.length) {
531647
console.error(
532648
`✗ check-watch-hint-literal: ${bad.length} of ${rows.length} declaration(s) are not readable `
533-
+ 'as literals. Spell the hints inside the declaration statement.',
649+
+ 'by the hint extractor. Spell the hints inside the declaration statement, and spell each one '
650+
+ 'so the extractor admits it.',
534651
);
535652
return 1;
536653
}
537654

538655
const perName = DECL_NAMES
539656
.map((n) => `${n} ${rows.filter((r) => r.name === n).length}`)
540657
.join(', ');
658+
const literals = rows.reduce((n, r) => n + r.hints.length, 0);
541659
console.log(
542660
`✓ check-watch-hint-literal: ${rows.length} declaration(s) across ${DECL_NAMES.length} rostered `
543-
+ `name(s) -- ${perName} -- every one an array of quoted literals inside its own statement, `
544-
+ 'every rostered name non-empty, and no unrostered spelling of the idiom in the tree.',
661+
+ `name(s) -- ${perName} -- every one an array of quoted literals inside its own statement, all `
662+
+ `${literals} of those literals admitted by the hint extractor, every rostered name non-empty, `
663+
+ 'and no unrostered spelling of the idiom in the tree.',
545664
);
546665
return 0;
547666
}
@@ -686,6 +805,60 @@ export function selfTest() {
686805
t('an empty file list produces no rows, which the floor above refuses',
687806
audit([]).rows.length === 0 && missingNames(audit([]).rows).length === DECL_NAMES.length);
688807

808+
// -- literals the extractor's admission DROPS ------------------------------
809+
// ⭐ The value-side drop. Every case here declares a perfect literal array,
810+
// so every one of them passes the spelling-side question above; what
811+
// separates them is whether the hint extractor can make a hint out of what
812+
// is inside it.
813+
battery("literals the extractor's admission DROPS");
814+
const KIND_ONE_LINER = "['**/package.json']";
815+
t('a declaration whose only literal opens with a glob is rejected',
816+
rejected(decl(KIND_ONE_LINER)));
817+
t('...and it is rejected on the VALUE, not on the spelling -- the declaration IS a literal array',
818+
literalHints(KIND_ONE_LINER) !== null && literalHints(KIND_ONE_LINER).length === 1);
819+
const globWhy = verdict(decl(KIND_ONE_LINER))?.why ?? '';
820+
t('the remedy names the literal that was dropped', globWhy.includes('**/package.json'));
821+
t('the remedy states the admission rule that dropped it',
822+
globWhy.includes('STARTS with a word character, a dot or an @'));
823+
t('the remedy prescribes the enumerable root-prefixed spelling rather than only refusing',
824+
globWhy.includes('enumerable root-prefixed entry per root that holds the kind'));
825+
t('the remedy carries the measurement behind the refusal, so widening admission is not the obvious read',
826+
globWhy.includes('+139084') && globWhy.includes('every gate in the farm'));
827+
t('the remedy says what the finding is actually for',
828+
globWhy.includes('it solves not knowing that you need to enumerate'));
829+
830+
// The negatives PR #16446 shipped: the same population, enumerated.
831+
const ADMISSIBLE_KIND = [
832+
'packages/**/package.json',
833+
'apps/**/package.json',
834+
'examples/**/package.json',
835+
'package.json/**',
836+
];
837+
t('the enumerated spelling of the same population is accepted',
838+
accepted(decl(`[${ADMISSIBLE_KIND.map((h) => `'${h}'`).join(', ')}]`)),
839+
ADMISSIBLE_KIND.join(' · '));
840+
t('...and every one of those literals is admitted individually',
841+
droppedByAdmission(DIR_NAME, ADMISSIBLE_KIND).length === 0);
842+
t('one dropped literal among admissible ones still fails, and only it is named',
843+
rejected(decl(`['scripts/**', '**/package.json']`))
844+
&& droppedByAdmission(DIR_NAME, ['scripts/**', '**/package.json']).join() === '**/package.json');
845+
846+
// ⭐ The false positive a whole-MODULE diff would produce, which is why the
847+
// probe is per literal: `scripts/pm/dispatch-gates.mjs` spells a rostered
848+
// declaration inside its own self-test, and the extractor blanks self-tests --
849+
// so that literal is absent from that module's extraction while being
850+
// perfectly admissible.
851+
t('a literal is judged on its own admissibility, not on whether its module extraction carries it',
852+
droppedByAdmission(DIR_NAME, ['packages/drivers/**']).length === 0);
853+
t('...and a module-relative literal, whose extracted hint is the RESOLVED path, is not accused either',
854+
droppedByAdmission(DIR_NAME, ['../../packages/spec/**']).length === 0);
855+
856+
// The two states that must keep their OWN remedy rather than acquiring this one.
857+
t('an EMPTY declaration keeps the empty-population remedy, not the admission one',
858+
(verdict(decl('[]'))?.why ?? '').includes('names no subtree at all'));
859+
t('a COMPUTED declaration keeps the computed remedy, not the admission one',
860+
(verdict(decl('[`${SCAN_ROOT}/**`]'))?.why ?? '').includes('is COMPUTED, not a literal array'));
861+
689862
// -- the live tree ---------------------------------------------------------
690863
battery('the live tree');
691864
const { rows: live, strays } = audit(walk(REPO_ROOT));
@@ -694,6 +867,10 @@ export function selfTest() {
694867
`${live.length} declaration(s)`);
695868
t('every live declaration is a literal', live.every((r) => r.ok),
696869
live.filter((r) => !r.ok).map((r) => `${r.rel}:${r.name}`).join(' · '));
870+
t('and every literal in every live declaration is one the hint extractor admits',
871+
live.every((r) => !r.ok || droppedByAdmission(r.name, r.hints).length === 0),
872+
live.flatMap((r) => (r.ok ? droppedByAdmission(r.name, r.hints)
873+
.map((h) => `${r.rel}:${r.name}:${h}`) : [])).join(' · '));
697874
t('EVERY rostered name has a live declarer -- the floor is armed, not merely coded',
698875
missingNames(live).length === 0, `missing: ${missingNames(live).join(', ') || 'none'}`);
699876
for (const name of DECL_NAMES) {
@@ -737,7 +914,8 @@ export function selfTest() {
737914
+ 'spellings rejected, the statement-scoped search proved against runtime and comment copies of '
738915
+ `the literal beside it, all ${DECL_NAMES.length} rostered names judged for both spellings, the `
739916
+ 'per-name floor proved against a population that is healthy on every name but one, unrostered '
740-
+ 'spellings of the idiom discovered, and the live repo-wide population judged.',
917+
+ 'spellings of the idiom discovered, the value-side drop proved on a literal array the extractor '
918+
+ 'admits nothing out of, and the live repo-wide population judged on both questions.',
741919
);
742920
selfTestReachedVerdict = true;
743921
return 0;

0 commit comments

Comments
 (0)