@@ -155,13 +155,69 @@ function git(args: string[]): string[] {
155155 return stdout . split ( '\0' ) . filter ( ( line ) => line . length > 0 ) ;
156156}
157157
158- const isScannedSource = ( path : string ) => / \. t s x ? $ / . test ( path ) && ! path . endsWith ( '.d.ts' ) ;
158+ /**
159+ * ── The EXTENSION boundary is `.ts` ALONE, and unlike the tree it is not free ──
160+ *
161+ * `.ts` and NOT `.tsx`, which is what this filter said first (`/\.tsx?$/`, one
162+ * character wide). The population this pin JUDGES has to equal the population
163+ * turbo RE-RUNS it for, and the second of those is declared somewhere else: the
164+ * radius roster entry for `@objectstack/core` in the declaration table at
165+ * scripts/cross-package-test-inputs.mjs declares a `packages/**` subtree glob
166+ * ending in `.ts`, which does not cover `.tsx`.
167+ *
168+ * A scanner wider than that glob judges files neither scoping layer of
169+ * `check:cross-package-test-inputs` can see. Layer A never unions this package
170+ * into the test shard when the diff touches one of them; Layer B never moves
171+ * this package's `test` task cache hash for one of them. So a `.tsx` file under
172+ * packages/ that declared its own copy would be scanned by this pin and
173+ * invisible to both — landing on `main` with every PR reporting green, and then
174+ * reddening whichever unrelated PR next touches a `.ts` file. That is #7802's
175+ * shape, one extension wide, and it is exactly what the declaration table
176+ * exists to prevent.
177+ *
178+ * ⛔ The repair is to narrow the SCANNER, never to widen the GLOB — the two
179+ * directions are not symmetric. Those globs are INHERITED as watch hints by
180+ * `check:cross-package-test-inputs`, and a self-test in the dispatch-gates tool
181+ * pins that no hint of that family reaches the realtime-hooks test file — the
182+ * `.tsx` one — in the client-react package, its live specimen for "a test class
183+ * the hint route cannot reach". Measured on b548e438d rather than reasoned:
184+ * with that same subtree glob ending in `.tsx` added to this package's roster
185+ * entry and that family's hints re-derived, the case flipped from true to false
186+ * and the added glob was itself the covering hint. It is a real red and not a
187+ * nuisance — the specimen is how that tool proves its residue classes are not
188+ * empty — and re-pointing it is an edit in another lane. ⇒ Extensions and glob
189+ * widen together or not at all; the sibling pin in `@objectstack/types` records
190+ * the same trade from the other side of it.
191+ *
192+ * (That specimen is named in two halves rather than as one quoted path on
193+ * purpose. `check:cross-package-test-inputs` collects quoted whole
194+ * repo-relative paths out of this file's source, COMMENTS INCLUDED, into the
195+ * roster it demands the declared globs cover — so spelling it here in one
196+ * quoted piece would demand the very `.tsx` glob this paragraph exists to
197+ * forbid. Measured the same way.)
198+ *
199+ * ⚠️ What narrowing GIVES UP, measured rather than assumed. Under packages/ on
200+ * b548e438d, over this pin's own surface (tracked PLUS untracked, ignored paths
201+ * excluded), against this pin's own two symbols:
202+ *
203+ * .ts 5408 files, 8 mention a guarded symbol <- scanned
204+ * .tsx 8 files, 0 mention a guarded symbol <- excluded
205+ *
206+ * ⇒ the loss is empty today. ⛔ That reading is not transcribed and then
207+ * trusted: the last test below RE-MEASURES it on every run, because a count
208+ * with a commit on it is honest and a count without one rots silently. The day
209+ * a `.tsx` file under packages/ declares either symbol, that case goes red and
210+ * says what the choice actually is — which is not "widen this filter".
211+ */
212+ const isScannedSource = ( path : string ) => path . endsWith ( '.ts' ) && ! path . endsWith ( '.d.ts' ) ;
159213
160214/**
161- * The scan surface: every `.ts`/`.tsx` file under `packages/` that a human
162- * authored — tracked plus untracked, ignored paths (build output) excluded.
215+ * Every file under `packages/` that a human authored — tracked plus untracked,
216+ * ignored paths (build output) excluded — BEFORE the extension boundary. Split
217+ * out from `scannedFiles()` so the boundary has something to be measured
218+ * against: a filter is only an exclusion while the set it filters is non-empty.
163219 */
164- function scannedFiles ( ) : string [ ] {
220+ function authoredPaths ( ) : string [ ] {
165221 return git ( [
166222 'ls-files' ,
167223 '-z' ,
@@ -170,15 +226,22 @@ function scannedFiles(): string[] {
170226 '--exclude-standard' ,
171227 '--' ,
172228 'packages' ,
173- ] ) . filter ( isScannedSource ) ;
229+ ] ) ;
230+ }
231+
232+ /** The scan surface: the authored files the extension boundary above admits. */
233+ function scannedFiles ( ) : string [ ] {
234+ return authoredPaths ( ) . filter ( isScannedSource ) ;
174235}
175236
176237/**
177- * The subset of the scan surface that so much as mentions either identifier.
178- * Fixed-string, so it is an exact superset of `declarationMatcher()`'s matches; the regex
179- * still decides which of these — if any — is an actual declaration.
238+ * Every path the fixed-string prefilter returns, BEFORE the extension boundary
239+ * — so it includes the `.tsx` (and `.md`, and config) files the scan does not
240+ * judge. Split out for the same reason as `authoredPaths()`: the trade the
241+ * boundary rests on is a claim about the files it drops, and a claim about
242+ * dropped files cannot be made from the set they were dropped from.
180243 */
181- function filesMentioningASymbol ( ) : string [ ] {
244+ function prefilterHits ( ) : string [ ] {
182245 return git ( [
183246 'grep' ,
184247 '--files-with-matches' ,
@@ -192,7 +255,16 @@ function filesMentioningASymbol(): string[] {
192255 HELPER_SYMBOL ,
193256 '--' ,
194257 'packages' ,
195- ] ) . filter ( isScannedSource ) ;
258+ ] ) ;
259+ }
260+
261+ /**
262+ * The subset of the scan surface that so much as mentions either identifier.
263+ * Fixed-string, so it is an exact superset of `declarationMatcher()`'s matches; the regex
264+ * still decides which of these — if any — is an actual declaration.
265+ */
266+ function filesMentioningASymbol ( ) : string [ ] {
267+ return prefilterHits ( ) . filter ( isScannedSource ) ;
196268}
197269
198270function declaringFiles ( ) : string [ ] {
@@ -297,4 +369,73 @@ describe('the `__` operation-private-key convention has one owner (#7284)', () =
297369 } ,
298370 SCAN_TIMEOUT_MS ,
299371 ) ;
372+
373+ it (
374+ 'the extension boundary is `.ts` alone, and the trade that buys it has not expired' ,
375+ ( ) => {
376+ // The executable half of the EXTENSION boundary section on
377+ // `isScannedSource`. That paragraph is the only thing keeping the judged
378+ // population equal to the population turbo re-runs this pin for, and
379+ // #9763 is the day a radius held by prose alone came unforced by an
380+ // innocent reword. Three claims, in the order they can go false.
381+
382+ // 1. THE EXCLUSION IS REAL. `.tsx` files exist under packages/, so "the
383+ // scan sees none" is an exclusion and not an empty tree describing
384+ // itself — #4690's shape, applied to the boundary rather than to the
385+ // offender set.
386+ const excluded = authoredPaths ( ) . filter ( ( file ) => file . endsWith ( '.tsx' ) ) ;
387+
388+ expect (
389+ excluded . length ,
390+ 'No `.tsx` file exists under packages/ at all, so the extension boundary '
391+ + 'below excludes nothing and this case proves nothing. Re-measure the '
392+ + 'trade in the header before trusting it.' ,
393+ ) . toBeGreaterThan ( 0 ) ;
394+
395+ // 2. …AND THE FILTER REALLY DROPS THEM. Re-widen `isScannedSource` and
396+ // this is what goes red first, in the same run that the offender test
397+ // above stays green — which is the whole point: widening the scanner
398+ // alone reads as coverage while turbo never re-runs this pin for the
399+ // files it has started to judge.
400+ expect (
401+ scannedFiles ( ) . filter ( ( file ) => file . endsWith ( '.tsx' ) ) ,
402+ 'The scan surface has grown `.tsx` files back. The declared radius for '
403+ + 'this package covers `.ts` only, so these are judged by a pin that '
404+ + 'CI will not re-run when they change. Narrow the filter back, or '
405+ + 'widen the glob too — and read the header first, because the second '
406+ + 'half of that is a trade with another gate, not a formality.' ,
407+ ) . toEqual ( [ ] ) ;
408+
409+ // 3. THE TRADE ITSELF, re-measured rather than transcribed. The boundary
410+ // gives up naming a `.tsx` redeclaration as an offender; that costs
411+ // nothing only while no excluded file declares either symbol. The
412+ // header's counts carry a commit precisely because they rot — this is
413+ // the part that cannot.
414+ const excludedDeclarations = prefilterHits ( )
415+ . filter ( ( file ) => file . endsWith ( '.tsx' ) )
416+ . filter ( ( file ) => declarationMatcher ( ) . test ( readFileSync ( join ( REPO_ROOT , file ) , 'utf8' ) ) ) ;
417+
418+ expect (
419+ excludedDeclarations ,
420+ excludedDeclarations . length === 0
421+ ? ''
422+ : [
423+ 'These `.tsx` files declare their own copy of the `__` operation-private-key' ,
424+ 'convention, and the extension boundary above means this pin no longer names' ,
425+ 'them as offenders:' ,
426+ ...excludedDeclarations . map ( ( f ) => ` - ${ f } ` ) ,
427+ '' ,
428+ 'The trade recorded in this file\'s header has expired. ⛔ Do NOT simply widen' ,
429+ '`isScannedSource` back to `.tsx`: that rebuilds the exact mismatch it was' ,
430+ 'narrowed to remove — a pin judging files CI never re-runs it for. Widening' ,
431+ 'the scanner is only HALF the change; this package\'s declared radius has to' ,
432+ 'widen with it, and that glob is inherited as a watch hint by a second gate' ,
433+ 'whose self-test owns a live `.tsx` specimen. Read the EXTENSION boundary' ,
434+ 'section above, then take it to whoever owns that tool — it is a decision,' ,
435+ 'not a one-line fix.' ,
436+ ] . join ( '\n' ) ,
437+ ) . toEqual ( [ ] ) ;
438+ } ,
439+ SCAN_TIMEOUT_MS ,
440+ ) ;
300441} ) ;
0 commit comments