@@ -61,6 +61,13 @@ import { readFileSync, existsSync } from 'node:fs';
6161import { join } from 'node:path' ;
6262import { fileURLToPath } from 'node:url' ;
6363import { isEntrypoint } from '../invoked-as.mjs' ;
64+ // The idiom's own parser, reused rather than re-implemented: a DECLARED-NARROWER
65+ // row's whole claim is about the hint array a gate SPELLS, so the pin below has
66+ // to read that array from the gate's source. `auditSource` already knows every
67+ // rostered declaration name and the literal-array shape the extractor can read,
68+ // and a second copy of either here would be a roster this file has to keep in
69+ // step by hand -- the failure mode it exists to catch, one level up. #15201.
70+ import { auditSource } from '../check-watch-hint-literal.mjs' ;
6471import {
6572 collapseHint ,
6673 discoverFamilies ,
@@ -117,6 +124,18 @@ const POPULATION_CONSTANT = /^(?:[A-Z0-9_]*_ROOTS?|[A-Z0-9_]*_DIRS?|POPULATION|[
117124 * than the bare word. The row stays in the sweep because
118125 * the bare root is still not covered — which is correct,
119126 * not outstanding debt.
127+ * ⛔ A record carrying a `spelling` is pinned against the
128+ * DECLARATION as well as the tree: `--self-test` reads the
129+ * gate's own hint array out of its source and holds the
130+ * recorded spelling SET-EQUAL to it at this row's root,
131+ * with any deliberate omission NAMED in `omits` and
132+ * re-measured. Every other pin in this file asks the TREE,
133+ * and the tree cannot answer "did the gate widen what it
134+ * declares" — #15201 is that gap measured: a gate grew a
135+ * third hint, this row kept describing two, and every
136+ * LIVE/PRECISE/COMPLETE pin stayed green because the
137+ * recorded spelling was still true OF THE TREE. No pin, no
138+ * verdict, the same way as below.
120139 * REFUSE-WIDE the population really IS the whole top-level root. A
121140 * declaration would be TRUE, and is refused anyway: it
122141 * names the gate for every card under a root the fleet
@@ -236,6 +255,23 @@ const POPULATION_CONSTANT = /^(?:[A-Z0-9_]*_ROOTS?|[A-Z0-9_]*_DIRS?|POPULATION|[
236255 * gate. ⛔ Its numbers are re-measured on the 2026-09-01 tree in BOTH terms and
237256 * NOT carried from the row they replace, which this docblock forbids by name.
238257 *
258+ * ⭐ That NINETEENTH row was RE-MEASURED on 2026-09-04 (#15201) — both terms
259+ * together on one tree, never refreshed apart, at commit a23603ea. Not a
260+ * re-decision: the verdict is the one it already carried, and no authorisation
261+ * sentence is claimed for it. What moved is the GATE. #15179 widened
262+ * `check-ratchet-remedy-authority`'s walk to a second directory read one level
263+ * (`scripts/` and `scripts/pm/`), so the row's shape claim — "no nested script
264+ * at any depth" — became false of the tree, its numbers described a smaller
265+ * walk, and its recorded spelling named two of the three hints the gate now
266+ * declares. ⚠️ Nothing reddened, and the reason is the point of this entry:
267+ * every pin in this file asks the TREE, the recorded two-hint spelling stayed
268+ * LIVE, PRECISE and COMPLETE over its own claim, and the term that had actually
269+ * moved — what the gate DECLARES — was not a term any pin read. The repair is
270+ * therefore two things and not one: the row re-measured, and the missing
271+ * direction pinned for every DECLARED-NARROWER record that carries a spelling
272+ * (see that verdict's definition above). ⛔ The gate itself is NOT edited from
273+ * here: it is correct, and the map is what went stale.
274+ *
239275 * ⚠️ One row of that seventeen was re-measured into a DIFFERENT population, not
240276 * merely fresher digits: #12392 (PR #12423, `69d0e18`) made
241277 * `check-skills-token-ratchet`'s walk RECURSIVE over whole skill directories, so
@@ -371,14 +407,23 @@ const SPELLINGS = new Map([
371407 holds : ( s ) => s [ 0 ] === 'packages' && s . length >= 2
372408 && ( s [ s . length - 1 ] === 'package.json' || s [ s . length - 1 ] === 'tsup.config.ts' ) ,
373409 } ] ,
374- [ 'scripts top-level script files' , {
410+ [ 'scripts walked-directory script files' , {
411+ // One hint per (walked directory, extension) pair the gate's walk really
412+ // admits -- the shape its own docblock argues for, and the reason this entry
413+ // is NOT `scripts/**`: neither directory is descended, so the recursive
414+ // spelling would name this gate for every nested script it never opens.
415+ // `scripts/pm/*.mts` is deliberately absent because that directory carries
416+ // no `.mts` file, and a hint reaching nothing is a dead declaration.
375417 segments : [
376418 [ 'scripts' , '*.mjs' ] ,
377419 [ 'scripts' , '*.mts' ] ,
420+ [ 'scripts' , 'pm' , '*.mjs' ] ,
378421 ] ,
379- claim : 'every `.mjs` or `.mts` file directly under the scripts root (non-recursive)' ,
380- holds : ( s ) => s . length === 2 && s [ 0 ] === 'scripts'
381- && ( s [ 1 ] . endsWith ( '.mjs' ) || s [ 1 ] . endsWith ( '.mts' ) ) ,
422+ claim : 'every `.mjs` or `.mts` file directly under the scripts root, or `.mjs` directly under '
423+ + 'its pm directory (non-recursive in both)' ,
424+ holds : ( s ) => s [ 0 ] === 'scripts'
425+ && ( ( s . length === 2 && ( s [ 1 ] . endsWith ( '.mjs' ) || s [ 1 ] . endsWith ( '.mts' ) ) )
426+ || ( s . length === 3 && s [ 1 ] === 'pm' && s [ 2 ] . endsWith ( '.mjs' ) ) ) ,
382427 } ] ,
383428] ) ;
384429
@@ -513,36 +558,47 @@ const TRIAGE = new Map([
513558 } ] ,
514559 [ 'scripts/check-ratchet-remedy-authority.mjs SCRIPTS_DIR scripts' , {
515560 verdict : 'DECLARED-NARROWER' ,
516- spelling : 'scripts top-level script files' ,
561+ spelling : 'scripts walked-directory script files' ,
517562 why : 'RE-DECIDED 2026-09-01 (#13813) from REFUSE-UNSPELLABLE, whose stated reason — "the idiom '
518563 + 'has no non-recursive spelling" — was TRUE when written and is FALSE of this tree. It '
519564 + 'rested on the deletion-collapse: a glob carrying a literal SUFFIX in the final segment '
520565 + 'was spliced WITHIN the segment, so the only non-recursive spelling reduced to a string no '
521566 + 'tree can hold and reached nothing. #13448 retired that collapse for exactly this shape — '
522567 + 'judgedAsPattern routes it to triggerCovers now — which is the same retired collapse the '
523568 + 'seventeen rows of 2026-08-26 and the eighteenth of 2026-08-29 were re-decided under, one '
524- + 'refinement finer, and this row is re-decided under that authorisation sentence and no '
525- + 'wider one. Both terms re-measured together on this tree, never refreshed apart: the gate '
526- + 'own corpusFiles() walk admits 183 files, against 310 tracked under the bare root (59%). '
527- + 'The gate now declares ONE hint per admitted extension beside SCRIPTS_DIR under the '
528- + 'ROOT_DIR_WATCH_HINTS idiom, and the pair is SET-EQUAL to that walk in both directions — '
529- + '183 of 183, nothing read left uncovered, nothing covered left unread — so 100% precise '
530- + 'and complete. The coupling described above was, at the time this row was decided, held '
531- + "only in the gate's own --self-test (which pins the hints against SCRIPTS_DIR and "
532- + 'CORPUS_EXTENSIONS and refuses both the subtree spelling and the brace form its own '
533- + 'messages print), because SPELLINGS held ONE hint per entry and this population needs one '
534- + 'per extension — the same shape as the check:logger-receiver-detach and '
535- + 'check:dual-build-cjs-loads rows above, and the gap #14233 measured and closed: SPELLINGS '
536- + 'now holds a LIST of segment arrays, so the pair is pinned HERE too, LIVE, PRECISE and '
537- + 'COMPLETE (183 of 310 tracked files under the bare root, matching the gate own walk exactly, '
538- + "re-measured 2026-09-01) — the gate's own pin and this one now independently corroborate "
539- + 'the same declaration rather than only one of them re-measuring it. The consumer is '
569+ + 'refinement finer, and this row was re-decided under that authorisation sentence and no '
570+ + 'wider one. RE-MEASURED 2026-09-04 (#15201) at commit a23603ea — both terms together on '
571+ + 'one tree and never refreshed apart — because #15179 widened the WALK under the row: '
572+ + 'corpusFiles() reads the scripts root AND its pm directory, one level each, and admits '
573+ + '202 files (188 at the top level, 14 in pm) against 324 tracked under the bare root, 62%. '
574+ + '⛔ The previous reading, 183 of 310, described the one-directory walk that preceded it '
575+ + 'and is superseded WHOLE — neither term is carried forward, which this docblock forbids '
576+ + 'by name. The gate declares ONE hint per (walked directory, extension) pair the walk '
577+ + 'admits, beside SCRIPTS_DIR under the ROOT_DIR_WATCH_HINTS idiom — three today, the pm '
578+ + 'mts pair deliberately absent because that directory holds no such file and a hint '
579+ + 'reaching nothing is a dead declaration — and the set is SET-EQUAL to that walk in both '
580+ + 'directions: 202 of 202, nothing read left uncovered, nothing covered left unread, so '
581+ + '100% precise and complete. SPELLINGS holds a LIST of segment arrays since #14233, so '
582+ + 'that set is pinned HERE too, LIVE, PRECISE and COMPLETE (202 of 324 tracked files under '
583+ + 'the bare root, matching the gate own walk exactly) — the same multi-hint shape as the '
584+ + 'check:logger-receiver-detach and check:dual-build-cjs-loads rows above. ⚠️ Those pins '
585+ + 'ask the TREE, and #15201 is the measurement that they are not sufficient alone: the walk '
586+ + 'widened, this row went on describing two hints, and every one of them stayed GREEN '
587+ + 'because the two-hint spelling was still true of the tree it was asked about. So the '
588+ + 'declaration is now pinned from both sides — the gate own --self-test derives its '
589+ + 'expected hints from corpusFiles() and holds its array to them (refusing the subtree '
590+ + 'spelling and the brace form its own messages print), while this file reads that same '
591+ + 'array out of the gate SOURCE and holds the recorded spelling set-equal to it, so the '
592+ + 'next widening of that walk reds in both places instead of neither. The consumer is '
540593 + 'MEASURED, not argued: before this, the derivation placed this family in the residue '
541594 + 'undetermined bucket, absent from the matched list a brief prints, and a PR that ran its '
542595 + 'whole derived family green locally lost a CI round to this gate. The row STAYS in the '
543596 + 'sweep because the bare root is still not covered — no arbitrary file at the top of the '
544- + 'root is reached, and no nested script at any depth — which is what this verdict says and '
545- + 'is correct, not outstanding debt' ,
597+ + 'root is reached, no nested directory other than pm is walked, and no level below either '
598+ + 'walked directory is descended — which is what this verdict says and is correct, not '
599+ + 'outstanding debt. ⚠️ The scripts one level down in pm ARE reached now: the flat "no '
600+ + 'nested script at any depth" this row carried until 2026-09-04 was true of the walk it '
601+ + 'was written about and false of this one' ,
546602 } ] ,
547603 // ── Refused: the population is the whole root, and the root is saturated ──
548604 [ 'scripts/check-skill-identifier-liveness.mjs IMPL_ROOTS packages' , {
@@ -793,6 +849,18 @@ const TRIAGE = new Map([
793849 [ 'packages/spec/scripts/build-skill-docs.ts SKILLS_DIR skills' , {
794850 verdict : 'DECLARED-NARROWER' ,
795851 spelling : 'skill entrypoints' ,
852+ // The one hint this gate declares at this root that the recorded spelling
853+ // does NOT name, restated where the declaration pin can read it (#15201).
854+ // Its reason is the `why` below, unchanged and not re-decided: no single
855+ // spelling of the idiom reaches both a per-skill SKILL.md and a file at the
856+ // root beside those directories. Naming it here is what keeps the pin
857+ // two-sided for this row — a hint the gate ADDS later is not covered by
858+ // this omission and reds, which is the whole point.
859+ // ⚠️ SEGMENTS, joined at runtime — the rule SPELLINGS states above, and it
860+ // is not decoration: spelled as a path literal this entry enters THIS
861+ // file's own hint set and hands a reporting tool a population it never
862+ // reads. The self-test that forbids it caught this exact line.
863+ omits : [ [ 'skills' , 'README.md' ] ] ,
796864 why : 'RE-POINTED 2026-09-01 (#13519) from SPELLABLE-UNDECLARED, and the deferral it replaces '
797865 + 'is not merely overruled — its own premise stopped holding. That record read: the '
798866 + 'population is 12 of 50 (24%), the recorded spelling is 100% PRECISE and deliberately '
@@ -1617,6 +1685,85 @@ function selfTest() {
16171685 rootFiles . length > 0 && covered . length < rootFiles . length ) ;
16181686 }
16191687
1688+ // ── The DECLARATION a row describes, read from the gate's own SOURCE ──────
1689+ //
1690+ // The one direction every pin above is blind to. They all ask the TREE: is
1691+ // the recorded spelling live, does it over-name, does it under-name. A gate
1692+ // that WIDENS what it declares moves none of those answers -- the old
1693+ // spelling goes on covering exactly what it always covered -- so a
1694+ // DECLARED-NARROWER record can stop describing its own gate with this whole
1695+ // battery green. #15201 is that measured, not argued:
1696+ // check-ratchet-remedy-authority grew a third hint, the row here went on
1697+ // naming two, and nothing reddened anywhere.
1698+ //
1699+ // So the gate's array is read back out of its SOURCE -- through the idiom's
1700+ // own parser, never a roster retyped here -- and held SET-EQUAL to the
1701+ // recorded spelling at THIS row's bare root. Two directions, plus one field
1702+ // for the single honest asymmetry:
1703+ //
1704+ // recorded in declared the record cannot claim a hint the gate never
1705+ // spelled; that is a declaration nobody made.
1706+ // declared in recorded the record cannot pass over a hint the gate DID
1707+ // spell -- unless the row NAMES it in `omits`, which
1708+ // is a measured claim of its own and is re-checked
1709+ // every run: an omission the gate stopped declaring
1710+ // reds exactly as loudly as one it started.
1711+ //
1712+ // ⛔ Only DECLARED-NARROWER rows carrying a `spelling` are asked, and that is
1713+ // a definition rather than an exemption: a SPELLABLE-UNDECLARED row IS the
1714+ // row whose gate declares nothing for that population, so the comparison
1715+ // there has its answer built into the verdict, and a row with no spelling
1716+ // records no set to compare. ⛔ When this reds the remedy is to re-measure
1717+ // the ROW -- never to extend `omits` until it agrees again, which is the same
1718+ // move as relaxing `holds`, refused above in as many words.
1719+ const declaredHintsAt = ( file , root ) => {
1720+ const abs = join ( ROOT , file ) ;
1721+ if ( ! existsSync ( abs ) ) return null ;
1722+ return auditSource ( file , readFileSync ( abs , 'utf8' ) )
1723+ . filter ( ( d ) => d . ok )
1724+ . flatMap ( ( d ) => d . hints )
1725+ . filter ( ( h ) => h . split ( '/' ) [ 0 ] === root ) ;
1726+ } ;
1727+ const sameSet = ( a , b ) => a . length === b . length
1728+ && [ ...a ] . sort ( ) . join ( '\n' ) === [ ...b ] . sort ( ) . join ( '\n' ) ;
1729+ t ( 'an `omits` field appears only on a DECLARED-NARROWER record that names a spelling, and is a '
1730+ + 'non-empty list of SEGMENT ARRAYS — it records what a DECLARATION says, so there is nothing '
1731+ + "for it to be about on any other row, and a path literal there would enter this file's own "
1732+ + 'hint set the way a spelling literal would' ,
1733+ [ ...TRIAGE . values ( ) ] . every ( ( v ) => ! v . omits
1734+ || ( v . verdict === 'DECLARED-NARROWER' && Boolean ( v . spelling )
1735+ && Array . isArray ( v . omits ) && v . omits . length > 0
1736+ && v . omits . every ( ( parts ) => Array . isArray ( parts ) && parts . length > 0
1737+ && parts . every ( ( p ) => typeof p === 'string' && p . length > 0 ) ) ) ) ) ;
1738+ let declPinned = 0 ;
1739+ for ( const [ key , v ] of TRIAGE ) {
1740+ if ( v . verdict !== 'DECLARED-NARROWER' || ! v . spelling ) continue ;
1741+ const [ file , , root ] = key . split ( ' ' ) ;
1742+ const recorded = hintsOf ( SPELLINGS . get ( v . spelling ) . segments ) . map ( ( parts ) => parts . join ( '/' ) ) ;
1743+ const declared = declaredHintsAt ( file , root ) ;
1744+ t ( `the gate source the row "${ key } " judges is readable from here — a row whose file moved `
1745+ + 'cannot have its declaration read, and reporting that as agreement would be the silence '
1746+ + 'this pin exists to break' , declared !== null ) ;
1747+ if ( declared === null ) continue ;
1748+ t ( `the spelling recorded for "${ key } " is rooted at that row's own bare root (${ root } ) — a `
1749+ + 'spelling paired with the wrong row would otherwise be compared against a declaration it '
1750+ + 'was never about' , recorded . length > 0 && recorded . every ( ( h ) => h . split ( '/' ) [ 0 ] === root ) ) ;
1751+ const unspelled = recorded . filter ( ( h ) => ! declared . includes ( h ) ) ;
1752+ t ( `every hint the row "${ key } " records is one its gate DECLARES at ${ root } ${ unspelled . length
1753+ ? ` — NOT DECLARED: ${ unspelled . join ( ' · ' ) } . The record claims a declaration the gate does `
1754+ + 'not make; re-measure the row.' : '' } `, unspelled . length === 0 ) ;
1755+ const unrecorded = declared . filter ( ( h ) => ! recorded . includes ( h ) ) ;
1756+ const named = ( v . omits ?? [ ] ) . map ( ( parts ) => parts . join ( '/' ) ) ;
1757+ t ( `…and every hint that gate declares at ${ root } is either recorded by "${ v . spelling } " or `
1758+ + `named in the row's \`omits\`${ sameSet ( unrecorded , named ) ? '' : ` — DRIFT: declared and `
1759+ + `not recorded [${ unrecorded . join ( ' · ' ) || 'none' } ], named as omitted `
1760+ + `[${ named . join ( ' · ' ) || 'none' } ]. The gate moved under the record: re-measure BOTH `
1761+ + 'terms of the row and re-decide it.' } `, sameSet ( unrecorded , named ) ) ;
1762+ declPinned += 1 ;
1763+ }
1764+ t ( 'the declaration pin judges something — at least one DECLARED-NARROWER record carries a '
1765+ + 'spelling to hold against its gate' , declPinned > 0 ) ;
1766+
16201767 // Every verdict must be one of the four the docblock defines, and every one
16211768 // must carry its measured reason — a bare verdict is the allowlist row this
16221769 // file exists not to become.
@@ -1681,7 +1828,10 @@ function selfTest() {
16811828 + "and no folded row's invocations disagree about reachability. "
16821829 + `${ spellingRows . length } record(s) carry a spelling and every one of ${ usedSpellings . size } `
16831830 + 'distinct spelling(s) is pinned LIVE, PRECISE and COMPLETE against the tracked corpus in '
1684- + "hintCovers' own terms. The recogniser is proven to speak and to discriminate (a "
1831+ + `hintCovers' own terms. ${ declPinned } DECLARED-NARROWER record(s) are held SET-EQUAL, in `
1832+ + "both directions, to their gate's own declared hint array read from its source, with "
1833+ + 'every deliberate omission named and re-measured. The recogniser is proven to speak and '
1834+ + 'to discriminate (a '
16851835 + 'separator-carrying and a dotted root are both refused as already visible), the '
16861836 + 'constant-name restriction is proven to restrict, and neither the triage keys nor this '
16871837 + `file declare any population of their own. ${ CENSUS_REFUSE_WIDE . size } CENSUS row(s) `
0 commit comments