Skip to content

Commit fed4fa4

Browse files
claude[bot]claude
andauthored
fix(pm): let a SPELLINGS entry hold a list of segment arrays for multi-hint rows (#14523)
bare-root-worklist.mjs's SPELLINGS harness held exactly one hint per entry, so a DECLARED-NARROWER row whose gate declares more than one hint could not record a spelling at all — five rows were in that state (check:logger-receiver-detach's three SCAN_ROOTS rows, check:dual-build-cjs-loads, check:ratchet-remedy-authority). Nothing in this file re-measured those declarations against hintCovers. Extend SPELLINGS so an entry's `segments` may hold either one hint (a flat array, unchanged for the nine existing entries) or a LIST of segment arrays; the pin loop asks `hintCovers` and `holds` of the union. Add five multi-hint entries covering the five rows' declared hints, wire each row's `spelling:`, and correct the `why` prose that previously stated the harness could not hold this shape. Fixes #14233 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 447eee7 commit fed4fa4

1 file changed

Lines changed: 114 additions & 21 deletions

File tree

scripts/pm/bare-root-worklist.mjs

Lines changed: 114 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,18 @@ const POPULATION_CONSTANT = /^(?:[A-Z0-9_]*_ROOTS?|[A-Z0-9_]*_DIRS?|POPULATION|[
243243
* this file's own hint set and hand a reporting tool a population it does not
244244
* read, which the self-test refuses in as many words.
245245
*
246+
* An entry's `segments` records either ONE hint (a flat array of path
247+
* segments, as above) or SEVERAL (a LIST of segment arrays) — #14233's
248+
* addition, for a gate whose declared population is spelled as more than one
249+
* glob (one hint per extension, or one per file kind at the same root). A
250+
* multi-hint entry's `hintCovers` reach is the UNION of its members, asked of
251+
* every hint in turn; its `holds` is written to accept whatever any member
252+
* covers, since the two-sided pin below (LIVE/PRECISE/COMPLETE/NARROWING)
253+
* still runs over that union. Every hint of a multi-hint entry is required to
254+
* share one root — the sweep below has exactly one denominator per entry, not
255+
* one per hint — and that is verified per entry when the row is written, not
256+
* pinned mechanically here (see the five rows #14233 added).
257+
*
246258
* `holds` is the spelling's claim written INDEPENDENTLY of `hintCovers` — a
247259
* plain segment test a reader can check by eye. It is not a copy of the matcher:
248260
* `hintCovers` reaches its answer through `globInNonFinalSegment`,
@@ -305,12 +317,66 @@ const SPELLINGS = new Map([
305317
claim: "every file inside a skill's references folder, at any depth",
306318
holds: (s) => s[0] === 'skills' && s.length >= 4 && s[2] === 'references',
307319
}],
320+
// ── Multi-hint entries (#14233) — a gate whose declared population is more
321+
// than one glob at the same root. `holds` accepts whatever any member hint
322+
// covers; see `hintsOf` and the pin loop below for how the union is asked.
323+
['packages TypeScript source', {
324+
segments: [
325+
['packages', '**', '*.ts'],
326+
['packages', '**', '*.tsx'],
327+
['packages', '**', '*.mts'],
328+
],
329+
claim: 'every `.ts`, `.tsx` or `.mts` file at any depth under the packages root',
330+
holds: (s) => s[0] === 'packages' && s.length >= 2
331+
&& ['.ts', '.tsx', '.mts'].some((ext) => s[s.length - 1].endsWith(ext)),
332+
}],
333+
['examples TypeScript source', {
334+
segments: ['examples', '**', '*.ts'],
335+
claim: 'every `.ts` file at any depth under the examples root',
336+
holds: (s) => s[0] === 'examples' && s.length >= 2 && s[s.length - 1].endsWith('.ts'),
337+
}],
338+
['apps TypeScript source', {
339+
segments: [
340+
['apps', '**', '*.ts'],
341+
['apps', '**', '*.tsx'],
342+
],
343+
claim: 'every `.ts` or `.tsx` file at any depth under the apps root',
344+
holds: (s) => s[0] === 'apps' && s.length >= 2
345+
&& ['.ts', '.tsx'].some((ext) => s[s.length - 1].endsWith(ext)),
346+
}],
347+
['dual-build manifests and configs', {
348+
segments: [
349+
['packages', '**', 'package.json'],
350+
['packages', '**', 'tsup.config.ts'],
351+
],
352+
claim: 'every workspace manifest or tsup config at any depth under the packages root',
353+
holds: (s) => s[0] === 'packages' && s.length >= 2
354+
&& (s[s.length - 1] === 'package.json' || s[s.length - 1] === 'tsup.config.ts'),
355+
}],
356+
['scripts top-level script files', {
357+
segments: [
358+
['scripts', '*.mjs'],
359+
['scripts', '*.mts'],
360+
],
361+
claim: 'every `.mjs` or `.mts` file directly under the scripts root (non-recursive)',
362+
holds: (s) => s.length === 2 && s[0] === 'scripts'
363+
&& (s[1].endsWith('.mjs') || s[1].endsWith('.mts')),
364+
}],
308365
]);
309366

310-
/** The recorded spelling, assembled. Never a literal — see `SPELLINGS`. */
367+
/**
368+
* Normalises a `SPELLINGS` entry's `segments` to a LIST of segment arrays,
369+
* whether it records one hint (a flat array) or several (already a list) —
370+
* distinguished by whether the first element is itself an array. #14233.
371+
*/
372+
function hintsOf(segments) {
373+
return Array.isArray(segments[0]) ? segments : [segments];
374+
}
375+
376+
/** The recorded spelling(s), assembled. Never a literal — see `SPELLINGS`. */
311377
export function spellingOf(name) {
312378
const s = SPELLINGS.get(name);
313-
return s ? s.segments.join('/') : null;
379+
return s ? hintsOf(s.segments).map((seg) => seg.join('/')).join(' | ') : null;
314380
}
315381

316382
const TRIAGE = new Map([
@@ -322,22 +388,36 @@ const TRIAGE = new Map([
322388
}],
323389
['check:logger-receiver-detach SCAN_ROOTS packages', {
324390
verdict: 'DECLARED-NARROWER',
391+
spelling: 'packages TypeScript source',
325392
why: 'the population is the non-test TypeScript source under the root, declared beside the '
326393
+ 'constant at the three live extensions (4968 of 5485 tracked files, 90.6%) instead of '
327394
+ 'the bare root. The remainder is manifests, JSON, markdown and fixtures the gate never '
328-
+ 'opens, and the test files it deliberately does not read',
395+
+ 'opens, and the test files it deliberately does not read. #14233: the declared population '
396+
+ 'needs three hints (`.ts`/`.tsx`/`.mts`), which the SPELLINGS harness could not hold before '
397+
+ 'it grew a multi-hint entry — the union now pins the three declared hints LIVE, PRECISE and '
398+
+ 'COMPLETE against a plain "ends with one of these extensions" claim (5240 of 5796 tracked '
399+
+ 'files under the bare root, re-measured 2026-09-01 — a different, wider count than the '
400+
+ "gate's own non-test figure above, since the pin holds the raw hints' literal reach and "
401+
+ "the gate's own test-file exclusion is not something a hint can spell)",
329402
}],
330403
['check:logger-receiver-detach SCAN_ROOTS examples', {
331404
verdict: 'DECLARED-NARROWER',
405+
spelling: 'examples TypeScript source',
332406
why: 'same declaration, same gate: the TypeScript source under the examples root, 204 of 241 '
333407
+ 'tracked files (84.6%), rather than the bare word. The uncovered remainder carries no '
334-
+ 'TypeScript for this gate to read',
408+
+ 'TypeScript for this gate to read. #14233: this root needs only the one hint the gate '
409+
+ 'declares (`.ts`), now pinned LIVE, PRECISE and COMPLETE (206 of 243 tracked files under '
410+
+ 'the bare root, re-measured 2026-09-01)',
335411
}],
336412
['check:logger-receiver-detach SCAN_ROOTS apps', {
337413
verdict: 'DECLARED-NARROWER',
414+
spelling: 'apps TypeScript source',
338415
why: 'same declaration, same gate: 28 of 40 tracked files (70.0%) under the apps root, at the '
339416
+ 'two extensions that exist there. The lowest ratio of the three and still a real '
340-
+ 'narrowing — the uncovered dozen are config and content files, none of them TypeScript',
417+
+ 'narrowing — the uncovered dozen are config and content files, none of them TypeScript. '
418+
+ '#14233: the two declared hints (`.ts`/`.tsx`) are now pinned as one multi-hint spelling, '
419+
+ 'LIVE, PRECISE and COMPLETE (29 of 41 tracked files under the bare root, re-measured '
420+
+ '2026-09-01)',
341421
}],
342422
['check:objectql-double-limit SCAN_ROOT packages', {
343423
verdict: 'DECLARED-NARROWER',
@@ -395,6 +475,7 @@ const TRIAGE = new Map([
395475
}],
396476
['check:dual-build-cjs-loads SCAN_ROOT packages', {
397477
verdict: 'DECLARED-NARROWER',
478+
spelling: 'dual-build manifests and configs',
398479
why: 'the gate walks every publishable manifest under the root to find published `require` '
399480
+ 'conditions, then reads only the dist/ those manifests point at — so the two literals it '
400481
+ 'declares beside SCAN_ROOT under the ROOT_DIR_WATCH_HINTS idiom are the files whose '
@@ -407,10 +488,14 @@ const TRIAGE = new Map([
407488
+ 'error this map names. The recall is not lost: the gate is a step in Build Core, a '
408489
+ 'required context on every PR, so the omission costs one CI round trip rather than a '
409490
+ 'missed defect. The row STAYS in the sweep because the bare root is still not covered — '
410-
+ 'no arbitrary file at the top of packages/ is reached — which is what this verdict says',
491+
+ 'no arbitrary file at the top of packages/ is reached — which is what this verdict says. '
492+
+ "#14233: the two declared hints could not be pinned before SPELLINGS held only one per "
493+
+ 'entry; they are now one multi-hint spelling, LIVE, PRECISE and COMPLETE against the same '
494+
+ '74/20 split, re-measured 2026-09-01 (94 of 5796 tracked files under the bare root)',
411495
}],
412496
['check:ratchet-remedy-authority SCRIPTS_DIR scripts', {
413497
verdict: 'DECLARED-NARROWER',
498+
spelling: 'scripts top-level script files',
414499
why: 'RE-DECIDED 2026-09-01 (#13813) from REFUSE-UNSPELLABLE, whose stated reason — "the idiom '
415500
+ 'has no non-recursive spelling" — was TRUE when written and is FALSE of this tree. It '
416501
+ 'rested on the deletion-collapse: a glob carrying a literal SUFFIX in the final segment '
@@ -424,17 +509,22 @@ const TRIAGE = new Map([
424509
+ 'The gate now declares ONE hint per admitted extension beside SCRIPTS_DIR under the '
425510
+ 'ROOT_DIR_WATCH_HINTS idiom, and the pair is SET-EQUAL to that walk in both directions — '
426511
+ '183 of 183, nothing read left uncovered, nothing covered left unread — so 100% precise '
427-
+ 'and complete. ⛔ NO `spelling` is recorded and that is deliberate, not an omission: '
428-
+ 'SPELLINGS holds ONE hint per entry and this population needs one per extension, the same '
429-
+ 'shape as the check:logger-receiver-detach and check:dual-build-cjs-loads rows above, so '
430-
+ 'the liveness-and-precision coupling is held in the gate own --self-test, which pins the '
431-
+ 'hints against SCRIPTS_DIR and CORPUS_EXTENSIONS and refuses both the subtree spelling and '
432-
+ 'the brace form its own messages print. The consumer is MEASURED, not argued: before this, '
433-
+ 'the derivation placed this family in the residue undetermined bucket, absent from the '
434-
+ 'matched list a brief prints, and a PR that ran its whole derived family green locally '
435-
+ 'lost a CI round to this gate. The row STAYS in the sweep because the bare root is still '
436-
+ 'not covered — no arbitrary file at the top of the root is reached, and no nested script '
437-
+ 'at any depth — which is what this verdict says and is correct, not outstanding debt',
512+
+ 'and complete. The coupling described above was, at the time this row was decided, held '
513+
+ "only in the gate's own --self-test (which pins the hints against SCRIPTS_DIR and "
514+
+ 'CORPUS_EXTENSIONS and refuses both the subtree spelling and the brace form its own '
515+
+ 'messages print), because SPELLINGS held ONE hint per entry and this population needs one '
516+
+ 'per extension — the same shape as the check:logger-receiver-detach and '
517+
+ 'check:dual-build-cjs-loads rows above, and the gap #14233 measured and closed: SPELLINGS '
518+
+ 'now holds a LIST of segment arrays, so the pair is pinned HERE too, LIVE, PRECISE and '
519+
+ 'COMPLETE (183 of 310 tracked files under the bare root, matching the gate own walk exactly, '
520+
+ "re-measured 2026-09-01) — the gate's own pin and this one now independently corroborate "
521+
+ 'the same declaration rather than only one of them re-measuring it. The consumer is '
522+
+ 'MEASURED, not argued: before this, the derivation placed this family in the residue '
523+
+ 'undetermined bucket, absent from the matched list a brief prints, and a PR that ran its '
524+
+ 'whole derived family green locally lost a CI round to this gate. The row STAYS in the '
525+
+ 'sweep because the bare root is still not covered — no arbitrary file at the top of the '
526+
+ 'root is reached, and no nested script at any depth — which is what this verdict says and '
527+
+ 'is correct, not outstanding debt',
438528
}],
439529
// ── Refused: the population is the whole root, and the root is saturated ──
440530
['check:skill-identifier-liveness IMPL_ROOTS packages', {
@@ -1115,12 +1205,15 @@ function selfTest() {
11151205

11161206
for (const name of [...usedSpellings].sort()) {
11171207
const { segments, claim, holds } = SPELLINGS.get(name);
1118-
const hint = segments.join('/');
1119-
const covered = files.filter((f) => hintCovers(hint, f));
1208+
// A multi-hint entry's `hintCovers` reach is the UNION of its members —
1209+
// asked of every hint in turn, never collapsed into one string first
1210+
// (there is no glob idiom that would spell "either of these two globs").
1211+
const hints = hintsOf(segments).map((s) => s.join('/'));
1212+
const covered = files.filter((f) => hints.some((hint) => hintCovers(hint, f)));
11201213
const over = covered.filter((f) => !holds(seg(f)));
11211214
const claimed = files.filter((f) => holds(seg(f)));
1122-
const under = claimed.filter((f) => !hintCovers(hint, f));
1123-
const rootFiles = files.filter((f) => seg(f)[0] === segments[0]);
1215+
const under = claimed.filter((f) => !hints.some((hint) => hintCovers(hint, f)));
1216+
const rootFiles = files.filter((f) => seg(f)[0] === hintsOf(segments)[0][0]);
11241217
t(`the spelling recorded as "${name}" is LIVE — hintCovers reaches ${covered.length} tracked `
11251218
+ `file(s) with it, so no record below names a hint that covers nothing`, covered.length > 0);
11261219
t(`…and it is PRECISE: every file hintCovers admits for "${name}" (${claim}) satisfies that `

0 commit comments

Comments
 (0)