@@ -18,6 +18,42 @@ import { normalizeSymbol } from '../../shared/normalize.js';
1818import { paginateResult } from '../../shared/paginate.js' ;
1919import { findMatchingNodes } from './symbol-lookup.js' ;
2020
21+ // ─── Shared BFS: transitive callers ────────────────────────────────────
22+
23+ /**
24+ * BFS traversal to find transitive callers of a node.
25+ *
26+ * @param {import('better-sqlite3').Database } db - Open read-only SQLite database handle (not a Repository)
27+ * @param {number } startId - Starting node ID
28+ * @param {{ noTests?: boolean, maxDepth?: number, onVisit?: (caller: object, parentId: number, depth: number) => void } } options
29+ * @returns {{ totalDependents: number, levels: Record<number, Array<{name:string, kind:string, file:string, line:number}>> } }
30+ */
31+ export function bfsTransitiveCallers ( db , startId , { noTests = false , maxDepth = 3 , onVisit } = { } ) {
32+ const visited = new Set ( [ startId ] ) ;
33+ const levels = { } ;
34+ let frontier = [ startId ] ;
35+
36+ for ( let d = 1 ; d <= maxDepth ; d ++ ) {
37+ const nextFrontier = [ ] ;
38+ for ( const fid of frontier ) {
39+ const callers = findDistinctCallers ( db , fid ) ;
40+ for ( const c of callers ) {
41+ if ( ! visited . has ( c . id ) && ( ! noTests || ! isTestFile ( c . file ) ) ) {
42+ visited . add ( c . id ) ;
43+ nextFrontier . push ( c . id ) ;
44+ if ( ! levels [ d ] ) levels [ d ] = [ ] ;
45+ levels [ d ] . push ( { name : c . name , kind : c . kind , file : c . file , line : c . line } ) ;
46+ if ( onVisit ) onVisit ( c , fid , d ) ;
47+ }
48+ }
49+ }
50+ frontier = nextFrontier ;
51+ if ( frontier . length === 0 ) break ;
52+ }
53+
54+ return { totalDependents : visited . size - 1 , levels } ;
55+ }
56+
2157export function impactAnalysisData ( file , customDbPath , opts = { } ) {
2258 const db = openReadonlyOrFail ( customDbPath ) ;
2359 try {
@@ -82,31 +118,11 @@ export function fnImpactData(name, customDbPath, opts = {}) {
82118 }
83119
84120 const results = nodes . map ( ( node ) => {
85- const visited = new Set ( [ node . id ] ) ;
86- const levels = { } ;
87- let frontier = [ node . id ] ;
88-
89- for ( let d = 1 ; d <= maxDepth ; d ++ ) {
90- const nextFrontier = [ ] ;
91- for ( const fid of frontier ) {
92- const callers = findDistinctCallers ( db , fid ) ;
93- for ( const c of callers ) {
94- if ( ! visited . has ( c . id ) && ( ! noTests || ! isTestFile ( c . file ) ) ) {
95- visited . add ( c . id ) ;
96- nextFrontier . push ( c . id ) ;
97- if ( ! levels [ d ] ) levels [ d ] = [ ] ;
98- levels [ d ] . push ( { name : c . name , kind : c . kind , file : c . file , line : c . line } ) ;
99- }
100- }
101- }
102- frontier = nextFrontier ;
103- if ( frontier . length === 0 ) break ;
104- }
105-
121+ const { levels, totalDependents } = bfsTransitiveCallers ( db , node . id , { noTests, maxDepth } ) ;
106122 return {
107123 ...normalizeSymbol ( node , db , hc ) ,
108124 levels,
109- totalDependents : visited . size - 1 ,
125+ totalDependents,
110126 } ;
111127 } ) ;
112128
@@ -232,40 +248,27 @@ export function diffImpactData(customDbPath, opts = {}) {
232248
233249 const allAffected = new Set ( ) ;
234250 const functionResults = affectedFunctions . map ( ( fn ) => {
235- const visited = new Set ( [ fn . id ] ) ;
236- let frontier = [ fn . id ] ;
237- let totalCallers = 0 ;
238- const levels = { } ;
239251 const edges = [ ] ;
240252 const idToKey = new Map ( ) ;
241253 idToKey . set ( fn . id , `${ fn . file } ::${ fn . name } :${ fn . line } ` ) ;
242- for ( let d = 1 ; d <= maxDepth ; d ++ ) {
243- const nextFrontier = [ ] ;
244- for ( const fid of frontier ) {
245- const callers = findDistinctCallers ( db , fid ) ;
246- for ( const c of callers ) {
247- if ( ! visited . has ( c . id ) && ( ! noTests || ! isTestFile ( c . file ) ) ) {
248- visited . add ( c . id ) ;
249- nextFrontier . push ( c . id ) ;
250- allAffected . add ( `${ c . file } :${ c . name } ` ) ;
251- const callerKey = `${ c . file } ::${ c . name } :${ c . line } ` ;
252- idToKey . set ( c . id , callerKey ) ;
253- if ( ! levels [ d ] ) levels [ d ] = [ ] ;
254- levels [ d ] . push ( { name : c . name , kind : c . kind , file : c . file , line : c . line } ) ;
255- edges . push ( { from : idToKey . get ( fid ) , to : callerKey } ) ;
256- totalCallers ++ ;
257- }
258- }
259- }
260- frontier = nextFrontier ;
261- if ( frontier . length === 0 ) break ;
262- }
254+
255+ const { levels, totalDependents } = bfsTransitiveCallers ( db , fn . id , {
256+ noTests,
257+ maxDepth,
258+ onVisit ( c , parentId ) {
259+ allAffected . add ( `${ c . file } :${ c . name } ` ) ;
260+ const callerKey = `${ c . file } ::${ c . name } :${ c . line } ` ;
261+ idToKey . set ( c . id , callerKey ) ;
262+ edges . push ( { from : idToKey . get ( parentId ) , to : callerKey } ) ;
263+ } ,
264+ } ) ;
265+
263266 return {
264267 name : fn . name ,
265268 kind : fn . kind ,
266269 file : fn . file ,
267270 line : fn . line ,
268- transitiveCallers : totalCallers ,
271+ transitiveCallers : totalDependents ,
269272 levels,
270273 edges,
271274 } ;
@@ -310,8 +313,8 @@ export function diffImpactData(customDbPath, opts = {}) {
310313 let boundaryViolations = [ ] ;
311314 let boundaryViolationCount = 0 ;
312315 try {
313- const config = loadConfig ( repoRoot ) ;
314- const boundaryConfig = config . manifesto ?. boundaries ;
316+ const cfg = opts . config || loadConfig ( repoRoot ) ;
317+ const boundaryConfig = cfg . manifesto ?. boundaries ;
315318 if ( boundaryConfig ) {
316319 const result = evaluateBoundaries ( db , boundaryConfig , {
317320 scopeFiles : [ ...changedRanges . keys ( ) ] ,
0 commit comments