From 25036cc5e1b2f253e84731b4a43744126fd48bbc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 06:38:30 +0000 Subject: [PATCH] fix(pm): bare-root-worklist sweeps the bare ROOTS / DIRS spellings POPULATION_CONSTANT required a literal underscore in every alternative but POPULATION, so `SCAN_ROOTS` matched and the plainest spelling of the shape this sweep exists to find -- `const ROOTS = [...]` -- did not. The instrument that reports on invisibility was itself blind to it, and no reader of its output could infer the omission from the output. Admitted as two EXACT alternatives, never as `_?` on the two that were there: that looser edit also admits ROOT, DIR and SCANROOTS, and the bare singular ROOT is this tree's commonest name for a repo root -- one path fragment, the thing the regex exists to exclude. Pinned in --self-test in both directions, with SCAN_ROOTS / SCAN_DIRS / POPULATION as the control that the widening added alternatives rather than replacing them. Measured on 66e68adc6 before the regex moved: the two names add 23 rows across 11 gate source files, and the profile is the OPPOSITE of --wide -- 21 of 23 are real populations (recursive readdirSync walk, or git ls-files over the root), 2 are a label/name field beside a separator-carrying path that was always visible, and NONE is a join() path component in a gate that never reads the root. 12 land REACHABLE against declarations their gates already carry; 11 land open with no verdict. No verdict rows are written -- the map is shrink-only and maintainer-ruled. The 11 go into a new UNJUDGED bucket: seen, measured, judged by nobody, and printed under the worklist so the debt list stops reading as complete. The exclusion buys no silence, because --self-test holds the bucket SET-EQUAL to the open rows carrying no verdict in both directions -- a row can only leave the FRESH case by being NAMED, and anything unlisted still prints as UNTRIAGED and still reds. The two battery floors this touches are re-baselined to their measured run counts (17 and 19 static cases, no loops or conditionals in either region), the convention the roster's other entries follow. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk --- scripts/pm/bare-root-worklist.mjs | 272 +++++++++++++++++++++++++++++- 1 file changed, 263 insertions(+), 9 deletions(-) diff --git a/scripts/pm/bare-root-worklist.mjs b/scripts/pm/bare-root-worklist.mjs index c1975d4961..4f2db5a836 100644 --- a/scripts/pm/bare-root-worklist.mjs +++ b/scripts/pm/bare-root-worklist.mjs @@ -88,8 +88,35 @@ const ROOT = new URL('../..', import.meta.url).pathname; * the derivation, and it is deliberately narrow: it selects the shape an author * uses when saying "this is what I walk". Widening it is how this becomes the * wide sweep, which is the thing not to build. + * + * ⚠️ The bare `ROOTS` and `DIRS` were admitted on 2026-09-05 (#15468), and they + * are the one widening that makes the restriction MORE faithful to itself + * rather than less. Every alternative but `POPULATION` requires a literal + * underscore — `[A-Z0-9_]*` may be empty only if an `_` follows it — so + * `SCAN_ROOTS` matched and the PLAINEST spelling of the very shape this sweep + * exists to find did not. A gate whose author wrote `const ROOTS = [...]` was + * invisible to the instrument that reports on invisibility, and no reader of + * its output could infer the omission from the output. + * + * ⛔ It is admitted as two EXACT alternatives, never as `_?` on the two + * existing ones. That looser edit reads as the same repair and is not: it also + * admits `ROOT`, `DIR` and `SCANROOTS`, and the singular bare `ROOT` is the + * commonest name in this tree for a REPO ROOT — one path fragment, the exact + * thing this regex exists to exclude. The self-test pins both directions. + * + * Measured before the widening, on 66e68adc6, which is the half of this the + * card asked for and the half a regex edit cannot assert: the two names add 23 + * rows across 11 gate source files, and the profile is the OPPOSITE of `--wide` + * — 21 of the 23 are real populations (a recursive `readdirSync` walk, or + * `git ls-files` over the root), 2 are a `label:`/`name:` field beside a + * separator-carrying path that was always visible, and NONE is a `join()` path + * component in a gate that never reads the root. That profile is why this + * widening is not the wide sweep in miniature: `--wide` finds roughly twice the + * rows and is overwhelmingly the component shape, and it remains untriaged. + * 12 of the 23 land REACHABLE — their gates already declare the root — and the + * remaining 11 are listed in `UNJUDGED` below. */ -const POPULATION_CONSTANT = /^(?:[A-Z0-9_]*_ROOTS?|[A-Z0-9_]*_DIRS?|POPULATION|[A-Z0-9_]*_SCOPE)$/; +const POPULATION_CONSTANT = /^(?:[A-Z0-9_]*_ROOTS?|[A-Z0-9_]*_DIRS?|ROOTS|DIRS|POPULATION|[A-Z0-9_]*_SCOPE)$/; /** * The recorded triage — the half of this file that a human decided and the tree @@ -1249,6 +1276,138 @@ export const CENSUS_REFUSE_WIDE = new Map([ }], ]); +/** + * SEEN, NOT YET JUDGED — the rows the widened recogniser made visible, named + * here so the worklist above does not read as complete while they sit outside + * it (#15468). + * + * ⛔ This is NOT a verdict table and NOT an exemption. An entry records exactly + * one fact: the sweep SEES this row today and nobody has triaged it. This map + * is shrink-only and maintainer-ruled, so the pass that widened the recogniser + * may not write `TRIAGE` rows on the maintainer's behalf — and the thing it + * must not do INSTEAD is go quiet, which is what an unlisted row would be the + * moment someone made `--self-test` green again by any other means. A row that + * is seen and unjudged is a fact about this map's own debt; the only dishonest + * options are to judge it here or to hide it. + * + * The `why` is a MEASUREMENT, never a judgement: it says how the literal is + * used in the gate's own source, at file:line, which is the reading a triage + * decision starts from and the only thing this pass could take honestly. Two + * of the eleven are not populations at all — a `label:` / `name:` field beside + * a separator-carrying path that was always visible to the derivation — and + * recording that is a measurement too, not a pre-judged REFUSE. ⛔ Neither is a + * verdict: a row that turns out to name no population still has to be DECIDED, + * because the honest outcome for it may be that the recogniser should not have + * matched it at all, and that is a decision about this instrument. + * + * `--self-test` holds this bucket in BOTH directions, the same coupling + * `TRIAGE` gets and for the same reason: every key here must still be a live + * open row carrying no verdict (nothing outlives what it names), and every live + * open row carrying no verdict must be named here (nothing joins the species + * unlisted). Set equality, so the bucket cannot rot into an allowlist — it reds + * when a row is judged and its key stays, exactly as it reds when a row arrives + * and its key is missing. The `base` is the tree the notes were measured on, + * shared, and pinned to be shared, the way `CENSUS_REFUSE_WIDE` pins its own. + * + * ⚠️ Every path in the notes below is DESCRIBED rather than quoted — "a + * two-segment path under the content root", not the literal. That is not + * fussiness: a separator-carrying literal written here would enter THIS file's + * own hint set and name this tool for every card under that root, which is the + * trap its own `--self-test` closes by name and which this pass tripped on the + * first draft of these notes. + */ +export const UNJUDGED = new Map([ + ['packages/lint/scripts/check-doc-security-posture.mjs ROOTS docs', { + population: false, + base: '66e68adc6', + why: 'the word is the `label` field at check-doc-security-posture.mjs:155, not a walk root: ' + + 'that entry\'s population is the `path` field one line above it, a two-segment path under ' + + 'the content root, which carries a separator and has always been visible to the ' + + 'derivation. The recogniser matched the LABEL because it sits inside the ROOTS span', + }], + ['packages/lint/scripts/check-doc-security-posture.mjs ROOTS skills', { + population: true, + base: '66e68adc6', + why: '`path: \'skills\'` at check-doc-security-posture.mjs:164, walked recursively by the `walk` ' + + 'at :189-:193 from the `for (const root of ROOTS)` at :462, for every `.md` file under it ' + + 'minus the entry\'s own `exclude` list', + }], + ['scripts/check-doc-frontmatter.mjs ROOTS docs', { + population: false, + base: '66e68adc6', + why: 'the word is the `name` field at check-doc-frontmatter.mjs:436, not a walk root: that ' + + 'entry\'s directory is a `join(REPO_ROOT, …)` of a two-segment path under the content root ' + + 'on the next line, and both of this gate\'s roots are separator-carrying paths under that ' + + 'root, which the derivation already sees', + }], + ['scripts/check-live-db-isolation.mjs ROOTS apps', { + population: true, + base: '66e68adc6', + why: 'one of `const ROOTS = [\'packages\', \'apps\', \'examples\']` at check-live-db-isolation.mjs:178, ' + + 'each walked recursively at :278-:282. The gate carries the #15341 `wide-population` marker ' + + 'at :58 and that marker is read here — but the marker excuses a COVERED row carrying a ' + + 'recorded verdict, and this row is neither, so it is unjudged rather than declared. The ' + + 'marker\'s own text records `check:live-db-isolation packages` in CENSUS_REFUSE_WIDE; that ' + + 'table is keyed on (family, root) and is deliberately not coupled to this sweep (#14695), ' + + 'and it says nothing about `apps`', + }], + ['scripts/check-live-db-isolation.mjs ROOTS examples', { + population: true, + base: '66e68adc6', + why: 'the `examples` member of the same walked triple at check-live-db-isolation.mjs:178, with ' + + 'the same #15341 marker at :58 and the same reading — the marker resolves a contradiction ' + + 'between a declaration and a verdict, and this row has neither. No CENSUS row names ' + + '`examples` either', + }], + ['scripts/check-live-db-isolation.mjs ROOTS packages', { + population: true, + base: '66e68adc6', + why: 'the `packages` member of the same walked triple at check-live-db-isolation.mjs:178. This ' + + 'is the one of the three that IS judged somewhere: `check:live-db-isolation packages` is a ' + + 'recorded REFUSE-WIDE in CENSUS_REFUSE_WIDE, at 90.3% of tracked packages/ files. It is ' + + 'listed here anyway because that table is a different key space this sweep cannot produce, ' + + 'and reading a census row as a sweep verdict is the coupling #14695 refused by name', + }], + ['scripts/check-vendor-version-stamps.mjs ROOTS apps', { + population: true, + base: '66e68adc6', + why: 'one of the four bare `path:` entries of `export const ROOTS` at ' + + 'check-vendor-version-stamps.mjs:232, read by `collectFiles()` at :916 and walked ' + + 'recursively at :885-:890 for the entry\'s own extension list', + }], + ['scripts/check-vendor-version-stamps.mjs ROOTS examples', { + population: true, + base: '66e68adc6', + why: 'the `examples` entry of the same walked list at check-vendor-version-stamps.mjs:232, on ' + + 'the same `collectFiles()` walk', + }], + ['scripts/check-vendor-version-stamps.mjs ROOTS packages', { + population: true, + base: '66e68adc6', + why: 'the `packages` entry of the same walked list at check-vendor-version-stamps.mjs:232, on ' + + 'the same `collectFiles()` walk', + }], + ['scripts/check-vendor-version-stamps.mjs ROOTS scripts', { + population: true, + base: '66e68adc6', + why: 'the `scripts` entry of the same walked list at check-vendor-version-stamps.mjs:232, on ' + + 'the same `collectFiles()` walk. The gate\'s fifth root is a two-segment path under the ' + + 'content root, which carries a separator and was already visible', + }], + ['scripts/check-whole-set-label-write.mjs ROOTS scripts', { + population: true, + base: '66e68adc6', + why: 'the third member of `export const ROOTS` at check-whole-set-label-write.mjs:152 — the ' + + 'other two are workflow and action directories under the dotted github root, which carry a ' + + 'separator and were already visible — walked at :492-:496. ⚠️ This gate already DECLARES at ' + + 'this root and the declaration is strictly NARROWER: `ROOT_DIR_WATCH_HINTS` at :193 is three ' + + 'recursive extension globs under the root (mjs, ts, sh), which is why `covered` still reads ' + + 'false and the row stays open. That is the measured shape of a verdict this file already ' + + 'defines, and naming which one is still the maintainer\'s call rather than this pass\'s', + }], +]); + + // --------------------------------------------------------------------------- // The sweep — derived at runtime. Nothing below is listed in this file. // --------------------------------------------------------------------------- @@ -1445,7 +1604,14 @@ function report({ wide = false } = {}) { ); for (const r of rows) { const t = TRIAGE.get(r.key); - const state = r.covered ? 'REACHABLE' : (t ? t.verdict : '⛔ UNTRIAGED'); + // UNJUDGED is a state of its own and NOT a quieter spelling of UNTRIAGED: + // it says a human has seen this row and owes it a verdict, where UNTRIAGED + // says nobody has seen it at all. Keeping the loud one for anything the + // bucket does not name is what stops the bucket from absorbing the next + // arrival silently — that row still prints ⛔ and still reds --self-test. + const state = r.covered + ? 'REACHABLE' + : (t ? t.verdict : (UNJUDGED.has(r.key) ? 'UNJUDGED' : '⛔ UNTRIAGED')); // The invocation count, never the invocations: a row folds every family CI // reaches this literal through, and the count is what says so without // putting an argv back on a line the key deliberately keeps it off. @@ -1453,12 +1619,28 @@ function report({ wide = false } = {}) { console.log(` ${state.padEnd(19)} ${r.key}${folded}`); if (t) console.log(` ${' '.repeat(19)} ${t.why}`); } - const untriaged = open.filter((r) => !TRIAGE.has(r.key)); + const unjudged = open.filter((r) => !TRIAGE.has(r.key) && UNJUDGED.has(r.key)); + const untriaged = open.filter((r) => !TRIAGE.has(r.key) && !UNJUDGED.has(r.key)); console.log( `\n${untriaged.length} untriaged row(s). Every verdict above is a judgement recorded once, ` + 'not a rule; the sweep itself is re-derived from the tree on every run.', ); + console.log( + `\n${unjudged.length} UNJUDGED row(s) (#15468) — SEEN by this sweep and judged by nobody. ` + + 'The constant-name recogniser was widened to admit the bare `ROOTS` / `DIRS` spellings, ' + + 'these rows became visible with it, and writing a verdict for them belongs to whoever owns ' + + 'this map — it is shrink-only, so the pass that made them visible may not judge them. They ' + + 'are printed here, rather than left to sit unmentioned among the rows above, because the ' + + 'failure this file exists to prevent is a debt list that READS as complete. ⛔ Each note is ' + + 'a measurement of how the literal is used in its gate, never a judgement of it:', + ); + for (const r of unjudged) { + const u = UNJUDGED.get(r.key); + console.log(` ${(u.population ? 'population' : 'NOT a population').padEnd(19)} ${r.key}`); + console.log(` ${' '.repeat(19)} ${u.why}`); + } + console.log( `\n${CENSUS_REFUSE_WIDE.size} CENSUS row(s) — found by #14325's wider fs-trace census, not by ` + 'the sweep above, and not coupled to it (#14695):', @@ -1498,8 +1680,8 @@ function report({ wide = false } = {}) { // a section head would be a source change this batch does not make, and leaving // them unattributed reds by the set difference below, which is the point. const SELF_TEST_BATTERIES = Object.freeze({ - 'The FOLD: one row per literal, however many invocations reach it': 14, - 'The triage coupling, both directions': 6, + 'The FOLD: one row per literal, however many invocations reach it': 17, + 'The triage coupling, both directions': 19, 'The MECHANISM a repaired reason turns on, held mechanically': 4, 'The RECORDED SPELLINGS, pinned: LIVENESS and PRECISION': 65, "The DECLARATION a row describes, read from the gate's own SOURCE": 43, @@ -1660,6 +1842,26 @@ function selfTest() { t('the constant-name restriction actually restricts — a bare root outside a population ' + 'constant yields no triple', populationSpans(`const somePath = ${JSON.stringify(someRoot)};`).length === 0); + + // The two spellings #15468 admitted and the four it refused, pinned on the + // RECOGNISER rather than on any row. What that card repaired is a judgement + // about NAMES, so a row-shaped pin would hold it only for as long as the tree + // happens to contain a gate spelled that way — and the defect it repaired was + // precisely that such gates were unseen, which no row could have recorded. + // The negatives are the widening's price tag: they are the names the ONE-CHAR + // `_?` edit would have swept in alongside the two, and `ROOT` is the sharpest + // of them — the commonest name in this tree for a repo root, which is one path + // fragment and the exact thing this regex exists to keep out. + const admits = (name) => populationSpans(`const ${name} = ${JSON.stringify(someRoot)};`).length === 1; + t('the recogniser admits the bare `ROOTS` and `DIRS` spellings — the plainest spelling of the ' + + 'shape this sweep exists to find (#15468)', admits('ROOTS') && admits('DIRS')); + t('…and admits them EXACTLY: the singular `ROOT` and `DIR`, the suffixed `ROOTS_X` and the ' + + 'underscore-less `SCANROOTS` all stay out, so this is two named alternatives and not an ' + + '`_?` loosening of the two that were already there', + !admits('ROOT') && !admits('DIR') && !admits('ROOTS_X') && !admits('SCANROOTS')); + t('control: the underscored spellings the restriction always admitted still match, so the case ' + + 'above is measuring an addition rather than a replacement', + admits('SCAN_ROOTS') && admits('SCAN_DIRS') && admits('POPULATION')); t('the live sweep is non-empty, so the cases below judge something', rows.length > 0); // ── The FOLD: one row per literal, however many invocations reach it ────── @@ -1772,13 +1974,59 @@ function selfTest() { // fabricated lead in every future dispatch prompt, which `hintCovers`' docblock // prices above a missing one. Refusing, with the measured reason, is a // first-class outcome here and most rows below are one. - const fresh = open.filter((r) => !TRIAGE.has(r.key)).map((r) => r.key).sort(); + // + // ⚠️ A row named in `UNJUDGED` is NOT fresh, and the difference is the whole + // content of that bucket: fresh means nobody has seen it, and a listed row has + // been seen, measured and left for the maintainer to judge (#15468). ⛔ The + // exclusion is not an exemption, and it buys no silence — it is paid for by + // the two cases below, which hold the bucket set-equal to exactly these rows + // in both directions, so a row can only leave this case by being NAMED, and + // naming it is what puts it in front of a reader. + const unjudgedSeen = open.filter((r) => !TRIAGE.has(r.key)).map((r) => r.key).sort(); + const fresh = unjudgedSeen.filter((k) => !UNJUDGED.has(k)); t(`no gate has NEWLY joined the invisible bare-root species${fresh.length ? ` — FRESH: ` + `${fresh.join(' · ')}. Record a verdict for it: REFUSE-WIDE, REFUSE-UNSPELLABLE, or a ` + 'declaration beside the constant (the ROOT_DIR_WATCH_HINTS idiom — check-role-word.mjs, ' + 'check-examples-live-imports.mjs, check-driver-conformance.mjs). ⛔ Declaring a root the ' + 'gate does not read wholesale is the costlier error.' : ''}`, fresh.length === 0); + // The other direction of the same coupling. An entry that outlives its row is + // the allowlist shape one level along: the row is judged, or the gate takes a + // declaration, or the constant is renamed, and a key sits here still claiming + // something is owed. It reds by NAME rather than by count so the reader is + // told which entry to delete. + const bucketStale = [...UNJUDGED.keys()].filter((k) => !unjudgedSeen.includes(k)).sort(); + t(`no UNJUDGED entry outlives the row it names${bucketStale.length ? ` — STALE: ` + + `${bucketStale.join(' · ')}. The row was judged, declared or renamed; delete the entry — do ` + + 'not re-point it at another row, and do not record a verdict here.' : ''}`, + bucketStale.length === 0); + // Stated as the identity the two cases above add up to, because "counted + // exactly" is the claim this bucket makes and a reader should not have to + // derive it from two negatives. + t(`the UNJUDGED bucket names EXACTLY the open rows carrying no verdict — ${UNJUDGED.size} ` + + `entr(ies), ${unjudgedSeen.length} such row(s)`, UNJUDGED.size === unjudgedSeen.length); + // Key spaces stay disjoint for the reason CENSUS_REFUSE_WIDE's does: a key + // judged by two independent audits can have them disagree in silence. + const bucketOverlap = [...UNJUDGED.keys()].filter((k) => TRIAGE.has(k) || CENSUS_REFUSE_WIDE.has(k)).sort(); + t(`no UNJUDGED key collides with a TRIAGE or CENSUS key${bucketOverlap.length + ? ` — COLLISION: ${bucketOverlap.join(' · ')}` : ''}`, bucketOverlap.length === 0); + // Every entry carries a measured note and the tree it was measured on, and + // the base is SHARED — mixing bases silently is what the tables above refuse + // by name, and a note is worthless without the tree it describes. + const bucketShape = [...UNJUDGED.values()].every((v) => ( + typeof v.population === 'boolean' + && typeof v.base === 'string' && /^[0-9a-f]{7,40}$/.test(v.base) + && typeof v.why === 'string' && v.why.length > 20 + )); + t('every UNJUDGED entry is well-formed: a boolean saying whether the literal is a real ' + + 'population, a base commit spelled as a short hex sha, and a measured note over 20 chars', + bucketShape); + const bucketBases = new Set([...UNJUDGED.values()].map((v) => v.base)); + t(`every UNJUDGED entry shares one base commit${bucketBases.size > 1 + ? ` — MIXED: ${[...bucketBases].join(' · ')}` : ''}`, bucketBases.size <= 1); + t('the bucket judges something — it is non-empty, so the identity above is not holding over ' + + 'an empty set while the sweep sees rows nobody named', UNJUDGED.size > 0); + // CONTRADICTED: a refusal that outlived the reachability it refused. This is // the THIRD direction of the same coupling, and the one neither assertion // above can see, because both audit the KEY SET. A row whose gate later @@ -1879,8 +2127,9 @@ function selfTest() { // The spelling rule the triage docblock states, held mechanically: a key // spelled as a bare path would enter this file's own declared population. const asHints = (s) => extractWatchHints(`const L = ${JSON.stringify(s)};`); - t('no triage key enters this file\'s own hint set as a path', - [...TRIAGE.keys(), 'scripts/check-sample-gate.mjs SOME_ROOT someroot'].every((k) => asHints(k).length === 0)); + t('no triage or UNJUDGED key enters this file\'s own hint set as a path', + [...TRIAGE.keys(), ...UNJUDGED.keys(), 'scripts/check-sample-gate.mjs SOME_ROOT someroot'] + .every((k) => asHints(k).length === 0)); t('…and that rule can FAIL: the bare-path spelling it forbids does build a hint', asHints('scripts/check-x.mjs').length === 1); @@ -2188,7 +2437,12 @@ function selfTest() { + 'to discriminate (a ' + 'separator-carrying and a dotted root are both refused as already visible), the ' + 'constant-name restriction is proven to restrict, and neither the triage keys nor this ' - + `file declare any population of their own. ${CENSUS_REFUSE_WIDE.size} CENSUS row(s) ` + + 'file declare any population of their own. The recogniser is pinned to admit the ' + + 'bare ROOTS and DIRS spellings EXACTLY, singulars and unsuffixed neighbours refused ' + + `(#15468). ${UNJUDGED.size} UNJUDGED row(s) — seen by that widened recogniser and judged ` + + 'by nobody — are held SET-EQUAL, in both directions, to the open rows carrying no verdict, ' + + 'so none of them is exempted here and none of them is silently fresh. ' + + `${CENSUS_REFUSE_WIDE.size} CENSUS row(s) ` + '(#14695) are well-formed, disjoint from TRIAGE, contribute no hint of their own, and share ' + 'one base commit.', );