@@ -8,6 +8,8 @@ import { resolveImportEdges } from '../layering/model.ts';
88import {
99 bound ,
1010 collectDependents ,
11+ collectExecutableDependents ,
12+ commandAttribution ,
1113 commandsReaching ,
1214 formatBounded ,
1315 guaranteeRowsForFile ,
@@ -23,6 +25,7 @@ import {
2325 parseRouteEntries ,
2426} from './affected-run.ts' ;
2527import { collapseEdges , type GraphNode } from './model.ts' ;
28+ import type { BlastRadius } from './affected-run.ts' ;
2629
2730const repoRoot = execFileSync ( 'git' , [ 'rev-parse' , '--show-toplevel' ] , {
2831 encoding : 'utf8' ,
@@ -67,6 +70,50 @@ test('collectDependents ignores type-only and dynamic dependents, which are coun
6770 assert . deepEqual ( weakDirectDependents ( 'src/core/target.ts' , edges ) , { type : 1 , dynamic : 1 } ) ;
6871} ) ;
6972
73+ test ( 'collectExecutableDependents reports what a dynamic import adds beyond value dependents' , ( ) => {
74+ const edges = edgesOf ( {
75+ 'src/core/target.ts' : 'export const t = 1;' ,
76+ 'src/core/mid.ts' : "import { t } from './target.ts';\nexport const m = t;" ,
77+ 'src/core/lazy.ts' : "export const load = () => import('./mid.ts');" ,
78+ 'src/core/lazier.ts' : "export const load = () => import('./lazy.ts');" ,
79+ 'src/core/typed.ts' : "import type { t } from './target.ts';\nexport type U = typeof t;" ,
80+ } ) ;
81+
82+ assert . deepEqual ( collectDependents ( 'src/core/target.ts' , edges ) . all , [ 'src/core/mid.ts' ] ) ;
83+ assert . deepEqual ( collectExecutableDependents ( 'src/core/target.ts' , edges ) , {
84+ direct : [ 'src/core/mid.ts' ] ,
85+ all : [ 'src/core/lazier.ts' , 'src/core/lazy.ts' , 'src/core/mid.ts' ] ,
86+ transitiveOnly : [ 'src/core/lazier.ts' , 'src/core/lazy.ts' ] ,
87+ beyondValue : [ 'src/core/lazier.ts' , 'src/core/lazy.ts' ] ,
88+ } ) ;
89+ } ) ;
90+
91+ test ( 'commandAttribution is partial only when the file is reachable from the platform-runtime composition root' , ( ) => {
92+ const edges = edgesOf ( {
93+ 'packages/platform-apple/src/leaf.ts' : 'export const leaf = 1;' ,
94+ 'packages/platform-apple/src/runtime.ts' :
95+ "import { leaf } from './leaf.ts';\nexport const r = leaf;" ,
96+ 'src/platform-runtime-host.ts' :
97+ "import { r } from '../packages/platform-apple/src/runtime.ts';\nexport const h = r;" ,
98+ 'packages/daemon/src/handler.ts' : 'export const handler = 1;' ,
99+ } ) ;
100+
101+ const injected = collectExecutableDependents ( 'packages/platform-apple/src/leaf.ts' , edges ) ;
102+ assert . deepEqual ( commandAttribution ( 'packages/platform-apple/src/leaf.ts' , injected . all ) , {
103+ kind : 'partial' ,
104+ reason : 'injected-runtime' ,
105+ compositionRoots : [ 'src/platform-runtime-host.ts' ] ,
106+ } ) ;
107+
108+ const daemonOnly = collectExecutableDependents ( 'packages/daemon/src/handler.ts' , edges ) ;
109+ assert . deepEqual ( commandAttribution ( 'packages/daemon/src/handler.ts' , daemonOnly . all ) , {
110+ kind : 'static-reachability' ,
111+ } ) ;
112+
113+ // The composition root itself is injected-runtime territory even with no dependents.
114+ assert . equal ( commandAttribution ( 'src/platform-runtime.ts' , [ ] ) . kind , 'partial' ) ;
115+ } ) ;
116+
70117test ( 'zoneBreakdown and rankByFanIn order by weight, then name' , ( ) => {
71118 const nodes = nodesOf ( {
72119 'packages/daemon/src/a.ts' : { zone : 'daemon-server' , fanIn : 3 } ,
@@ -197,37 +244,73 @@ test('parseInvocation rejects a missing path, a second path, and a non-positive
197244 assert . throws ( ( ) => parseInvocation ( [ 'a.ts' , '--limit' , 'lots' ] ) , / p o s i t i v e i n t e g e r / ) ;
198245} ) ;
199246
200- test ( 'text output bounds every list and says what it hid' , ( ) => {
201- // This is an arbitrary report input used to exercise formatting, not a live source path.
202- const text = formatBlastRadius (
203- {
204- file : 'src/utils/exec.ts' ,
205- zone : 'utils' ,
206- fanIn : 81 ,
207- fanOut : 3 ,
208- dependents : {
209- direct : [ 'src/a.ts' , 'src/b.ts' , 'src/c.ts' ] ,
210- transitive : [ 'src/d.ts' ] ,
211- byZone : [ { zone : 'utils' , count : 4 } ] ,
212- topByFanIn : [ { file : 'src/a.ts' , fanIn : 7 } ] ,
213- weakDirect : { type : 2 , dynamic : 1 } ,
214- } ,
215- gates : { checks : [ 'lint' , 'typecheck' ] , failOpen : false } ,
216- commands : [
217- { command : 'click' , route : 'interaction' , entry : 'packages/daemon/src/handlers/x.ts' } ,
218- ] ,
219- liveOwners : { manifest : 'test/…/coverage-manifest.ts' , present : false , owners : [ ] } ,
220- guaranteeRows : [ ] ,
247+ function radiusFixture ( overrides : Partial < BlastRadius > = { } ) : BlastRadius {
248+ // An arbitrary report input used to exercise formatting, not a live source path.
249+ return {
250+ file : 'src/utils/exec.ts' ,
251+ zone : 'utils' ,
252+ fanIn : 81 ,
253+ fanOut : 3 ,
254+ dependents : {
255+ direct : [ 'src/a.ts' , 'src/b.ts' , 'src/c.ts' ] ,
256+ transitive : [ 'src/d.ts' ] ,
257+ byZone : [ { zone : 'utils' , count : 4 } ] ,
258+ topByFanIn : [ { file : 'src/a.ts' , fanIn : 7 } ] ,
259+ weakDirect : { type : 2 , dynamic : 1 } ,
221260 } ,
222- 2 ,
223- ) ;
261+ executableDependents : {
262+ direct : [ 'src/a.ts' , 'src/b.ts' , 'src/c.ts' , 'src/lazy.ts' ] ,
263+ transitive : [ 'src/d.ts' , 'src/lazier.ts' ] ,
264+ beyondValue : [ 'src/lazy.ts' , 'src/lazier.ts' ] ,
265+ } ,
266+ commandAttribution : { kind : 'static-reachability' } ,
267+ gates : { checks : [ 'lint' , 'typecheck' ] , failOpen : false } ,
268+ commands : [
269+ { command : 'click' , route : 'interaction' , entry : 'packages/daemon/src/handlers/x.ts' } ,
270+ ] ,
271+ liveOwners : { manifest : 'test/…/coverage-manifest.ts' , present : false , owners : [ ] } ,
272+ guaranteeRows : [ ] ,
273+ ...overrides ,
274+ } ;
275+ }
276+
277+ test ( 'text output bounds every list and says what it hid' , ( ) => {
278+ const text = formatBlastRadius ( radiusFixture ( ) , 2 ) ;
224279
225280 assert . match ( text , / 3 d i r e c t , 1 t r a n s i t i v e .* 2 t y p e - o n l y a n d 1 d y n a m i c / ) ;
226281 assert . match ( text , / d i r e c t : { 2 } s r c \/ a \. t s , s r c \/ b \. t s \( \+ 1 m o r e \) / ) ;
282+ assert . match (
283+ text ,
284+ / e x e c u t a b l e d e p e n d e n t s .* : 4 d i r e c t , 2 t r a n s i t i v e ; 2 r e a c h e d o n l y a c r o s s a d y n a m i c i m p o r t : s r c \/ l a z y \. t s , s r c \/ l a z i e r \. t s / ,
285+ ) ;
227286 assert . match ( text , / g a t e s f o r t h i s s e t : l i n t , t y p e c h e c k / ) ;
228287 assert . match ( text , / i s n o t i n t h i s t r e e / ) ;
229288} ) ;
230289
290+ test ( 'text output tells a known-empty command list from an incomplete one' , ( ) => {
291+ const noCommands = { commands : [ ] } ;
292+ const staticText = formatBlastRadius ( radiusFixture ( noCommands ) , 5 ) ;
293+ const partialText = formatBlastRadius (
294+ radiusFixture ( {
295+ ...noCommands ,
296+ commandAttribution : {
297+ kind : 'partial' ,
298+ reason : 'injected-runtime' ,
299+ compositionRoots : [ 'src/platform-runtime.ts' , 'src/platform-runtime-host.ts' , 'src/x.ts' ] ,
300+ } ,
301+ } ) ,
302+ 2 ,
303+ ) ;
304+
305+ assert . match ( staticText , / \( 0 \) : \( n o n e \) \n { 2 } a t t r i b u t i o n : s t a t i c i m p o r t r e a c h a b i l i t y o n l y / ) ;
306+ assert . doesNotMatch ( staticText , / P A R T I A L / ) ;
307+ assert . match (
308+ partialText ,
309+ / \( 0 \) : \( n o n e \) \n { 2 } a t t r i b u t i o n : P A R T I A L \( i n j e c t e d r u n t i m e \) — r e a c h a b l e f r o m s r c \/ p l a t f o r m - r u n t i m e \. t s , s r c \/ p l a t f o r m - r u n t i m e - h o s t \. t s \( \+ 1 m o r e \) / ,
310+ ) ;
311+ assert . doesNotMatch ( partialText , / s t a t i c i m p o r t r e a c h a b i l i t y o n l y / ) ;
312+ } ) ;
313+
231314test ( 'blast radius over the real tree agrees with check:affected and stays under 10s' , async ( ) => {
232315 const file = 'packages/host-kit/src/internal/exec.ts' ;
233316 const started = Date . now ( ) ;
@@ -243,3 +326,23 @@ test('blast radius over the real tree agrees with check:affected and stays under
243326 const single = selectChecks ( { changedFiles : [ file ] } ) ;
244327 for ( const check of single . checks ) assert . ok ( radius . gates . checks . includes ( check ) , check ) ;
245328} ) ;
329+
330+ test ( 'the real platform snapshot route is flagged partial while a daemon handler is not' , async ( ) => {
331+ const platform = await computeBlastRadius (
332+ repoRoot ,
333+ 'packages/platform-apple/src/snapshot-route.ts' ,
334+ ) ;
335+ assert . deepEqual ( platform . commands , [ ] ) ;
336+ assert . equal ( platform . commandAttribution . kind , 'partial' ) ;
337+ assert . ok (
338+ platform . executableDependents . beyondValue . length > 0 ,
339+ 'platform files are reached from the composition root only across dynamic imports' ,
340+ ) ;
341+
342+ const handler = await computeBlastRadius (
343+ repoRoot ,
344+ 'packages/daemon/src/sessions/handlers/session.ts' ,
345+ ) ;
346+ assert . deepEqual ( handler . commandAttribution , { kind : 'static-reachability' } ) ;
347+ assert . ok ( handler . commands . length > 0 ) ;
348+ } ) ;
0 commit comments