104104// node scripts/check-ratchet-remedy-authority.mjs --self-test # the detector's own rules
105105
106106import { readFileSync , readdirSync } from 'node:fs' ;
107- import { join , dirname , resolve } from 'node:path' ;
107+ import { join , dirname , resolve , relative } from 'node:path' ;
108108import { fileURLToPath } from 'node:url' ;
109109import process from 'node:process' ;
110110import { isEntrypoint } from './invoked-as.mjs' ;
@@ -115,6 +115,72 @@ const SCRIPTS_DIR = join(REPO_ROOT, 'scripts');
115115const SELF_FILE = 'check-ratchet-remedy-authority.mjs' ;
116116const SELF = `scripts/${ SELF_FILE } ` ;
117117
118+ /**
119+ * The two extensions the corpus walk admits, as ONE constant rather than two
120+ * `endsWith` calls inline — so the declaration below can be held against the
121+ * thing the sweep really filters on instead of against a second copy of it.
122+ * The `.mts` half is the #8538 blind spot and is not decoration: see the header.
123+ */
124+ const CORPUS_EXTENSIONS = [ '.mjs' , '.mts' ] ;
125+
126+ /**
127+ * The population this gate READS, declared for `scripts/pm/dispatch-gates.mjs`
128+ * -- the `ROOT_DIR_WATCH_HINTS` idiom, spelled as a literal array because the
129+ * hint extractor reads SOURCE TEXT (`scripts/check-watch-hint-literal.mjs`
130+ * holds that spelling for every declarer in the tree).
131+ *
132+ * ## Why this gate declared NOTHING, and what that cost (#13813)
133+ *
134+ * `extractWatchHints` scans the module body for path-shaped literals. This file
135+ * builds its population at RUNTIME from `readdirSync(SCRIPTS_DIR)`, and the
136+ * string naming that population appeared only in the failure text, in the
137+ * `--list` banner and in the header comment -- never as a literal the scanner
138+ * could read. So the derivation read no population here and made no claim, and
139+ * the family landed in the residue's `undetermined` bucket: not matched, not
140+ * known-irrelevant, and absent from the `matched` list a dispatch brief prints.
141+ *
142+ * That is not a theoretical gap. A PR touching three `scripts/**` files ran its
143+ * whole derived family locally, green, and CI reddened on THIS gate -- both
144+ * readings true, of the same tree. Eight families matched those same three
145+ * paths, every one of them at `gate source 'scripts/**'`, because their
146+ * populations are spelled where the derivation can read them.
147+ *
148+ * ## Why the spelling is the flat-directory glob and NOT `scripts/**`
149+ *
150+ * `corpusFiles()` is a NON-RECURSIVE `readdirSync` admitted by extension. It
151+ * reads the top level of `scripts/` only -- never `scripts/pm/`,
152+ * `scripts/docs-audit/` or any other nested directory -- and only the two
153+ * extensions above. `scripts/**` would be the easy spelling and it would be
154+ * FALSE: it pastes this gate onto every card touching any nested script, for
155+ * files this gate never opens, which is the costlier error `hintCovers`' own
156+ * docblock prices above a missing declaration.
157+ *
158+ * The two hints here are 100% precise AND complete against this tree, set-equal
159+ * in both directions to what `corpusFiles()` walks -- 183 of 183, against 310
160+ * tracked files under the bare root -- and the self-test below re-measures that
161+ * rather than trusting this sentence.
162+ *
163+ * ⚠️ NOT `scripts/*.{mjs,mts}`, however natural it looks beside the header and
164+ * the failure text, which both spell the corpus that way for a human reader.
165+ * `hintCovers` has no brace expansion, so that one string reaches NOTHING --
166+ * a dead declaration, printed as the same silence as declaring nothing at all.
167+ * One hint per extension is the spelling that is live, and a self-test case
168+ * pins the brace form as dead so the tidier spelling cannot quietly land.
169+ *
170+ * ## Why this spelling exists at all today
171+ *
172+ * `scripts/pm/bare-root-worklist.mjs` recorded this population as
173+ * REFUSE-UNSPELLABLE on the grounds that "the idiom has no non-recursive
174+ * spelling". That was TRUE when it was written: `collapseHint` deleted the glob
175+ * and spliced the remainder inside the final segment, so this hint collapsed to
176+ * `scripts/.mjs`, a string no tree can hold. #13448 retired that collapse for
177+ * exactly this shape -- a glob carrying a literal SUFFIX in the final segment is
178+ * MATCHED now, through `judgedAsPattern`/`triggerCovers`, not collapsed -- so
179+ * the refusal became false of the tree without anything reddening. That row is
180+ * re-decided alongside this declaration, in the file that owns it.
181+ */
182+ const ROOT_DIR_WATCH_HINTS = [ 'scripts/*.mjs' , 'scripts/*.mts' ] ;
183+
118184/** The compliance token. Byte-identical to every instrumented gate's const. */
119185const RATCHET_AUTHORITY_MARKER = '⛔ MAINTAINER-ONLY' ;
120186
@@ -838,7 +904,7 @@ const CONTROL = {
838904/** @returns {string[] } corpus filenames, sorted. `*.{mjs,mts}` — never `*.mjs`. */
839905export function corpusFiles ( ) {
840906 return readdirSync ( SCRIPTS_DIR )
841- . filter ( ( f ) => f . endsWith ( '.mjs' ) || f . endsWith ( '.mts' ) )
907+ . filter ( ( f ) => CORPUS_EXTENSIONS . some ( ( ext ) => f . endsWith ( ext ) ) )
842908 . sort ( ) ;
843909}
844910
@@ -1119,6 +1185,79 @@ function selfTest() {
11191185 + 'control is a declaration registry, and its offer-shaped quotes live in comments)' ,
11201186 results . get ( SELF_FILE ) !== undefined && results . get ( SELF_FILE ) . verdict === 'excluded' ) ;
11211187
1188+ // ── (20) The declared population, held to the walk in BOTH directions (#13813)
1189+ //
1190+ // ⛔ Checked with a LOCAL matcher rather than by importing
1191+ // `scripts/pm/dispatch-gates.mjs`: the import specifier would itself be a path
1192+ // literal in this file's source, so the derivation would hand this gate that
1193+ // module's declared population as if it were this gate's own — a fabricated
1194+ // watch surface. (The same refusal `check-logger-receiver-detach.mjs` records.)
1195+ // The liveness of these hints in `hintCovers`' own terms is pinned where
1196+ // `hintCovers` is already in scope: `scripts/pm/bare-root-worklist.mjs`.
1197+ //
1198+ // Both directions, because either alone passes against the defect this closes.
1199+ // "The declaration is non-empty" was true of every gate that ever went silent;
1200+ // "it names the root" is true of `scripts/**`, which is the FALSE spelling
1201+ // here. What is asserted is that the hints are a function of the two constants
1202+ // the walk is a function of — move the read and this reds, in this file.
1203+ const CORPUS_ROOT = relative ( REPO_ROOT , SCRIPTS_DIR ) . split ( '\\' ) . join ( '/' ) ;
1204+ expect ( 'declaration — one hint per admitted extension, each the flat-directory glob under the '
1205+ + `very root the walk reads from (declared: ${ JSON . stringify ( ROOT_DIR_WATCH_HINTS ) } , root: `
1206+ + `${ CORPUS_ROOT } , extensions: ${ CORPUS_EXTENSIONS . join ( ' ' ) } )` ,
1207+ ROOT_DIR_WATCH_HINTS . length === CORPUS_EXTENSIONS . length
1208+ && CORPUS_EXTENSIONS . every ( ( ext ) => ROOT_DIR_WATCH_HINTS . includes ( `${ CORPUS_ROOT } /*${ ext } ` ) ) ) ;
1209+
1210+ // ⛔ The subtree spelling is the one this declaration must never take. The walk
1211+ // is a NON-RECURSIVE readdir; `scripts/**` would name this gate for every
1212+ // nested script it never opens, which is the costlier error the idiom prices
1213+ // above a missing declaration.
1214+ expect ( 'declaration — the subtree spelling is refused: the walk is non-recursive, so no hint '
1215+ + 'ends in a subtree glob and none collapses back to the bare root' ,
1216+ ! ROOT_DIR_WATCH_HINTS . some ( ( h ) => h . endsWith ( '/**' ) || h . replace ( / \/ \* + $ / , '' ) === CORPUS_ROOT ) ) ;
1217+
1218+ // The local matcher for `<root>/*<ext>`: one directory level, one extension.
1219+ const declaresPath = ( p ) => ROOT_DIR_WATCH_HINTS . some ( ( h ) => {
1220+ const m = / ^ ( [ ^ / ] + ) \/ \* ( \. [ a - z ] + ) $ / . exec ( h ) ;
1221+ return Boolean ( m ) && p . startsWith ( `${ m [ 1 ] } /` )
1222+ && ! p . slice ( m [ 1 ] . length + 1 ) . includes ( '/' ) && p . endsWith ( m [ 2 ] ) ;
1223+ } ) ;
1224+ const corpusPaths = corpusFiles ( ) . map ( ( f ) => `${ CORPUS_ROOT } /${ f } ` ) ;
1225+ const unnamed = corpusPaths . filter ( ( p ) => ! declaresPath ( p ) ) ;
1226+ expect ( 'declaration — COMPLETE: every file the sweep opens is named by a declared hint '
1227+ + `(${ corpusPaths . length } swept, ${ unnamed . length } unnamed`
1228+ + `${ unnamed . length ? `: ${ unnamed . slice ( 0 , 3 ) . join ( ', ' ) } ` : '' } )` ,
1229+ corpusPaths . length > 0 && unnamed . length === 0 ) ;
1230+ expect ( 'declaration — no hint is DEAD: each one names at least one file this gate really reads' ,
1231+ ROOT_DIR_WATCH_HINTS . every ( ( h ) => corpusPaths . some ( ( p ) => declaresPath ( p ) && p . endsWith ( h . slice ( h . indexOf ( '*' ) + 1 ) ) ) ) ) ;
1232+
1233+ // PRECISE, against the two shapes `scripts/**` would wrongly sweep in, both
1234+ // taken from the tree rather than spelled: a nested script, and a top-level
1235+ // file at an extension the walk does not admit. Non-vacuous by construction —
1236+ // the assertion fails if the tree stops carrying an example of either.
1237+ const topLevel = readdirSync ( SCRIPTS_DIR , { withFileTypes : true } ) ;
1238+ const nested = topLevel
1239+ . filter ( ( e ) => e . isDirectory ( ) )
1240+ . flatMap ( ( d ) => readdirSync ( join ( SCRIPTS_DIR , d . name ) )
1241+ . filter ( ( f ) => CORPUS_EXTENSIONS . some ( ( ext ) => f . endsWith ( ext ) ) )
1242+ . map ( ( f ) => `${ CORPUS_ROOT } /${ d . name } /${ f } ` ) ) ;
1243+ expect ( 'declaration — PRECISE against depth: the tree carries nested scripts at the admitted '
1244+ + `extensions (${ nested . length } ) and the declaration names NONE of them` ,
1245+ nested . length > 0 && ! nested . some ( declaresPath ) ) ;
1246+ const otherExt = topLevel
1247+ . filter ( ( e ) => e . isFile ( ) && ! CORPUS_EXTENSIONS . some ( ( ext ) => e . name . endsWith ( ext ) ) )
1248+ . map ( ( e ) => `${ CORPUS_ROOT } /${ e . name } ` ) ;
1249+ expect ( 'declaration — PRECISE against extension: the root carries top-level files the walk does '
1250+ + `not admit (${ otherExt . length } ) and the declaration names NONE of them` ,
1251+ otherExt . length > 0 && ! otherExt . some ( declaresPath ) ) ;
1252+
1253+ // ⚠️ The brace form is what the header, the `--list` banner and the OK line all
1254+ // spell for a human reader, and it is DEAD as a hint — no brace expansion in
1255+ // the matcher, so it would reach nothing while reading like a declaration.
1256+ // Pinned so the tidier-looking spelling cannot quietly replace the live one.
1257+ expect ( 'declaration — the brace form the messages spell is NOT what is declared (it reaches '
1258+ + 'nothing, and a dead declaration prints as the same silence as declaring nothing)' ,
1259+ ! ROOT_DIR_WATCH_HINTS . some ( ( h ) => h . includes ( '{' ) ) ) ;
1260+
11221261 if ( failures . length > 0 ) {
11231262 for ( const f of failures ) console . error ( ` x self-test: ${ f } ` ) ;
11241263 console . error ( `\ncheck-ratchet-remedy-authority --self-test: ${ failures . length } failure(s).\n` ) ;
0 commit comments