2121 * rest>." (`ui/dashboard.zod.ts` `compareTo.offset` is the model; the script
2222 * node's `config.actionType` is the other member.)
2323 *
24+ * [#7030] Widened, not duplicated: `packages/lint/src/validate-expressions.ts`
25+ * carries one live occurrence of the identical sentence (the lint diagnostic
26+ * for a script node's retired dispatch keys — same #6856 ruling, same
27+ * false-antecedent risk, since that branch too can DELETE the key rather than
28+ * rewrite it into anything). `judgeMigrateSentences` is a plain text scan with
29+ * no dependency on `retiredKey()` or on anything `packages/spec`-specific, so
30+ * this pin's INPUT (the CORPORA it walks) widens for free — the matching
31+ * mechanism below is unchanged. A second, standalone pin over that one lint
32+ * site could only drift from this one the moment either wording changes;
33+ * one pin covering both corpora cannot.
34+ *
2435 * Mechanism: a SOURCE scan over string literals (this pin pins textual facts —
2536 * the sentences ARE text in source). Comment lines are skipped: descriptive
2637 * prose about the tool ("`os migrate meta` rewrites sources") is not a
2738 * prescription. `migrations/registry.ts` is out of scope structurally — it is
2839 * the migration LEDGER, whose `notes` are release prose over whole migrations,
2940 * not tombstone prescriptions an author meets in a parse error. That is a
30- * scope bound on the corpus, not a per-site exemption: every prescription
31- * string in every schema file is judged, with no allowlist.
41+ * scope bound on the spec corpus, not a per-site exemption: every
42+ * prescription string in every scanned file is judged, with no allowlist.
3243 *
3344 * What this pin deliberately does NOT check: a tombstone whose prescription
3445 * carries no `os migrate meta` sentence at all (#6914's worklist) — absence of
@@ -43,10 +54,33 @@ import url from 'node:url';
4354import { describe , expect , it } from 'vitest' ;
4455
4556const HERE = path . dirname ( url . fileURLToPath ( import . meta. url ) ) ;
46- const SRC_ROOT = path . resolve ( HERE , '..' ) ;
57+ const SPEC_SRC_ROOT = path . resolve ( HERE , '..' ) ;
58+ /** #7030: `packages/lint/src`, the one other corpus carrying this sentence. */
59+ const LINT_SRC_ROOT = path . resolve ( HERE , '../../../lint/src' ) ;
60+
61+ /** One scanned corpus: a root directory, plus its own out-of-scope exemptions. */
62+ interface Corpus {
63+ /** Short label, used as the `file` prefix on judged sites (e.g. `spec:`, `lint:`). */
64+ name : string ;
65+ root : string ;
66+ /** Paths relative to `root` that are structurally out of scope (see module doc). */
67+ outOfScope : Set < string > ;
68+ }
4769
48- /** The migration ledger — release prose, not tombstone prescriptions (see module doc). */
49- const OUT_OF_SCOPE = new Set ( [ path . join ( 'migrations' , 'registry.ts' ) ] ) ;
70+ const CORPORA : Corpus [ ] = [
71+ {
72+ name : 'spec' ,
73+ root : SPEC_SRC_ROOT ,
74+ // The migration ledger — release prose, not tombstone prescriptions (see module doc).
75+ outOfScope : new Set ( [ path . join ( 'migrations' , 'registry.ts' ) ] ) ,
76+ } ,
77+ {
78+ // #7030: `validate-expressions.ts`'s script-node lint diagnostic is the only site.
79+ name : 'lint' ,
80+ root : LINT_SRC_ROOT ,
81+ outOfScope : new Set ( ) ,
82+ } ,
83+ ] ;
5084
5185const MARKER = / (?: R u n ) ? ` o s m i g r a t e m e t a - - f r o m \d + ` / g;
5286
@@ -66,7 +100,7 @@ const MIXED_AT_MARKER =
66100 / ^ R u n ` o s m i g r a t e m e t a - - f r o m \d + ` t o r e w r i t e t h e [ ^ ; ' " ] + c a s e [ ^ ; ' " ] * a u t o m a t i c a l l y ; [ ^ ; ' " ] + \. [ ' " ] / ;
67101
68102interface JudgedSite {
69- /** Path relative to `packages/ spec/src `. */
103+ /** Corpus-prefixed path, e.g. ` spec:data/object.zod.ts` or `lint:validate-expressions.ts `. */
70104 file : string ;
71105 /** 1-based line of the sentence's marker (best effort across concatenation). */
72106 line : number ;
@@ -129,16 +163,18 @@ function* walk(dir: string): Generator<string> {
129163
130164function judgeTree ( ) : JudgedSite [ ] {
131165 const all : JudgedSite [ ] = [ ] ;
132- for ( const file of walk ( SRC_ROOT ) ) {
133- const rel = path . relative ( SRC_ROOT , file ) ;
134- if ( OUT_OF_SCOPE . has ( rel ) ) continue ;
135- all . push ( ...judgeMigrateSentences ( fs . readFileSync ( file , 'utf8' ) , rel ) ) ;
166+ for ( const corpus of CORPORA ) {
167+ for ( const file of walk ( corpus . root ) ) {
168+ const rel = path . relative ( corpus . root , file ) ;
169+ if ( corpus . outOfScope . has ( rel ) ) continue ;
170+ all . push ( ...judgeMigrateSentences ( fs . readFileSync ( file , 'utf8' ) , `${ corpus . name } :${ rel } ` ) ) ;
171+ }
136172 }
137173 return all ;
138174}
139175
140- describe ( 'retiredKey() `os migrate meta` sentences are the house sentence (#6856 route D)' , ( ) => {
141- it ( 'every prescription sentence in packages/spec/src is house-form or MIXED two-clause' , ( ) => {
176+ describe ( '`os migrate meta` sentences are the house sentence, across corpora (#6856 route D, widened #7030 )' , ( ) => {
177+ it ( 'every prescription sentence in packages/spec/src and packages/lint/src is house-form or MIXED two-clause' , ( ) => {
142178 const judged = judgeTree ( ) ;
143179 const violations = judged . filter ( ( j ) => ! j . ok ) ;
144180 expect (
@@ -149,17 +185,30 @@ describe('retiredKey() `os migrate meta` sentences are the house sentence (#6856
149185 ) . toEqual ( [ ] ) ;
150186 } ) ;
151187
152- it ( 'anti-vacuity: the scanner actually judges the corpus (floor, not a census)' , ( ) => {
153- // 54 prescription sentences at the time of the sweep. The floor guards
154- // against the SCANNER going blind (a regex or comment-filter regression
155- // reporting an empty corpus as green), not against tombstones aging out —
156- // lower it deliberately, with the removal that shrinks the corpus, when
157- // that day comes. #6914's 35 pending sentences will only raise the count.
188+ it ( 'anti-vacuity: the scanner actually judges every corpus (floor, not a census)' , ( ) => {
189+ // 54 prescription sentences in packages/spec/src at the time of the #6856
190+ // sweep; packages/lint/src contributes one more under #7030's widened
191+ // scan. The floor guards against a SCANNER going blind on either corpus (a
192+ // regex or comment-filter regression reporting an empty tree as green),
193+ // not against tombstones aging out — lower it deliberately, with the
194+ // removal that shrinks a corpus, when that day comes. #6914's 35 pending
195+ // sentences will only raise the count further.
158196 const judged = judgeTree ( ) ;
159197 expect ( judged . length ) . toBeGreaterThanOrEqual ( 50 ) ;
160198 expect ( judged . every ( ( j ) => j . ok ) ) . toBe ( true ) ;
161199 } ) ;
162200
201+ it ( 'anti-vacuity: the lint corpus specifically is reached, not just outnumbered by spec' , ( ) => {
202+ // The combined floor above (>=50) is already satisfied by packages/spec/src
203+ // alone, so a broken LINT_SRC_ROOT (wrong relative path, corpus silently
204+ // walking zero files) would NOT fail it — this assertion is the one thing
205+ // that actually exercises #7030's widening rather than merely declaring it.
206+ const judged = judgeTree ( ) ;
207+ const lintSites = judged . filter ( ( j ) => j . file . startsWith ( 'lint:' ) ) ;
208+ expect ( lintSites . length ) . toBeGreaterThanOrEqual ( 1 ) ;
209+ expect ( lintSites . every ( ( j ) => j . ok ) ) . toBe ( true ) ;
210+ } ) ;
211+
163212 it ( 'goes RED on the retired "rewrite it" spelling, naming the site' , ( ) => {
164213 const planted = [
165214 "const X = retiredKey(" ,
0 commit comments