Skip to content

Commit 6f94458

Browse files
fix(core): narrow the operation-private-keys pin's scanner to .ts, matching its declared radius (#15499)
* fix(core): narrow the operation-private-keys pin scanner to `.ts`, matching its declared radius The pin filtered candidates with `/\.tsx?$/` while `@objectstack/core`'s declared radius is `packages/**` + `.ts`, so it judged a population strictly wider than the one turbo re-runs it for: a `.tsx` file under packages/ declaring its own `OPERATION_PRIVATE_KEY_PREFIX` or `withoutOperationPrivateKeys` would be scanned by the pin and invisible to both scoping layers of `check:cross-package-test-inputs`. Narrow the scanner rather than widen the glob. Measured: adding a `.tsx` glob to the roster entry flips the dispatch-gates self-test case "nor a .tsx test file inside it" from true to false, with the added glob as the covering hint. The correspondence is now executable as well as prose-held: a new case asserts the exclusion is real, that the filter drops `.tsx`, and that no excluded `.tsx` file declares either symbol — the trade that makes the boundary free, re-measured every run instead of transcribed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * chore: changeset for the operation-private-keys pin extension boundary Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2003259 commit 6f94458

3 files changed

Lines changed: 212 additions & 12 deletions

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/core": patch
3+
---
4+
5+
fix(core): narrow the operation-private-keys pin's scanner to `.ts`, so it judges exactly the population turbo re-runs it for (#15090)
6+
7+
`packages/core/src/security/operation-private-keys.pin.test.ts` filtered its
8+
candidate set with `/\.tsx?$/``.ts` **and** `.tsx` — while this package's
9+
declared radius in the cross-package declaration table is a `packages/**`
10+
subtree glob ending in `.ts`. So the pin judged a population **strictly wider**
11+
than the one either scoping layer of `check:cross-package-test-inputs` knows
12+
about: Layer A never unions this package into the test shard when a `.tsx` file
13+
changes, and Layer B never moves the `test` task's cache hash for one. A `.tsx`
14+
file under `packages/` declaring its own `OPERATION_PRIVATE_KEY_PREFIX` or
15+
`withoutOperationPrivateKeys` was therefore scanned by the pin and invisible to
16+
CI's scoping — landing on `main` with every PR green and then reddening whichever
17+
unrelated PR next touched a `.ts` file. That is the #7802 shape the declaration
18+
table exists to close, one extension wide.
19+
20+
Repaired by narrowing the **scanner**, not by widening the **glob** — and that
21+
asymmetry is measured rather than assumed. On `b548e438d`, adding a `.tsx` glob
22+
to this package's roster entry and re-deriving `check:cross-package-test-inputs`'
23+
watch hints flips the dispatch-gates self-test case *"nor a .tsx test file inside
24+
it"* from true to false, with the added glob itself as the covering hint. That
25+
case is a live specimen for "a test class the hint route cannot reach", so the
26+
red is real and re-pointing it is a decision in another lane, not a fixup.
27+
28+
What the boundary costs, measured on the pin's own surface (tracked **plus**
29+
untracked, ignored paths excluded) at `b548e438d`: **5408** `.ts` files scanned,
30+
8 of them mentioning a guarded symbol; **8** `.tsx` files excluded, **0** of them
31+
mentioning either symbol. The loss is empty today — and that reading is no longer
32+
transcribed and trusted. A new case re-measures it on every run: it asserts the
33+
excluded `.tsx` population is non-empty (so the boundary is an exclusion and not
34+
an empty tree describing itself), that the filter really drops those files, and
35+
that none of them declares either symbol. Ablation, with the restore proven by
36+
blob hash rather than by exit code: re-widening the scanner reddens it while the
37+
offender assertion stays green — which is precisely the failure mode, since a
38+
wider scanner reads as coverage CI never runs — and planting a `.tsx`
39+
redeclaration reddens it with a message that says the choice is a second-gate
40+
trade, not a one-line widening.
41+
42+
The correspondence between scanner and glob is now stated at **both** ends: the
43+
pin's header and the declaration table's entry for this package. No published
44+
surface moves — the only source file edited is a test.

packages/core/src/security/operation-private-keys.pin.test.ts

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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) => /\.tsx?$/.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

198270
function 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
});

scripts/cross-package-test-inputs.mjs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,23 @@ export const CROSS_PACKAGE_TEST_INPUTS = {
186186
},
187187
},
188188
'@objectstack/core': {
189-
// src/security/operation-private-keys.pin.test.ts walks `git ls-files` over
190-
// the whole repo and reads every matching source file.
189+
// src/security/operation-private-keys.pin.test.ts asks `git ls-files` for
190+
// every authored source file under `packages/` -- tracked plus untracked --
191+
// and reads the ones a fixed-string prefilter says mention its two symbols.
192+
//
193+
// ⛔ `.ts` and NOT `.tsx`, and this half of that boundary is load-bearing in
194+
// the same way the `@objectstack/types` entry below is. That pin's scanner
195+
// used to match `.ts` and `.tsx` while this glob covered only `.ts`, so it
196+
// judged a population neither scoping layer re-runs it for -- the #7802
197+
// shape, one extension wide. It was repaired by narrowing the SCANNER to
198+
// this glob, never by widening this glob to the scanner: these globs are
199+
// inherited as watch hints by `check:cross-package-test-inputs`, and the
200+
// dispatch-gates self-test pins that no hint of that family reaches
201+
// the `realtime-hooks.test.tsx` file in `packages/client-react`. Measured on
202+
// b548e438d, by adding a `.tsx` glob here and re-deriving that family's
203+
// hints: the case flipped from true to false with the added glob itself as
204+
// the covering hint. ⇒ Extensions and glob widen together or not at all,
205+
// and that pin's header carries the measurement of what the boundary costs.
191206
globs: ['packages/**/*.ts'],
192207
},
193208
'@objectstack/types': {

0 commit comments

Comments
 (0)