Skip to content

Commit f7ff95c

Browse files
committed
fix(pm): recognise a population declared as a lowercase object property
`scripts/pm/bare-root-worklist.mjs` recognised a gate's declared population by the NAME of the `const` holding it, uppercase only, so a population spelled as an object property could never match. `check-doc-route-spelling.mjs` declares `roots: ['content/docs', 'skills']` inside `const REAL_CONFIG`, so its `skills` root was invisible to the very instrument built to find undeclared roots — not a different defect, the same one spelled differently. `POPULATION_PROPERTY` mirrors `POPULATION_CONSTANT` term for term, the camelCase hump doing the work the literal underscore does there; the bare singular `root`/`dir` stay out for the reason `ROOT`/`DIR` do. Dispositions for the three rows it surfaces, measured on 91f65c4: - check-doc-route-spelling.mjs declares `skills/**` (46 of 47 tracked files), the idiom two sibling doc gates already carry at this root; the row is now REACHABLE and the family stops being Silent for skills-only cards. - the two check-console-injection.mjs rows are `join()` path components in a gate that never walks the root; recorded in UNJUDGED with measured notes, which is what a widening pass owes and all it may write. - the UNJUDGED shared base moved 66e68ad -> 91f65c4, re-read rather than re-judged; one cited line number corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
1 parent 3a4b9bc commit f7ff95c

2 files changed

Lines changed: 129 additions & 15 deletions

File tree

scripts/check-doc-route-spelling.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,31 @@ const REAL_CONFIG = {
147147
occurrenceFloors: { 'content/docs': 100, skills: 10 },
148148
};
149149

150+
/**
151+
* The population declared for `scripts/pm/dispatch-gates.mjs`, which builds a
152+
* dispatch's gate list by scanning each gate's source for the path literals it
153+
* operates on. "Looks like a path" there means "carries a separator", so of the
154+
* two roots above only `content/docs` reaches the hint set on its own —
155+
* `skills` does not, and without this declaration a skills-only card is never
156+
* told this gate reads its files. `check-corpus-claim-drift.mjs` and
157+
* `check-doc-authoring.mjs` carry the identical declaration at this root for
158+
* the identical reason.
159+
*
160+
* ⚠️ Spelled as a LITERAL array, never computed from `REAL_CONFIG.roots`: the
161+
* extractor reads SOURCE TEXT, so `roots.map((r) => …)` would contribute
162+
* nothing while every runtime assertion about the value stayed green.
163+
* `check-watch-hint-literal` enforces that; the self-test below pins the
164+
* coupling to `REAL_CONFIG.roots` in both directions.
165+
*
166+
* The subtree is what this gate reads: of the 47 files tracked under the root
167+
* on 91f65c4ea, the walk admits 46 — every `.md`/`.mdx` at any depth, nothing
168+
* under it skipped by `skipDirs` or `skipPaths` — so the declaration over-names
169+
* by exactly one file (a `.json`), at 97.9% precision. ⛔ The bare root is NOT
170+
* declared: it is the population, and the glob is the only spelling the
171+
* extractor can turn into a hint.
172+
*/
173+
const ROOT_DIR_WATCH_HINTS = ['skills/**'];
174+
150175
/** Segment-spelling variants the detector recognises as "same route, drifted
151176
* spelling". Plural/singular is computed; everything else is pinned HERE so a
152177
* new variant class is a reviewed one-line addition, never a loosened
@@ -819,6 +844,19 @@ function selfTest() {
819844
r.flags.some((f) => f.file.includes('releases')), false);
820845
expect('no verdict came from node_modules',
821846
r.flags.some((f) => f.file.includes('node_modules')), false);
847+
// The dispatch declaration, pinned to REAL_CONFIG.roots in BOTH directions
848+
// so it cannot rot: a root that stops being separatorless, or a hint whose
849+
// root leaves the config, reds here rather than silently naming this gate
850+
// for a tree it no longer reads.
851+
const separatorless = REAL_CONFIG.roots.filter((root) => !root.includes('/'));
852+
expect('every separatorless root the walk uses is declared as a watch hint',
853+
separatorless.every((root) => ROOT_DIR_WATCH_HINTS.includes(`${root}/**`)), true);
854+
expect('…and there is one, so the pin above is not holding over an empty list',
855+
separatorless.length > 0, true);
856+
expect('every declared hint names a root the walk actually uses',
857+
ROOT_DIR_WATCH_HINTS.every((h) => REAL_CONFIG.roots.includes(h.replace(/\/\*+$/, ''))), true);
858+
expect('the hint is the SUBTREE, not the bare root (a bare root builds no hint)',
859+
REAL_CONFIG.roots.some((root) => ROOT_DIR_WATCH_HINTS.includes(root)), false);
822860

823861
// ── The teeth: measured drift class flags, by name ───────────────────
824862
battery('The teeth: measured drift class flags, by name');

scripts/pm/bare-root-worklist.mjs

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,9 +1382,9 @@ export const CENSUS_REFUSE_WIDE = new Map([
13821382
]);
13831383

13841384
/**
1385-
* SEEN, NOT YET JUDGED — the rows the widened recogniser made visible, named
1385+
* SEEN, NOT YET JUDGED — the rows a widened recogniser made visible, named
13861386
* here so the worklist above does not read as complete while they sit outside
1387-
* it (#15468).
1387+
* it (#15468, and #17057's property widening after it).
13881388
*
13891389
* ⛔ This is NOT a verdict table and NOT an exemption. An entry records exactly
13901390
* one fact: the sweep SEES this row today and nobody has triaged it. This map
@@ -1414,6 +1414,20 @@ export const CENSUS_REFUSE_WIDE = new Map([
14141414
* and its key is missing. The `base` is the tree the notes were measured on,
14151415
* shared, and pinned to be shared, the way `CENSUS_REFUSE_WIDE` pins its own.
14161416
*
1417+
* ⚠️ That shared base is what a SECOND widening costs, and the cost is the
1418+
* point rather than an obstacle to route around: a bucket may not carry notes
1419+
* measured on two different trees, so #17057's pass could not append its rows
1420+
* without re-reading every note already here against its own base. It did, and
1421+
* the base moved 66e68adc6 to 91f65c4ea. Seven of the eleven inherited entries
1422+
* name gate sources that are BYTE-IDENTICAL across those two commits (compared
1423+
* by blob id rather than by reading), so their notes could not have drifted; of
1424+
* the two files that did change, the live-db-isolation gate changed comment
1425+
* prose only, with no net line movement, and the doc-frontmatter gate grew
1426+
* ABOVE the line its note cites — the one note whose text this pass corrected,
1427+
* :436 to :446. ⛔ No `population` boolean and no recorded reason was otherwise
1428+
* rewritten: a re-measure that quietly re-judges is the thing this map exists
1429+
* to refuse.
1430+
*
14171431
* ⚠️ Every path in the notes below is DESCRIBED rather than quoted — "a
14181432
* two-segment path under the content root", not the literal. That is not
14191433
* fussiness: a separator-carrying literal written here would enter THIS file's
@@ -1424,30 +1438,52 @@ export const CENSUS_REFUSE_WIDE = new Map([
14241438
export const UNJUDGED = new Map([
14251439
['packages/lint/scripts/check-doc-security-posture.mjs ROOTS docs', {
14261440
population: false,
1427-
base: '66e68adc6',
1441+
base: '91f65c4ea',
14281442
why: 'the word is the `label` field at check-doc-security-posture.mjs:155, not a walk root: '
14291443
+ 'that entry\'s population is the `path` field one line above it, a two-segment path under '
14301444
+ 'the content root, which carries a separator and has always been visible to the '
14311445
+ 'derivation. The recogniser matched the LABEL because it sits inside the ROOTS span',
14321446
}],
14331447
['packages/lint/scripts/check-doc-security-posture.mjs ROOTS skills', {
14341448
population: true,
1435-
base: '66e68adc6',
1449+
base: '91f65c4ea',
14361450
why: '`path: \'skills\'` at check-doc-security-posture.mjs:164, walked recursively by the `walk` '
14371451
+ 'at :189-:193 from the `for (const root of ROOTS)` at :462, for every `.md` file under it '
14381452
+ 'minus the entry\'s own `exclude` list',
14391453
}],
1454+
['scripts/check-console-injection.mjs distDir packages', {
1455+
population: false,
1456+
base: '91f65c4ea',
1457+
why: 'the word is a `join()` path COMPONENT at check-console-injection.mjs:940 — the gate '
1458+
+ 'assembles the console dist directory from four bare single-segment words, so the extractor '
1459+
+ 'sees the top-level one and nothing else. It never walks that root: `evaluate` at :247 opens '
1460+
+ 'the index file at :252, the stamp beside it at :268, and every JavaScript asset directly '
1461+
+ 'inside that directory\'s assets child at :269 — `readBundle` in console-spec-probes.mjs, '
1462+
+ 'non-recursive. Surfaced by the #17057 property widening because the component sits inside '
1463+
+ 'the `distDir` property of the argv object, which the recogniser now reads',
1464+
}],
1465+
['scripts/check-console-injection.mjs specDir packages', {
1466+
population: false,
1467+
base: '91f65c4ea',
1468+
why: 'the same shape one line down, at check-console-injection.mjs:941: a `join()` component of '
1469+
+ 'the spec package directory, assembled from bare words. The gate reads that package\'s '
1470+
+ 'manifest and the built JavaScript its `exports` map points at — `readSpecBlob` in '
1471+
+ 'console-spec-probes.mjs, reached at :348 — never an arbitrary file under the top-level '
1472+
+ 'root. ⚠️ Both of this gate\'s real populations are themselves invisible to the derivation, '
1473+
+ 'for the DIFFERENT reason that they are assembled rather than spelled; that is a fact this '
1474+
+ 'note records and not a verdict on what, if anything, should be declared for them',
1475+
}],
14401476
['scripts/check-doc-frontmatter.mjs ROOTS docs', {
14411477
population: false,
1442-
base: '66e68adc6',
1443-
why: 'the word is the `name` field at check-doc-frontmatter.mjs:436, not a walk root: that '
1478+
base: '91f65c4ea',
1479+
why: 'the word is the `name` field at check-doc-frontmatter.mjs:446, not a walk root: that '
14441480
+ 'entry\'s directory is a `join(REPO_ROOT, …)` of a two-segment path under the content root '
14451481
+ 'on the next line, and both of this gate\'s roots are separator-carrying paths under that '
14461482
+ 'root, which the derivation already sees',
14471483
}],
14481484
['scripts/check-live-db-isolation.mjs ROOTS apps', {
14491485
population: true,
1450-
base: '66e68adc6',
1486+
base: '91f65c4ea',
14511487
why: 'one of `const ROOTS = [\'packages\', \'apps\', \'examples\']` at check-live-db-isolation.mjs:178, '
14521488
+ 'each walked recursively at :278-:282. The gate carries the #15341 `wide-population` marker '
14531489
+ 'at :58 and that marker is read here — but the marker excuses a COVERED row carrying a '
@@ -1458,15 +1494,15 @@ export const UNJUDGED = new Map([
14581494
}],
14591495
['scripts/check-live-db-isolation.mjs ROOTS examples', {
14601496
population: true,
1461-
base: '66e68adc6',
1497+
base: '91f65c4ea',
14621498
why: 'the `examples` member of the same walked triple at check-live-db-isolation.mjs:178, with '
14631499
+ 'the same #15341 marker at :58 and the same reading — the marker resolves a contradiction '
14641500
+ 'between a declaration and a verdict, and this row has neither. No CENSUS row names '
14651501
+ '`examples` either',
14661502
}],
14671503
['scripts/check-live-db-isolation.mjs ROOTS packages', {
14681504
population: true,
1469-
base: '66e68adc6',
1505+
base: '91f65c4ea',
14701506
why: 'the `packages` member of the same walked triple at check-live-db-isolation.mjs:178. This '
14711507
+ 'is the one of the three that IS judged somewhere: `check:live-db-isolation packages` is a '
14721508
+ 'recorded REFUSE-WIDE in CENSUS_REFUSE_WIDE, at 90.3% of tracked packages/ files. It is '
@@ -1475,33 +1511,33 @@ export const UNJUDGED = new Map([
14751511
}],
14761512
['scripts/check-vendor-version-stamps.mjs ROOTS apps', {
14771513
population: true,
1478-
base: '66e68adc6',
1514+
base: '91f65c4ea',
14791515
why: 'one of the four bare `path:` entries of `export const ROOTS` at '
14801516
+ 'check-vendor-version-stamps.mjs:232, read by `collectFiles()` at :916 and walked '
14811517
+ 'recursively at :885-:890 for the entry\'s own extension list',
14821518
}],
14831519
['scripts/check-vendor-version-stamps.mjs ROOTS examples', {
14841520
population: true,
1485-
base: '66e68adc6',
1521+
base: '91f65c4ea',
14861522
why: 'the `examples` entry of the same walked list at check-vendor-version-stamps.mjs:232, on '
14871523
+ 'the same `collectFiles()` walk',
14881524
}],
14891525
['scripts/check-vendor-version-stamps.mjs ROOTS packages', {
14901526
population: true,
1491-
base: '66e68adc6',
1527+
base: '91f65c4ea',
14921528
why: 'the `packages` entry of the same walked list at check-vendor-version-stamps.mjs:232, on '
14931529
+ 'the same `collectFiles()` walk',
14941530
}],
14951531
['scripts/check-vendor-version-stamps.mjs ROOTS scripts', {
14961532
population: true,
1497-
base: '66e68adc6',
1533+
base: '91f65c4ea',
14981534
why: 'the `scripts` entry of the same walked list at check-vendor-version-stamps.mjs:232, on '
14991535
+ 'the same `collectFiles()` walk. The gate\'s fifth root is a two-segment path under the '
15001536
+ 'content root, which carries a separator and was already visible',
15011537
}],
15021538
['scripts/check-whole-set-label-write.mjs ROOTS scripts', {
15031539
population: true,
1504-
base: '66e68adc6',
1540+
base: '91f65c4ea',
15051541
why: 'the third member of `export const ROOTS` at check-whole-set-label-write.mjs:152 — the '
15061542
+ 'other two are workflow and action directories under the dotted github root, which carry a '
15071543
+ 'separator and were already visible — walked at :492-:496. ⚠️ This gate already DECLARES at '
@@ -2018,6 +2054,43 @@ function selfTest() {
20182054
t('control: the underscored spellings the restriction always admitted still match, so the case '
20192055
+ 'above is measuring an addition rather than a replacement',
20202056
admits('SCAN_ROOTS') && admits('SCAN_DIRS') && admits('POPULATION'));
2057+
2058+
// The PROPERTY spelling, #17057, pinned the same way and for the same reason:
2059+
// what that card repaired is a judgement about how a population is SPELLED,
2060+
// so a row-shaped pin would hold it only while the tree happens to contain a
2061+
// gate spelled that way — and a gate spelled that way being unseen is the
2062+
// defect itself. The fixture is an object property inside a config literal,
2063+
// which is the shape the card was filed from, and its root is taken FROM THE
2064+
// TREE like every probe above so this file still declares no population.
2065+
const admitsProp = (name) => populationSpans(
2066+
`const CFG = {\n ${name}: [${JSON.stringify(someRoot)}],\n};`,
2067+
).length === 1;
2068+
t('the recogniser admits a population declared as a lowercase object property — `roots:` and '
2069+
+ '`dirs:` inside a config literal (#17057)', admitsProp('roots') && admitsProp('dirs'));
2070+
t('…and the camelCase hump carries the weight the underscore carries above: `scanRoots`, '
2071+
+ '`searchDirs`, `population` and a `…Scope` all match',
2072+
admitsProp('scanRoots') && admitsProp('searchDirs') && admitsProp('population')
2073+
&& admitsProp('lintScope'));
2074+
t('…and it admits them EXACTLY: the bare singular `root` and `dir` stay OUT — the commonest '
2075+
+ 'property names in this tree for one repo-root path fragment, the exact thing the '
2076+
+ 'restriction exists to exclude — and so do a plain `label` and a suffixed `rootsX`',
2077+
!admitsProp('root') && !admitsProp('dir') && !admitsProp('label') && !admitsProp('rootsX'));
2078+
t('control: the property form is a TRANSLATION of the constant form and not a second, looser '
2079+
+ 'judgement — `skipDirs` matches here exactly as `SKIP_DIRS` already matches there, so the '
2080+
+ 'two spellings of one name are admitted or refused together',
2081+
admitsProp('skipDirs') === admits('SKIP_DIRS'));
2082+
t('…and the property span STOPS at its own value: a bare root sitting in the NEXT property of '
2083+
+ 'the same literal is not swept in by the one before it',
2084+
bareRootLiterals(`const CFG = { roots: ['x/y'], label: [${JSON.stringify(someRoot)}] };`, dirs)
2085+
.every(({ index }) => !populationSpans(
2086+
`const CFG = { roots: ['x/y'], label: [${JSON.stringify(someRoot)}] };`,
2087+
).some((s) => index > s.start && index < s.end)));
2088+
t('control: the same literal one property EARLIER, under a recognised key, IS swept in — so '
2089+
+ 'the case above measures the span boundary rather than a recogniser that never fires',
2090+
bareRootLiterals(`const CFG = { roots: [${JSON.stringify(someRoot)}], label: ['x/y'] };`, dirs)
2091+
.some(({ index }) => populationSpans(
2092+
`const CFG = { roots: [${JSON.stringify(someRoot)}], label: ['x/y'] };`,
2093+
).some((s) => index > s.start && index < s.end)));
20212094
t('the live sweep is non-empty, so the cases below judge something', rows.length > 0);
20222095

20232096
// ── The FOLD: one row per literal, however many invocations reach it ──────
@@ -2595,7 +2668,10 @@ function selfTest() {
25952668
+ 'constant-name restriction is proven to restrict, and neither the triage keys nor this '
25962669
+ 'file declare any population of their own. The recogniser is pinned to admit the '
25972670
+ 'bare ROOTS and DIRS spellings EXACTLY, singulars and unsuffixed neighbours refused '
2598-
+ `(#15468). ${UNJUDGED.size} UNJUDGED row(s) — seen by that widened recogniser and judged `
2671+
+ '(#15468), and to read a population declared as a lowercase object PROPERTY — the '
2672+
+ 'camelCase mirror of the same judgement, bare singulars refused there too, its span '
2673+
+ 'proven to stop at its own value (#17057). '
2674+
+ `${UNJUDGED.size} UNJUDGED row(s) — seen by those widened recognisers and judged `
25992675
+ 'by nobody — are held SET-EQUAL, in both directions, to the open rows carrying no verdict, '
26002676
+ 'so none of them is exempted here and none of them is silently fresh. '
26012677
+ `${CENSUS_REFUSE_WIDE.size} CENSUS row(s) `

0 commit comments

Comments
 (0)