11import { diffImpactData } from '../domain/analysis/diff-impact.js' ;
22
3- export function diffImpactMermaid (
4- customDbPath : string ,
5- opts : {
6- noTests ?: boolean ;
7- depth ?: number ;
8- staged ?: boolean ;
9- ref ?: string ;
10- includeImplementors ?: boolean ;
11- limit ?: number ;
12- offset ?: number ;
13- config ?: any ;
14- } = { } ,
15- ) : string {
16- const data : any = diffImpactData ( customDbPath , opts ) ;
17- if ( 'error' in data ) return data . error as string ;
18- if ( data . changedFiles === 0 || data . affectedFunctions . length === 0 ) {
19- return 'flowchart TB\n none["No impacted functions detected"]' ;
20- }
3+ interface MermaidNodeRegistry {
4+ nodeIdMap : Map < string , string > ;
5+ nodeLabels : Map < string , string > ;
6+ counter : number ;
7+ }
218
22- const newFileSet = new Set ( data . newFiles || [ ] ) ;
23- const lines = [ 'flowchart TB' ] ;
9+ interface ImpactEdgeSets {
10+ allEdges : Set < string > ;
11+ edgeFromNodes : Set < string > ;
12+ edgeToNodes : Set < string > ;
13+ changedKeys : Set < string > ;
14+ }
2415
25- // Assign stable Mermaid node IDs
26- let nodeCounter = 0 ;
27- const nodeIdMap = new Map < string , string > ( ) ;
28- const nodeLabels = new Map < string , string > ( ) ;
29- function nodeId ( key : string , label ?: string ) : string {
30- if ( ! nodeIdMap . has ( key ) ) {
31- nodeIdMap . set ( key , `n${ nodeCounter ++ } ` ) ;
32- if ( label ) nodeLabels . set ( key , label ) ;
33- }
34- return nodeIdMap . get ( key ) ! ;
16+ function createNodeRegistry ( ) : MermaidNodeRegistry {
17+ return { nodeIdMap : new Map ( ) , nodeLabels : new Map ( ) , counter : 0 } ;
18+ }
19+
20+ function registerNode ( reg : MermaidNodeRegistry , key : string , label ?: string ) : string {
21+ if ( ! reg . nodeIdMap . has ( key ) ) {
22+ reg . nodeIdMap . set ( key , `n${ reg . counter ++ } ` ) ;
23+ if ( label ) reg . nodeLabels . set ( key , label ) ;
3524 }
25+ return reg . nodeIdMap . get ( key ) ! ;
26+ }
3627
37- // Register all nodes (changed functions + their callers)
38- for ( const fn of data . affectedFunctions ) {
39- nodeId ( `${ fn . file } ::${ fn . name } :${ fn . line } ` , fn . name ) ;
28+ function registerAllNodes ( reg : MermaidNodeRegistry , affectedFunctions : any [ ] ) : void {
29+ for ( const fn of affectedFunctions ) {
30+ registerNode ( reg , `${ fn . file } ::${ fn . name } :${ fn . line } ` , fn . name ) ;
4031 for ( const callers of Object . values ( fn . levels || { } ) ) {
4132 for ( const c of callers as Array < { name : string ; file : string ; line : number } > ) {
42- nodeId ( `${ c . file } ::${ c . name } :${ c . line } ` , c . name ) ;
33+ registerNode ( reg , `${ c . file } ::${ c . name } :${ c . line } ` , c . name ) ;
4334 }
4435 }
4536 }
37+ }
4638
47- // Collect all edges and determine blast radius
39+ function collectEdges ( affectedFunctions : any [ ] ) : ImpactEdgeSets {
4840 const allEdges = new Set < string > ( ) ;
4941 const edgeFromNodes = new Set < string > ( ) ;
5042 const edgeToNodes = new Set < string > ( ) ;
5143 const changedKeys = new Set < string > ( ) ;
5244
53- for ( const fn of data . affectedFunctions ) {
45+ for ( const fn of affectedFunctions ) {
5446 changedKeys . add ( `${ fn . file } ::${ fn . name } :${ fn . line } ` ) ;
5547 for ( const edge of fn . edges || [ ] ) {
5648 const edgeKey = `${ edge . from } |${ edge . to } ` ;
@@ -62,30 +54,42 @@ export function diffImpactMermaid(
6254 }
6355 }
6456
65- // Blast radius: caller nodes that are never a source (leaf nodes of the impact tree)
57+ return { allEdges, edgeFromNodes, edgeToNodes, changedKeys } ;
58+ }
59+
60+ function classifyCallerNodes ( edges : ImpactEdgeSets ) : {
61+ blastRadiusKeys : Set < string > ;
62+ intermediateKeys : Set < string > ;
63+ } {
6664 const blastRadiusKeys = new Set < string > ( ) ;
67- for ( const key of edgeToNodes ) {
68- if ( ! edgeFromNodes . has ( key ) && ! changedKeys . has ( key ) ) {
65+ for ( const key of edges . edgeToNodes ) {
66+ if ( ! edges . edgeFromNodes . has ( key ) && ! edges . changedKeys . has ( key ) ) {
6967 blastRadiusKeys . add ( key ) ;
7068 }
7169 }
7270
73- // Intermediate callers: not changed, not blast radius
7471 const intermediateKeys = new Set < string > ( ) ;
75- for ( const key of edgeToNodes ) {
76- if ( ! changedKeys . has ( key ) && ! blastRadiusKeys . has ( key ) ) {
72+ for ( const key of edges . edgeToNodes ) {
73+ if ( ! edges . changedKeys . has ( key ) && ! blastRadiusKeys . has ( key ) ) {
7774 intermediateKeys . add ( key ) ;
7875 }
7976 }
8077
81- // Group changed functions by file
82- const fileGroups = new Map < string , typeof data . affectedFunctions > ( ) ;
83- for ( const fn of data . affectedFunctions ) {
78+ return { blastRadiusKeys, intermediateKeys } ;
79+ }
80+
81+ function emitFileSubgraphs (
82+ lines : string [ ] ,
83+ affectedFunctions : any [ ] ,
84+ newFileSet : Set < string > ,
85+ reg : MermaidNodeRegistry ,
86+ ) : number {
87+ const fileGroups = new Map < string , any [ ] > ( ) ;
88+ for ( const fn of affectedFunctions ) {
8489 if ( ! fileGroups . has ( fn . file ) ) fileGroups . set ( fn . file , [ ] ) ;
8590 fileGroups . get ( fn . file ) ! . push ( fn ) ;
8691 }
8792
88- // Emit changed-file subgraphs
8993 let sgCounter = 0 ;
9094 for ( const [ file , fns ] of fileGroups ) {
9195 const isNew = newFileSet . has ( file ) ;
@@ -94,33 +98,71 @@ export function diffImpactMermaid(
9498 lines . push ( ` subgraph ${ sgId } ["${ file } **(${ tag } )**"]` ) ;
9599 for ( const fn of fns ) {
96100 const key = `${ fn . file } ::${ fn . name } :${ fn . line } ` ;
97- lines . push ( ` ${ nodeIdMap . get ( key ) } ["${ fn . name } "]` ) ;
101+ lines . push ( ` ${ reg . nodeIdMap . get ( key ) } ["${ fn . name } "]` ) ;
98102 }
99103 lines . push ( ' end' ) ;
100104 const style = isNew ? 'fill:#e8f5e9,stroke:#4caf50' : 'fill:#fff3e0,stroke:#ff9800' ;
101105 lines . push ( ` style ${ sgId } ${ style } ` ) ;
102106 }
103107
104- // Emit intermediate caller nodes (outside subgraphs)
105- for ( const key of intermediateKeys ) {
106- lines . push ( ` ${ nodeIdMap . get ( key ) } ["${ nodeLabels . get ( key ) } "]` ) ;
108+ return sgCounter ;
109+ }
110+
111+ function emitBlastRadiusSubgraph (
112+ lines : string [ ] ,
113+ blastRadiusKeys : Set < string > ,
114+ reg : MermaidNodeRegistry ,
115+ sgCounter : number ,
116+ ) : void {
117+ if ( blastRadiusKeys . size === 0 ) return ;
118+ const sgId = `sg${ sgCounter } ` ;
119+ lines . push ( ` subgraph ${ sgId } ["Callers **(blast radius)**"]` ) ;
120+ for ( const key of blastRadiusKeys ) {
121+ lines . push ( ` ${ reg . nodeIdMap . get ( key ) } ["${ reg . nodeLabels . get ( key ) } "]` ) ;
107122 }
123+ lines . push ( ' end' ) ;
124+ lines . push ( ` style ${ sgId } fill:#f3e5f5,stroke:#9c27b0` ) ;
125+ }
108126
109- // Emit blast radius subgraph
110- if ( blastRadiusKeys . size > 0 ) {
111- const sgId = `sg${ sgCounter ++ } ` ;
112- lines . push ( ` subgraph ${ sgId } ["Callers **(blast radius)**"]` ) ;
113- for ( const key of blastRadiusKeys ) {
114- lines . push ( ` ${ nodeIdMap . get ( key ) } ["${ nodeLabels . get ( key ) } "]` ) ;
115- }
116- lines . push ( ' end' ) ;
117- lines . push ( ` style ${ sgId } fill:#f3e5f5,stroke:#9c27b0` ) ;
127+ export function diffImpactMermaid (
128+ customDbPath : string ,
129+ opts : {
130+ noTests ?: boolean ;
131+ depth ?: number ;
132+ staged ?: boolean ;
133+ ref ?: string ;
134+ includeImplementors ?: boolean ;
135+ limit ?: number ;
136+ offset ?: number ;
137+ config ?: any ;
138+ } = { } ,
139+ ) : string {
140+ const data : any = diffImpactData ( customDbPath , opts ) ;
141+ if ( 'error' in data ) return data . error as string ;
142+ if ( data . changedFiles === 0 || data . affectedFunctions . length === 0 ) {
143+ return 'flowchart TB\n none["No impacted functions detected"]' ;
118144 }
119145
120- // Emit edges (impact flows from changed fn toward callers)
121- for ( const edgeKey of allEdges ) {
146+ const newFileSet = new Set < string > ( data . newFiles || [ ] ) ;
147+ const lines = [ 'flowchart TB' ] ;
148+
149+ const reg = createNodeRegistry ( ) ;
150+ registerAllNodes ( reg , data . affectedFunctions ) ;
151+
152+ const edges = collectEdges ( data . affectedFunctions ) ;
153+ const { blastRadiusKeys, intermediateKeys } = classifyCallerNodes ( edges ) ;
154+
155+ const sgCounter = emitFileSubgraphs ( lines , data . affectedFunctions , newFileSet , reg ) ;
156+
157+ for ( const key of intermediateKeys ) {
158+ lines . push ( ` ${ reg . nodeIdMap . get ( key ) } ["${ reg . nodeLabels . get ( key ) } "]` ) ;
159+ }
160+
161+ emitBlastRadiusSubgraph ( lines , blastRadiusKeys , reg , sgCounter ) ;
162+
163+ for ( const edgeKey of edges . allEdges ) {
122164 const [ from , to ] = edgeKey . split ( '|' ) as [ string , string ] ;
123- lines . push ( ` ${ nodeIdMap . get ( from ) } --> ${ nodeIdMap . get ( to ) } ` ) ;
165+ lines . push ( ` ${ reg . nodeIdMap . get ( from ) } --> ${ reg . nodeIdMap . get ( to ) } ` ) ;
124166 }
125167
126168 return lines . join ( '\n' ) ;
0 commit comments