3838 * ## What counts as a loader
3939 *
4040 * Anything that names the artifact — an `import`, a `readFileSync`, a glob
41- * whose pattern names the file or its directory. All of them must SPELL the
42- * file or the fixtures directory somewhere in source, so a text scan over the
43- * tracked tree is a sound instrument for the question. Two dispositions are
44- * allowed and every other reference fails:
41+ * whose pattern names the file or its directory. The scan reads source TEXT,
42+ * so what it checks is a spelling ({@link referencesArtifact}): the file's
43+ * basename; a path that resolves to the fixtures directory once case, `\`,
44+ * `.`, `..` and `//` are folded and a relative path is joined to the
45+ * directory of the file that holds it; or `__fixtures__` with `hotcrm` or
46+ * `.artifact` on one line. A text scan cannot follow a COMPUTED path — a
47+ * concatenation, a `.replace`, a variable, a base other than the holding
48+ * file's directory or the repository root — and this one does not expand a
49+ * wildcard in a directory segment, so a loader that reaches the artifact only
50+ * that way, with neither marker on its line, is outside what this gate sees.
51+ * Two dispositions are allowed and every other reference fails:
4552 *
4653 * TEST a `*.test.ts` / `*.spec.ts` file, or a file under a `test/`,
4754 * `tests/` or `__tests__/` directory. A test composes nothing.
@@ -84,6 +91,9 @@ const ARTIFACT_BASENAME = 'hotcrm-17.1-built-permissions.artifact.json';
8491/** The directory the artifact lives in: a reference to it is a reference to the artifact. */
8592const FIXTURES_DIR = 'packages/metadata/src/__fixtures__' ;
8693
94+ /** {@link FIXTURES_DIR } in the form rule 2 compares a folded spelling against. */
95+ const FIXTURES_DIR_FOLDED = FIXTURES_DIR . toLowerCase ( ) ;
96+
8797/** The two permission-set names that make this artifact load-bearing for #13419. */
8898const FOLDED_NAMES = [ 'sales_rep' , 'sales_manager' ] ;
8999
@@ -137,7 +147,9 @@ export function classifyReference(relPath) {
137147 * only one of them is a literal path:
138148 *
139149 * 1. the basename — an `import`, a `readFileSync`, a `join(HERE, '…')`;
140- * 2. the repo-relative fixtures directory — a config or workflow path;
150+ * 2. the fixtures directory in any spelling that RESOLVES to it
151+ * ({@link namesFixturesDir}) — a config or workflow path, a relative
152+ * climb from the file that holds it, a case variant;
141153 * 3. `__fixtures__` on the SAME LINE as `hotcrm` or `.artifact` — the glob
142154 * shape (`__fixtures__/*.artifact.json`), which names neither the file nor
143155 * the full directory and is invisible to 1 and 2.
@@ -161,32 +173,96 @@ export function classifyReference(relPath) {
161173 * - no `..` segment;
162174 * - after `path.posix.normalize` (so `packages/./metadata/…` and
163175 * `packages/metadata/./src/…` are read as the directories they are), it
164- * does not lie in this package's fixtures directory;
176+ * does not lie in this package's fixtures directory, judged by the one
177+ * folded comparison rule 2 uses ({@link liesInFixturesDir});
165178 * - the normalized token is one literal file path, `packages/<dirs>/src/
166179 * __fixtures__/<file>` with only word characters, `.` and `-` in its
167180 * segments — so a token carrying `*`, `?`, `[`, `{` or `${` is never set
168181 * aside.
169182 *
170183 * Only such a token leaves the line; the rest of the line is still judged, so
171- * a bare glob beside it still counts. This package's own directory, spelled
172- * literally , is rule 2's, before rule 3 is asked at all.
184+ * a bare glob beside it still counts. This package's own directory, in any
185+ * spelling rule 2 resolves , is rule 2's, before rule 3 is asked at all.
173186 */
174187const PATH_TOKEN = / (?< = ^ | [ \s ' " ` , [ ( ] ) p a c k a g e s \/ [ ^ \s ' " ` , \] ) ] * / g;
175188const LITERAL_OTHER_FIXTURE_FILE = / ^ p a c k a g e s \/ (?: [ \w . - ] + \/ ) + s r c \/ _ _ f i x t u r e s _ _ \/ [ \w . - ] + (?: \/ [ \w . - ] + ) * $ / ;
176189
177190export function isOtherPackageFixturePath ( token ) {
178191 if ( token . split ( '/' ) . includes ( '..' ) ) return false ;
179192 const normalized = posix . normalize ( token ) ;
180- if ( normalized === FIXTURES_DIR || normalized . startsWith ( ` ${ FIXTURES_DIR } /` ) ) return false ;
193+ if ( liesInFixturesDir ( foldPath ( normalized ) ) ) return false ;
181194 return LITERAL_OTHER_FIXTURE_FILE . test ( normalized ) ;
182195}
183196
184197function withoutOtherPackageFixturePaths ( line ) {
185198 return line . replace ( PATH_TOKEN , ( token ) => ( isOtherPackageFixturePath ( token ) ? '' : token ) ) ;
186199}
187200
188- export function referencesArtifact ( text ) {
189- if ( text . includes ( ARTIFACT_BASENAME ) || text . includes ( FIXTURES_DIR ) ) return true ;
201+ /**
202+ * Rule 2: does this text spell a path that resolves to the fixtures directory,
203+ * or into it?
204+ *
205+ * The repo-relative literal is one spelling of the directory among several,
206+ * and the others reach it just the same: `../../metadata/src/__fixtures__`
207+ * from a file in `packages/objectql/src/`, `packages/Metadata/src/__fixtures__`
208+ * on a case-insensitive filesystem, `packages\metadata\src\__fixtures__` on
209+ * Windows. So every path spelling ({@link PATH_SPELLING}) on a line that
210+ * carries `__fixtures__` is FOLDED ({@link foldPath}) and read two ways:
211+ *
212+ * - as written: the folded spelling holds the directory's four segments in a
213+ * row — a repo-relative path, `ROOT + '/packages/…'`, a URL, a vendored
214+ * prefix, a climb that comes back in;
215+ * - from the file that holds it: a relative spelling, joined to that file's
216+ * own directory, lands on the directory or under it. A relative climb can
217+ * only be resolved against the file that contains it, which is why this
218+ * rule takes `relPath`.
219+ *
220+ * Segments are compared, never substrings:
221+ * `packages/services/service-metadata/src/__fixtures__` contains the string
222+ * `metadata/src/__fixtures__` and is another package's directory. The literal
223+ * repo-relative substring is still asked first, unchanged, so the rule counts
224+ * every file it counted before it learned to fold.
225+ */
226+ const PATH_SPELLING = / [ ^ \s ' " ` , ; : = + ( ) [ \] { } < > | & ] + / g;
227+
228+ /** One path spelling in the form rule 2 compares: `\` read as `/`, `.`, `..` and `//` resolved, case folded. */
229+ function foldPath ( spelling ) {
230+ return posix . normalize ( spelling . replace ( / \\ + / g, '/' ) ) . toLowerCase ( ) ;
231+ }
232+
233+ /** Is this folded path the fixtures directory, or something under it? */
234+ function liesInFixturesDir ( folded ) {
235+ return folded === FIXTURES_DIR_FOLDED || folded . startsWith ( `${ FIXTURES_DIR_FOLDED } /` ) ;
236+ }
237+
238+ function namesFixturesDir ( text , relPath ) {
239+ if ( text . includes ( FIXTURES_DIR ) ) return true ;
240+ if ( ! / _ _ f i x t u r e s _ _ / i. test ( text ) ) return false ;
241+ const holder = posix . dirname ( relPath ) ;
242+ for ( const line of text . split ( '\n' ) ) {
243+ if ( ! line . toLowerCase ( ) . includes ( '__fixtures__' ) ) continue ;
244+ for ( const spelling of line . match ( PATH_SPELLING ) ?? [ ] ) {
245+ const folded = foldPath ( spelling ) ;
246+ if ( `/${ folded } /` . includes ( `/${ FIXTURES_DIR_FOLDED } /` ) ) return true ;
247+ if ( ! folded . startsWith ( '/' ) && liesInFixturesDir ( foldPath ( `${ holder } /${ spelling } ` ) ) ) return true ;
248+ }
249+ }
250+ return false ;
251+ }
252+
253+ /**
254+ * `relPath` is the repo-relative path of the file `text` was read from, and it
255+ * is required: rule 2 resolves a relative spelling against it, so a caller
256+ * that dropped it would lose every relative climb without a word.
257+ */
258+ export function referencesArtifact ( text , relPath ) {
259+ if ( typeof relPath !== 'string' || relPath === '' ) {
260+ throw new TypeError (
261+ 'referencesArtifact(text, relPath): relPath, the repo-relative path of the file holding the text, '
262+ + 'is required. A relative climb can only be resolved against the file that contains it.' ,
263+ ) ;
264+ }
265+ if ( text . includes ( ARTIFACT_BASENAME ) || namesFixturesDir ( text , relPath ) ) return true ;
190266 for ( const raw of text . split ( '\n' ) ) {
191267 if ( ! raw . includes ( '__fixtures__' ) ) continue ;
192268 const line = withoutOtherPackageFixturePaths ( raw ) ;
@@ -231,8 +307,8 @@ export function scanTree(root = REPO_ROOT) {
231307 } catch {
232308 continue ;
233309 }
234- if ( ! referencesArtifact ( text ) ) continue ;
235310 const rel = relative ( root , abs ) . split ( sep ) . join ( '/' ) ;
311+ if ( ! referencesArtifact ( text , rel ) ) continue ;
236312 references . push ( { file : rel , disposition : classifyReference ( rel ) } ) ;
237313 }
238314 references . sort ( ( a , b ) => a . file . localeCompare ( b . file ) ) ;
@@ -356,7 +432,7 @@ let selfTestReachedVerdict = false;
356432const SELF_TEST_BATTERIES = Object . freeze ( {
357433 'REVERSE CONTROL. A zero from a scan that cannot report anything is not a' : 3 ,
358434 'The allowed dispositions, each for its own stated reason.' : 5 ,
359- 'The text matcher, both directions. A matcher that never matches turns' : 20 ,
435+ 'The text matcher, both directions. A matcher that never matches turns' : 29 ,
360436 'The permission-set extractor, on the real artifact shape.' : 2 ,
361437} ) ;
362438
@@ -430,90 +506,145 @@ function selfTest() {
430506 // every loader into a silent pass, and it is the half a clean tree cannot
431507 // exercise from the negative side alone.
432508 battery ( 'The text matcher, both directions. A matcher that never matches turns' ) ;
433- check ( 'matcher sees the basename' , referencesArtifact ( `join(HERE, '__fixtures__/${ ARTIFACT_BASENAME } ')` ) , true ) ;
434- check ( 'matcher sees a fixtures-directory glob' , referencesArtifact ( `glob('${ FIXTURES_DIR } /*.artifact.json')` ) , true ) ;
435- check ( 'matcher ignores an unrelated hotcrm mention' , referencesArtifact ( "// the HotCRM shape from hotcrm#788" ) , false ) ;
436- check ( 'matcher sees a bare glob sweep of the fixtures dir' , referencesArtifact ( "await glob('**/__fixtures__/*.artifact.json')" ) , true ) ;
509+ // The file a case's text is read as coming from. Rule 2 resolves a relative
510+ // spelling against it, so a case that is ABOUT where the text lives names its
511+ // own file instead.
512+ const PROBE_FILE = 'packages/objectql/src/loader.ts' ;
513+ check ( 'matcher sees the basename' , referencesArtifact ( `join(HERE, '__fixtures__/${ ARTIFACT_BASENAME } ')` , PROBE_FILE ) , true ) ;
514+ check ( 'matcher sees a fixtures-directory glob' , referencesArtifact ( `glob('${ FIXTURES_DIR } /*.artifact.json')` , PROBE_FILE ) , true ) ;
515+ check ( 'matcher ignores an unrelated hotcrm mention' , referencesArtifact ( "// the HotCRM shape from hotcrm#788" , PROBE_FILE ) , false ) ;
516+ check ( 'matcher sees a bare glob sweep of the fixtures dir' , referencesArtifact ( "await glob('**/__fixtures__/*.artifact.json')" , PROBE_FILE ) , true ) ;
437517 check (
438518 'matcher does NOT credit __fixtures__ and hotcrm on separate lines' ,
439- referencesArtifact ( "import x from './__fixtures__/widget.json';\n// unrelated: hotcrm#788\n" ) ,
519+ referencesArtifact ( "import x from './__fixtures__/widget.json';\n// unrelated: hotcrm#788\n" , PROBE_FILE ) ,
440520 false ,
441521 ) ;
442522 check (
443523 "matcher does NOT count a full path to another package's fixture" ,
444- referencesArtifact ( " 'packages/objectql/src/__fixtures__/hotcrm-4.0.1-published-pages.artifact.json'," ) ,
524+ referencesArtifact ( " 'packages/objectql/src/__fixtures__/hotcrm-4.0.1-published-pages.artifact.json'," , PROBE_FILE ) ,
445525 false ,
446526 ) ;
447527 check (
448528 "matcher does NOT count a full path to a nested package's fixture" ,
449- referencesArtifact ( "readFileSync('packages/services/service-x/src/__fixtures__/hotcrm-1.artifact.json')" ) ,
529+ referencesArtifact ( "readFileSync('packages/services/service-x/src/__fixtures__/hotcrm-1.artifact.json')" , PROBE_FILE ) ,
450530 false ,
451531 ) ;
452532 check (
453533 'matcher still sees a bare fixtures glob' ,
454- referencesArtifact ( "globs: ['__fixtures__/*.artifact.json']," ) ,
534+ referencesArtifact ( "globs: ['__fixtures__/*.artifact.json']," , PROBE_FILE ) ,
455535 true ,
456536 ) ;
457537 check (
458538 'matcher still sees a path into this package\'s own fixtures directory' ,
459- referencesArtifact ( "readFileSync('packages/metadata/src/__fixtures__/x.json')" ) ,
539+ referencesArtifact ( "readFileSync('packages/metadata/src/__fixtures__/x.json')" , PROBE_FILE ) ,
460540 true ,
461541 ) ;
462542 check (
463543 'matcher still sees a bare glob beside another package\'s fixture path on one line' ,
464- referencesArtifact ( "['packages/objectql/src/__fixtures__/a.json', '**/__fixtures__/*.artifact.json']" ) ,
544+ referencesArtifact ( "['packages/objectql/src/__fixtures__/a.json', '**/__fixtures__/*.artifact.json']" , PROBE_FILE ) ,
465545 true ,
466546 ) ;
467547 check (
468548 'matcher still sees another package\'s path that climbs back toward this directory' ,
469- referencesArtifact ( "glob('packages/objectql/src/__fixtures__/../../../metadata/src/__fixtures__/*.artifact.json')" ) ,
549+ referencesArtifact ( "glob('packages/objectql/src/__fixtures__/../../../metadata/src/__fixtures__/*.artifact.json')" , PROBE_FILE ) ,
470550 true ,
471551 ) ;
472552 check (
473553 'matcher still sees a wildcard package path' ,
474- referencesArtifact ( "glob('packages/*/src/__fixtures__/*.artifact.json')" ) ,
554+ referencesArtifact ( "glob('packages/*/src/__fixtures__/*.artifact.json')" , PROBE_FILE ) ,
475555 true ,
476556 ) ;
477557 check (
478558 'matcher still sees a `.`-segment path to this directory, in a template literal' ,
479- referencesArtifact ( "readFileSync(`packages/./metadata/src/__fixtures__/hotcrm-17.1-${kind}.artifact.json`)" ) ,
559+ referencesArtifact ( "readFileSync(`packages/./metadata/src/__fixtures__/hotcrm-17.1-${kind}.artifact.json`)" , PROBE_FILE ) ,
480560 true ,
481561 ) ;
482562 check (
483563 'matcher still sees a `.`-segment glob into this directory (package segment)' ,
484- referencesArtifact ( "glob('packages/./metadata/src/__fixtures__/hotcrm-*.artifact.json')" ) ,
564+ referencesArtifact ( "glob('packages/./metadata/src/__fixtures__/hotcrm-*.artifact.json')" , PROBE_FILE ) ,
485565 true ,
486566 ) ;
487567 check (
488568 'matcher still sees a `.`-segment glob into this directory (src segment)' ,
489- referencesArtifact ( "glob('packages/metadata/./src/__fixtures__/hotcrm-*.artifact.json')" ) ,
569+ referencesArtifact ( "glob('packages/metadata/./src/__fixtures__/hotcrm-*.artifact.json')" , PROBE_FILE ) ,
490570 true ,
491571 ) ;
492572 check (
493573 'matcher still sees another package\'s fixtures path that a glob continues' ,
494- referencesArtifact ( "glob('packages/objectql/src/__fixtures__/a*.artifact.json')" ) ,
574+ referencesArtifact ( "glob('packages/objectql/src/__fixtures__/a*.artifact.json')" , PROBE_FILE ) ,
495575 true ,
496576 ) ;
497577 check (
498578 'matcher still sees another package\'s fixtures path that a template continues' ,
499- referencesArtifact ( "readFileSync(`packages/objectql/src/__fixtures__/${name}.artifact.json`)" ) ,
579+ referencesArtifact ( "readFileSync(`packages/objectql/src/__fixtures__/${name}.artifact.json`)" , PROBE_FILE ) ,
500580 true ,
501581 ) ;
502582 check (
503583 'matcher still sees a fixtures path that does not START a token' ,
504- referencesArtifact ( "readFileSync('vendor/packages/objectql/src/__fixtures__/hotcrm.artifact.json')" ) ,
584+ referencesArtifact ( "readFileSync('vendor/packages/objectql/src/__fixtures__/hotcrm.artifact.json')" , PROBE_FILE ) ,
505585 true ,
506586 ) ;
507587 check (
508588 'matcher still sees a `.`-segment LITERAL path into this directory (only normalization reads it)' ,
509- referencesArtifact ( "readFileSync('packages/./metadata/src/__fixtures__/hotcrm-17.1-built-bare-root-predicates.artifact.json')" ) ,
589+ referencesArtifact ( "readFileSync('packages/./metadata/src/__fixtures__/hotcrm-17.1-built-bare-root-predicates.artifact.json')" , PROBE_FILE ) ,
510590 true ,
511591 ) ;
512592 check (
513593 "matcher does NOT count a `.`-segment literal path to another package's fixture" ,
514- referencesArtifact ( " 'packages/./objectql/src/__fixtures__/hotcrm-4.0.1-published-pages.artifact.json'," ) ,
594+ referencesArtifact ( " 'packages/./objectql/src/__fixtures__/hotcrm-4.0.1-published-pages.artifact.json'," , PROBE_FILE ) ,
515595 false ,
516596 ) ;
597+ check (
598+ 'matcher sees a relative climb into this directory, resolved from the file that holds it' ,
599+ referencesArtifact ( "readdirSync(resolve(HERE, '../../../metadata/src/__fixtures__'))" , 'packages/objectql/src/loaders/catalog.ts' ) ,
600+ true ,
601+ ) ;
602+ check (
603+ 'matcher does NOT count the same climb from a file where it lands in another directory' ,
604+ referencesArtifact ( "readdirSync(resolve(HERE, '../../../metadata/src/__fixtures__'))" , 'packages/services/service-metadata/src/loaders/catalog.ts' ) ,
605+ false ,
606+ ) ;
607+ check (
608+ 'matcher sees a case variant of this directory' ,
609+ referencesArtifact ( "readdirSync('packages/Metadata/src/__fixtures__')" , PROBE_FILE ) ,
610+ true ,
611+ ) ;
612+ check (
613+ 'matcher sees a backslash-separated spelling of this directory' ,
614+ referencesArtifact ( "readdirSync('packages\\\\metadata\\\\src\\\\__fixtures__')" , PROBE_FILE ) ,
615+ true ,
616+ ) ;
617+ check (
618+ "matcher does NOT count service-metadata's fixtures directory, whose path contains the string metadata/src/__fixtures__" ,
619+ referencesArtifact ( "readdirSync('packages/services/service-metadata/src/__fixtures__')" , PROBE_FILE ) ,
620+ false ,
621+ ) ;
622+ // Rule 3's token runs PAST `${`, `?`, `[` and `{`. Each of the four below
623+ // puts its character in the MIDDLE of a file name: cut there, the token is
624+ // `…/__fixtures__/hotcrm-` (or `x-`), a literal file path that is set aside,
625+ // and the marker left behind has no `__fixtures__` beside it. Right after
626+ // `__fixtures__/` a cut would leave a token ending in `/`, which the literal
627+ // test keeps anyway, so the case would pin nothing.
628+ check (
629+ "matcher still sees a `${kind}` in the middle of a file name in another package's fixtures path" ,
630+ referencesArtifact ( "readFileSync(`packages/objectql/src/__fixtures__/hotcrm-${kind}.artifact.json`)" , PROBE_FILE ) ,
631+ true ,
632+ ) ;
633+ check (
634+ "matcher still sees a `hotcrm-?` glob in another package's fixtures path" ,
635+ referencesArtifact ( "glob('packages/objectql/src/__fixtures__/hotcrm-?.artifact.json')" , PROBE_FILE ) ,
636+ true ,
637+ ) ;
638+ check (
639+ "matcher still sees a `hotcrm-[ab]` glob in another package's fixtures path" ,
640+ referencesArtifact ( "glob('packages/objectql/src/__fixtures__/hotcrm-[ab].artifact.json')" , PROBE_FILE ) ,
641+ true ,
642+ ) ;
643+ check (
644+ "matcher still sees a `{a,hotcrm}` glob in another package's fixtures path" ,
645+ referencesArtifact ( "glob('packages/objectql/src/__fixtures__/x-{a,hotcrm}.artifact.json')" , PROBE_FILE ) ,
646+ true ,
647+ ) ;
517648
518649 // ── The permission-set extractor, on the real artifact shape.
519650 battery ( 'The permission-set extractor, on the real artifact shape.' ) ;
0 commit comments