11import path from 'node:path' ;
2- import Graph from 'graphology' ;
3- import louvain from 'graphology-communities-louvain' ;
4- import {
5- getCallableNodes ,
6- getCallEdges ,
7- getFileNodesAll ,
8- getImportEdges ,
9- openReadonlyOrFail ,
10- } from './db.js' ;
11- import { isTestFile } from './infrastructure/test-filter.js' ;
2+ import { openReadonlyOrFail } from './db.js' ;
3+ import { louvainCommunities } from './graph/algorithms/louvain.js' ;
4+ import { buildDependencyGraph } from './graph/builders/dependency.js' ;
125import { paginateResult } from './paginate.js' ;
136
14- // ─── Graph Construction ───────────────────────────────────────────────
15-
16- /**
17- * Build a graphology graph from the codegraph SQLite database.
18- *
19- * @param {object } db - open better-sqlite3 database (readonly)
20- * @param {object } opts
21- * @param {boolean } [opts.functions] - Function-level instead of file-level
22- * @param {boolean } [opts.noTests] - Exclude test files
23- * @returns {Graph }
24- */
25- function buildGraphologyGraph ( db , opts = { } ) {
26- const graph = new Graph ( { type : 'undirected' } ) ;
27-
28- if ( opts . functions ) {
29- // Function-level: nodes = function/method/class symbols, edges = calls
30- let nodes = getCallableNodes ( db ) ;
31- if ( opts . noTests ) nodes = nodes . filter ( ( n ) => ! isTestFile ( n . file ) ) ;
32-
33- const nodeIds = new Set ( ) ;
34- for ( const n of nodes ) {
35- const key = String ( n . id ) ;
36- graph . addNode ( key , { label : n . name , file : n . file , kind : n . kind } ) ;
37- nodeIds . add ( n . id ) ;
38- }
39-
40- const edges = getCallEdges ( db ) ;
41- for ( const e of edges ) {
42- if ( ! nodeIds . has ( e . source_id ) || ! nodeIds . has ( e . target_id ) ) continue ;
43- const src = String ( e . source_id ) ;
44- const tgt = String ( e . target_id ) ;
45- if ( src === tgt ) continue ;
46- if ( ! graph . hasEdge ( src , tgt ) ) {
47- graph . addEdge ( src , tgt ) ;
48- }
49- }
50- } else {
51- // File-level: nodes = files, edges = imports + imports-type (deduplicated, cross-file)
52- let nodes = getFileNodesAll ( db ) ;
53- if ( opts . noTests ) nodes = nodes . filter ( ( n ) => ! isTestFile ( n . file ) ) ;
54-
55- const nodeIds = new Set ( ) ;
56- for ( const n of nodes ) {
57- const key = String ( n . id ) ;
58- graph . addNode ( key , { label : n . file , file : n . file } ) ;
59- nodeIds . add ( n . id ) ;
60- }
61-
62- const edges = getImportEdges ( db ) ;
63- for ( const e of edges ) {
64- if ( ! nodeIds . has ( e . source_id ) || ! nodeIds . has ( e . target_id ) ) continue ;
65- const src = String ( e . source_id ) ;
66- const tgt = String ( e . target_id ) ;
67- if ( src === tgt ) continue ;
68- if ( ! graph . hasEdge ( src , tgt ) ) {
69- graph . addEdge ( src , tgt ) ;
70- }
71- }
72- }
73-
74- return graph ;
75- }
76-
777// ─── Directory Helpers ────────────────────────────────────────────────
788
799function getDirectory ( filePath ) {
@@ -97,39 +27,38 @@ function getDirectory(filePath) {
9727 */
9828export function communitiesData ( customDbPath , opts = { } ) {
9929 const db = openReadonlyOrFail ( customDbPath ) ;
100- const resolution = opts . resolution ?? 1.0 ;
10130 let graph ;
10231 try {
103- graph = buildGraphologyGraph ( db , {
104- functions : opts . functions ,
32+ graph = buildDependencyGraph ( db , {
33+ fileLevel : ! opts . functions ,
10534 noTests : opts . noTests ,
10635 } ) ;
10736 } finally {
10837 db . close ( ) ;
10938 }
11039
11140 // Handle empty or trivial graphs
112- if ( graph . order === 0 || graph . size === 0 ) {
41+ if ( graph . nodeCount === 0 || graph . edgeCount === 0 ) {
11342 return {
11443 communities : [ ] ,
11544 modularity : 0 ,
11645 drift : { splitCandidates : [ ] , mergeCandidates : [ ] } ,
117- summary : { communityCount : 0 , modularity : 0 , nodeCount : graph . order , driftScore : 0 } ,
46+ summary : { communityCount : 0 , modularity : 0 , nodeCount : graph . nodeCount , driftScore : 0 } ,
11847 } ;
11948 }
12049
12150 // Run Louvain
122- const details = louvain . detailed ( graph , { resolution } ) ;
123- const assignments = details . communities ; // node → community id
124- const modularity = details . modularity ;
51+ const resolution = opts . resolution ?? 1.0 ;
52+ const { assignments, modularity } = louvainCommunities ( graph , { resolution } ) ;
12553
12654 // Group nodes by community
12755 const communityMap = new Map ( ) ; // community id → node keys[]
128- graph . forEachNode ( ( key ) => {
129- const cid = assignments [ key ] ;
56+ for ( const [ key ] of graph . nodes ( ) ) {
57+ const cid = assignments . get ( key ) ;
58+ if ( cid == null ) continue ;
13059 if ( ! communityMap . has ( cid ) ) communityMap . set ( cid , [ ] ) ;
13160 communityMap . get ( cid ) . push ( key ) ;
132- } ) ;
61+ }
13362
13463 // Build community objects
13564 const communities = [ ] ;
@@ -139,7 +68,7 @@ export function communitiesData(customDbPath, opts = {}) {
13968 const dirCounts = { } ;
14069 const memberData = [ ] ;
14170 for ( const key of members ) {
142- const attrs = graph . getNodeAttributes ( key ) ;
71+ const attrs = graph . getNodeAttrs ( key ) ;
14372 const dir = getDirectory ( attrs . file ) ;
14473 dirCounts [ dir ] = ( dirCounts [ dir ] || 0 ) + 1 ;
14574 memberData . push ( {
@@ -196,7 +125,6 @@ export function communitiesData(customDbPath, opts = {}) {
196125 mergeCandidates . sort ( ( a , b ) => b . directoryCount - a . directoryCount ) ;
197126
198127 // Drift score: 0-100 based on how much directory structure diverges from communities
199- // Higher = more drift (directories don't match communities)
200128 const totalDirs = dirToCommunities . size ;
201129 const splitDirs = splitCandidates . length ;
202130 const splitRatio = totalDirs > 0 ? splitDirs / totalDirs : 0 ;
@@ -214,7 +142,7 @@ export function communitiesData(customDbPath, opts = {}) {
214142 summary : {
215143 communityCount : communities . length ,
216144 modularity : + modularity . toFixed ( 4 ) ,
217- nodeCount : graph . order ,
145+ nodeCount : graph . nodeCount ,
218146 driftScore,
219147 } ,
220148 } ;
0 commit comments