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.', );