@@ -608,16 +608,34 @@ export function diffImpactData(customDbPath, opts = {}) {
608608
609609 if ( ! diffOutput . trim ( ) ) {
610610 db . close ( ) ;
611- return { changedFiles : 0 , affectedFunctions : [ ] , affectedFiles : [ ] , summary : null } ;
611+ return {
612+ changedFiles : 0 ,
613+ newFiles : [ ] ,
614+ affectedFunctions : [ ] ,
615+ affectedFiles : [ ] ,
616+ summary : null ,
617+ } ;
612618 }
613619
614620 const changedRanges = new Map ( ) ;
621+ const newFiles = new Set ( ) ;
615622 let currentFile = null ;
623+ let prevIsDevNull = false ;
616624 for ( const line of diffOutput . split ( '\n' ) ) {
625+ if ( line . startsWith ( '--- /dev/null' ) ) {
626+ prevIsDevNull = true ;
627+ continue ;
628+ }
629+ if ( line . startsWith ( '--- ' ) ) {
630+ prevIsDevNull = false ;
631+ continue ;
632+ }
617633 const fileMatch = line . match ( / ^ \+ \+ \+ b \/ ( .+ ) / ) ;
618634 if ( fileMatch ) {
619635 currentFile = fileMatch [ 1 ] ;
620636 if ( ! changedRanges . has ( currentFile ) ) changedRanges . set ( currentFile , [ ] ) ;
637+ if ( prevIsDevNull ) newFiles . add ( currentFile ) ;
638+ prevIsDevNull = false ;
621639 continue ;
622640 }
623641 const hunkMatch = line . match ( / ^ @ @ .+ \+ ( \d + ) (?: , ( \d + ) ) ? @ @ / ) ;
@@ -630,7 +648,13 @@ export function diffImpactData(customDbPath, opts = {}) {
630648
631649 if ( changedRanges . size === 0 ) {
632650 db . close ( ) ;
633- return { changedFiles : 0 , affectedFunctions : [ ] , affectedFiles : [ ] , summary : null } ;
651+ return {
652+ changedFiles : 0 ,
653+ newFiles : [ ] ,
654+ affectedFunctions : [ ] ,
655+ affectedFiles : [ ] ,
656+ summary : null ,
657+ } ;
634658 }
635659
636660 const affectedFunctions = [ ] ;
@@ -658,6 +682,10 @@ export function diffImpactData(customDbPath, opts = {}) {
658682 const visited = new Set ( [ fn . id ] ) ;
659683 let frontier = [ fn . id ] ;
660684 let totalCallers = 0 ;
685+ const levels = { } ;
686+ const edges = [ ] ;
687+ const idToKey = new Map ( ) ;
688+ idToKey . set ( fn . id , `${ fn . file } ::${ fn . name } :${ fn . line } ` ) ;
661689 for ( let d = 1 ; d <= maxDepth ; d ++ ) {
662690 const nextFrontier = [ ] ;
663691 for ( const fid of frontier ) {
@@ -673,6 +701,11 @@ export function diffImpactData(customDbPath, opts = {}) {
673701 visited . add ( c . id ) ;
674702 nextFrontier . push ( c . id ) ;
675703 allAffected . add ( `${ c . file } :${ c . name } ` ) ;
704+ const callerKey = `${ c . file } ::${ c . name } :${ c . line } ` ;
705+ idToKey . set ( c . id , callerKey ) ;
706+ if ( ! levels [ d ] ) levels [ d ] = [ ] ;
707+ levels [ d ] . push ( { name : c . name , kind : c . kind , file : c . file , line : c . line } ) ;
708+ edges . push ( { from : idToKey . get ( fid ) , to : callerKey } ) ;
676709 totalCallers ++ ;
677710 }
678711 }
@@ -686,6 +719,8 @@ export function diffImpactData(customDbPath, opts = {}) {
686719 file : fn . file ,
687720 line : fn . line ,
688721 transitiveCallers : totalCallers ,
722+ levels,
723+ edges,
689724 } ;
690725 } ) ;
691726
@@ -695,6 +730,7 @@ export function diffImpactData(customDbPath, opts = {}) {
695730 db . close ( ) ;
696731 return {
697732 changedFiles : changedRanges . size ,
733+ newFiles : [ ...newFiles ] ,
698734 affectedFunctions : functionResults ,
699735 affectedFiles : [ ...affectedFiles ] ,
700736 summary : {
@@ -705,6 +741,120 @@ export function diffImpactData(customDbPath, opts = {}) {
705741 } ;
706742}
707743
744+ export function diffImpactMermaid ( customDbPath , opts = { } ) {
745+ const data = diffImpactData ( customDbPath , opts ) ;
746+ if ( data . error ) return data . error ;
747+ if ( data . changedFiles === 0 || data . affectedFunctions . length === 0 ) {
748+ return 'flowchart TB\n none["No impacted functions detected"]' ;
749+ }
750+
751+ const newFileSet = new Set ( data . newFiles || [ ] ) ;
752+ const lines = [ 'flowchart TB' ] ;
753+
754+ // Assign stable Mermaid node IDs
755+ let nodeCounter = 0 ;
756+ const nodeIdMap = new Map ( ) ;
757+ const nodeLabels = new Map ( ) ;
758+ function nodeId ( key , label ) {
759+ if ( ! nodeIdMap . has ( key ) ) {
760+ nodeIdMap . set ( key , `n${ nodeCounter ++ } ` ) ;
761+ if ( label ) nodeLabels . set ( key , label ) ;
762+ }
763+ return nodeIdMap . get ( key ) ;
764+ }
765+
766+ // Register all nodes (changed functions + their callers)
767+ for ( const fn of data . affectedFunctions ) {
768+ nodeId ( `${ fn . file } ::${ fn . name } :${ fn . line } ` , fn . name ) ;
769+ for ( const callers of Object . values ( fn . levels || { } ) ) {
770+ for ( const c of callers ) {
771+ nodeId ( `${ c . file } ::${ c . name } :${ c . line } ` , c . name ) ;
772+ }
773+ }
774+ }
775+
776+ // Collect all edges and determine blast radius
777+ const allEdges = new Set ( ) ;
778+ const edgeFromNodes = new Set ( ) ;
779+ const edgeToNodes = new Set ( ) ;
780+ const changedKeys = new Set ( ) ;
781+
782+ for ( const fn of data . affectedFunctions ) {
783+ changedKeys . add ( `${ fn . file } ::${ fn . name } :${ fn . line } ` ) ;
784+ for ( const edge of fn . edges || [ ] ) {
785+ const edgeKey = `${ edge . from } |${ edge . to } ` ;
786+ if ( ! allEdges . has ( edgeKey ) ) {
787+ allEdges . add ( edgeKey ) ;
788+ edgeFromNodes . add ( edge . from ) ;
789+ edgeToNodes . add ( edge . to ) ;
790+ }
791+ }
792+ }
793+
794+ // Blast radius: caller nodes that are never a source (leaf nodes of the impact tree)
795+ const blastRadiusKeys = new Set ( ) ;
796+ for ( const key of edgeToNodes ) {
797+ if ( ! edgeFromNodes . has ( key ) && ! changedKeys . has ( key ) ) {
798+ blastRadiusKeys . add ( key ) ;
799+ }
800+ }
801+
802+ // Intermediate callers: not changed, not blast radius
803+ const intermediateKeys = new Set ( ) ;
804+ for ( const key of edgeToNodes ) {
805+ if ( ! changedKeys . has ( key ) && ! blastRadiusKeys . has ( key ) ) {
806+ intermediateKeys . add ( key ) ;
807+ }
808+ }
809+
810+ // Group changed functions by file
811+ const fileGroups = new Map ( ) ;
812+ for ( const fn of data . affectedFunctions ) {
813+ if ( ! fileGroups . has ( fn . file ) ) fileGroups . set ( fn . file , [ ] ) ;
814+ fileGroups . get ( fn . file ) . push ( fn ) ;
815+ }
816+
817+ // Emit changed-file subgraphs
818+ let sgCounter = 0 ;
819+ for ( const [ file , fns ] of fileGroups ) {
820+ const isNew = newFileSet . has ( file ) ;
821+ const tag = isNew ? 'new' : 'modified' ;
822+ const sgId = `sg${ sgCounter ++ } ` ;
823+ lines . push ( ` subgraph ${ sgId } ["${ file } **(${ tag } )**"]` ) ;
824+ for ( const fn of fns ) {
825+ const key = `${ fn . file } ::${ fn . name } :${ fn . line } ` ;
826+ lines . push ( ` ${ nodeIdMap . get ( key ) } ["${ fn . name } "]` ) ;
827+ }
828+ lines . push ( ' end' ) ;
829+ const style = isNew ? 'fill:#e8f5e9,stroke:#4caf50' : 'fill:#fff3e0,stroke:#ff9800' ;
830+ lines . push ( ` style ${ sgId } ${ style } ` ) ;
831+ }
832+
833+ // Emit intermediate caller nodes (outside subgraphs)
834+ for ( const key of intermediateKeys ) {
835+ lines . push ( ` ${ nodeIdMap . get ( key ) } ["${ nodeLabels . get ( key ) } "]` ) ;
836+ }
837+
838+ // Emit blast radius subgraph
839+ if ( blastRadiusKeys . size > 0 ) {
840+ const sgId = `sg${ sgCounter ++ } ` ;
841+ lines . push ( ` subgraph ${ sgId } ["Callers **(blast radius)**"]` ) ;
842+ for ( const key of blastRadiusKeys ) {
843+ lines . push ( ` ${ nodeIdMap . get ( key ) } ["${ nodeLabels . get ( key ) } "]` ) ;
844+ }
845+ lines . push ( ' end' ) ;
846+ lines . push ( ` style ${ sgId } fill:#f3e5f5,stroke:#9c27b0` ) ;
847+ }
848+
849+ // Emit edges (impact flows from changed fn toward callers)
850+ for ( const edgeKey of allEdges ) {
851+ const [ from , to ] = edgeKey . split ( '|' ) ;
852+ lines . push ( ` ${ nodeIdMap . get ( from ) } --> ${ nodeIdMap . get ( to ) } ` ) ;
853+ }
854+
855+ return lines . join ( '\n' ) ;
856+ }
857+
708858export function listFunctionsData ( customDbPath , opts = { } ) {
709859 const db = openReadonlyOrFail ( customDbPath ) ;
710860 const noTests = opts . noTests || false ;
@@ -2079,8 +2229,12 @@ export function fnImpact(name, customDbPath, opts = {}) {
20792229}
20802230
20812231export function diffImpact ( customDbPath , opts = { } ) {
2232+ if ( opts . format === 'mermaid' ) {
2233+ console . log ( diffImpactMermaid ( customDbPath , opts ) ) ;
2234+ return ;
2235+ }
20822236 const data = diffImpactData ( customDbPath , opts ) ;
2083- if ( opts . json ) {
2237+ if ( opts . json || opts . format === 'json' ) {
20842238 console . log ( JSON . stringify ( data , null , 2 ) ) ;
20852239 return ;
20862240 }
0 commit comments