@optave/codegraph 3.11.0 → 3.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +38 -31
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +91 -60
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  6. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  7. package/dist/ast-analysis/visitor-utils.js +83 -49
  8. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  11. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  12. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  14. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  15. package/dist/cli/commands/embed.d.ts.map +1 -1
  16. package/dist/cli/commands/embed.js +49 -4
  17. package/dist/cli/commands/embed.js.map +1 -1
  18. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  19. package/dist/domain/analysis/dependencies.js +106 -80
  20. package/dist/domain/analysis/dependencies.js.map +1 -1
  21. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  22. package/dist/domain/analysis/fn-impact.js +77 -52
  23. package/dist/domain/analysis/fn-impact.js.map +1 -1
  24. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  25. package/dist/domain/analysis/module-map.js +132 -121
  26. package/dist/domain/analysis/module-map.js.map +1 -1
  27. package/dist/domain/graph/builder/call-resolver.d.ts +71 -0
  28. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  29. package/dist/domain/graph/builder/call-resolver.js +130 -0
  30. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  31. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  32. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/helpers.js +47 -33
  34. package/dist/domain/graph/builder/helpers.js.map +1 -1
  35. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  36. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/incremental.js +214 -127
  38. package/dist/domain/graph/builder/incremental.js.map +1 -1
  39. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  40. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/pipeline.js +10 -766
  42. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/stages/build-edges.js +151 -192
  45. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  48. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  56. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  58. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  60. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  62. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  63. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  67. package/dist/domain/graph/cycles.d.ts +6 -4
  68. package/dist/domain/graph/cycles.d.ts.map +1 -1
  69. package/dist/domain/graph/cycles.js +50 -55
  70. package/dist/domain/graph/cycles.js.map +1 -1
  71. package/dist/domain/graph/journal.d.ts.map +1 -1
  72. package/dist/domain/graph/journal.js +89 -70
  73. package/dist/domain/graph/journal.js.map +1 -1
  74. package/dist/domain/graph/watcher.d.ts.map +1 -1
  75. package/dist/domain/graph/watcher.js +10 -4
  76. package/dist/domain/graph/watcher.js.map +1 -1
  77. package/dist/domain/parser.d.ts +12 -23
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +126 -79
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/search/generator.d.ts +3 -1
  82. package/dist/domain/search/generator.d.ts.map +1 -1
  83. package/dist/domain/search/generator.js +68 -45
  84. package/dist/domain/search/generator.js.map +1 -1
  85. package/dist/domain/search/models.d.ts +2 -0
  86. package/dist/domain/search/models.d.ts.map +1 -1
  87. package/dist/domain/search/models.js +37 -3
  88. package/dist/domain/search/models.js.map +1 -1
  89. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  90. package/dist/domain/search/search/hybrid.js +49 -40
  91. package/dist/domain/search/search/hybrid.js.map +1 -1
  92. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  93. package/dist/domain/search/search/semantic.js +69 -49
  94. package/dist/domain/search/search/semantic.js.map +1 -1
  95. package/dist/domain/wasm-worker-entry.js +201 -136
  96. package/dist/domain/wasm-worker-entry.js.map +1 -1
  97. package/dist/extractors/elixir.js +95 -71
  98. package/dist/extractors/elixir.js.map +1 -1
  99. package/dist/extractors/gleam.d.ts.map +1 -1
  100. package/dist/extractors/gleam.js +23 -31
  101. package/dist/extractors/gleam.js.map +1 -1
  102. package/dist/extractors/helpers.d.ts +79 -1
  103. package/dist/extractors/helpers.d.ts.map +1 -1
  104. package/dist/extractors/helpers.js +137 -0
  105. package/dist/extractors/helpers.js.map +1 -1
  106. package/dist/extractors/java.d.ts.map +1 -1
  107. package/dist/extractors/java.js +37 -49
  108. package/dist/extractors/java.js.map +1 -1
  109. package/dist/extractors/javascript.d.ts.map +1 -1
  110. package/dist/extractors/javascript.js +44 -44
  111. package/dist/extractors/javascript.js.map +1 -1
  112. package/dist/extractors/julia.js +27 -34
  113. package/dist/extractors/julia.js.map +1 -1
  114. package/dist/extractors/r.d.ts.map +1 -1
  115. package/dist/extractors/r.js +33 -58
  116. package/dist/extractors/r.js.map +1 -1
  117. package/dist/extractors/solidity.d.ts.map +1 -1
  118. package/dist/extractors/solidity.js +38 -61
  119. package/dist/extractors/solidity.js.map +1 -1
  120. package/dist/features/boundaries.d.ts.map +1 -1
  121. package/dist/features/boundaries.js +49 -39
  122. package/dist/features/boundaries.js.map +1 -1
  123. package/dist/features/cfg.d.ts.map +1 -1
  124. package/dist/features/cfg.js +90 -63
  125. package/dist/features/cfg.js.map +1 -1
  126. package/dist/features/check.d.ts.map +1 -1
  127. package/dist/features/check.js +43 -34
  128. package/dist/features/check.js.map +1 -1
  129. package/dist/features/cochange.d.ts.map +1 -1
  130. package/dist/features/cochange.js +68 -56
  131. package/dist/features/cochange.js.map +1 -1
  132. package/dist/features/complexity.d.ts.map +1 -1
  133. package/dist/features/complexity.js +105 -75
  134. package/dist/features/complexity.js.map +1 -1
  135. package/dist/features/dataflow.d.ts.map +1 -1
  136. package/dist/features/dataflow.js +37 -29
  137. package/dist/features/dataflow.js.map +1 -1
  138. package/dist/features/flow.d.ts.map +1 -1
  139. package/dist/features/flow.js +31 -22
  140. package/dist/features/flow.js.map +1 -1
  141. package/dist/features/graph-enrichment.d.ts.map +1 -1
  142. package/dist/features/graph-enrichment.js +77 -70
  143. package/dist/features/graph-enrichment.js.map +1 -1
  144. package/dist/features/owners.d.ts +17 -26
  145. package/dist/features/owners.d.ts.map +1 -1
  146. package/dist/features/owners.js +120 -109
  147. package/dist/features/owners.js.map +1 -1
  148. package/dist/features/sequence.d.ts.map +1 -1
  149. package/dist/features/sequence.js +59 -54
  150. package/dist/features/sequence.js.map +1 -1
  151. package/dist/features/structure-query.d.ts.map +1 -1
  152. package/dist/features/structure-query.js +60 -60
  153. package/dist/features/structure-query.js.map +1 -1
  154. package/dist/features/structure.d.ts.map +1 -1
  155. package/dist/features/structure.js +149 -52
  156. package/dist/features/structure.js.map +1 -1
  157. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  158. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  159. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  160. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  161. package/dist/graph/classifiers/roles.js +63 -59
  162. package/dist/graph/classifiers/roles.js.map +1 -1
  163. package/dist/infrastructure/config.d.ts +1 -1
  164. package/dist/infrastructure/config.d.ts.map +1 -1
  165. package/dist/infrastructure/config.js +1 -1
  166. package/dist/infrastructure/config.js.map +1 -1
  167. package/dist/presentation/cfg.d.ts.map +1 -1
  168. package/dist/presentation/cfg.js +44 -29
  169. package/dist/presentation/cfg.js.map +1 -1
  170. package/dist/presentation/flow.d.ts.map +1 -1
  171. package/dist/presentation/flow.js +58 -38
  172. package/dist/presentation/flow.js.map +1 -1
  173. package/dist/types.d.ts +1 -1
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-erlang.wasm +0 -0
  176. package/package.json +9 -9
  177. package/src/ast-analysis/engine.ts +145 -61
  178. package/src/ast-analysis/visitor-utils.ts +86 -46
  179. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  180. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  181. package/src/cli/commands/embed.ts +54 -4
  182. package/src/domain/analysis/dependencies.ts +166 -85
  183. package/src/domain/analysis/fn-impact.ts +120 -50
  184. package/src/domain/analysis/module-map.ts +175 -140
  185. package/src/domain/graph/builder/call-resolver.ts +181 -0
  186. package/src/domain/graph/builder/helpers.ts +85 -76
  187. package/src/domain/graph/builder/incremental.ts +321 -152
  188. package/src/domain/graph/builder/pipeline.ts +19 -957
  189. package/src/domain/graph/builder/stages/build-edges.ts +229 -275
  190. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  191. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  192. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  193. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  194. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  195. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  196. package/src/domain/graph/cycles.ts +51 -49
  197. package/src/domain/graph/journal.ts +84 -69
  198. package/src/domain/graph/watcher.ts +12 -4
  199. package/src/domain/parser.ts +143 -66
  200. package/src/domain/search/generator.ts +132 -74
  201. package/src/domain/search/models.ts +39 -3
  202. package/src/domain/search/search/hybrid.ts +53 -42
  203. package/src/domain/search/search/semantic.ts +105 -65
  204. package/src/domain/wasm-worker-entry.ts +235 -152
  205. package/src/extractors/elixir.ts +91 -64
  206. package/src/extractors/gleam.ts +33 -37
  207. package/src/extractors/helpers.ts +205 -1
  208. package/src/extractors/java.ts +42 -45
  209. package/src/extractors/javascript.ts +44 -43
  210. package/src/extractors/julia.ts +28 -35
  211. package/src/extractors/r.ts +38 -56
  212. package/src/extractors/solidity.ts +43 -71
  213. package/src/features/boundaries.ts +64 -46
  214. package/src/features/cfg.ts +145 -74
  215. package/src/features/check.ts +60 -43
  216. package/src/features/cochange.ts +95 -72
  217. package/src/features/complexity.ts +134 -79
  218. package/src/features/dataflow.ts +57 -34
  219. package/src/features/flow.ts +48 -24
  220. package/src/features/graph-enrichment.ts +105 -70
  221. package/src/features/owners.ts +186 -146
  222. package/src/features/sequence.ts +99 -69
  223. package/src/features/structure-query.ts +94 -79
  224. package/src/features/structure.ts +199 -79
  225. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  226. package/src/graph/classifiers/roles.ts +64 -54
  227. package/src/infrastructure/config.ts +1 -1
  228. package/src/presentation/cfg.ts +48 -32
  229. package/src/presentation/flow.ts +100 -52
  230. package/src/types.ts +1 -1
@@ -88,12 +88,10 @@ export function runLouvainUndirectedModularity(
88
88
  optionsInput: LeidenOptions = {},
89
89
  ): LouvainResult {
90
90
  const options: NormalizedOptions = normalizeOptions(optionsInput);
91
- let currentGraph: CodeGraph = graph;
92
- const levels: LevelEntry[] = [];
93
91
  const rngSource = createRng(options.randomSeed);
94
92
  const random: () => number = () => rngSource.nextDouble();
95
93
 
96
- const baseGraphAdapter: GraphAdapter = makeGraphAdapter(currentGraph, {
94
+ const baseGraphAdapter: GraphAdapter = makeGraphAdapter(graph, {
97
95
  directed: options.directed,
98
96
  ...optionsInput,
99
97
  });
@@ -101,98 +99,27 @@ export function runLouvainUndirectedModularity(
101
99
  const originalToCurrent = new Int32Array(origN);
102
100
  for (let i = 0; i < origN; i++) originalToCurrent[i] = i;
103
101
 
104
- let fixedNodeMask: Uint8Array | null = null;
105
- if (options.fixedNodes) {
106
- const fixed = new Uint8Array(origN);
107
- const asSet: Set<string> =
108
- options.fixedNodes instanceof Set ? options.fixedNodes : new Set(options.fixedNodes);
109
- for (const id of asSet) {
110
- const idx = baseGraphAdapter.idToIndex.get(String(id));
111
- if (idx != null) fixed[idx] = 1;
112
- }
113
- fixedNodeMask = fixed;
114
- }
102
+ const fixedNodeMask: Uint8Array | null = buildFixedNodeMask(baseGraphAdapter, options.fixedNodes);
115
103
 
104
+ const levels: LevelEntry[] = [];
105
+ let currentGraph: CodeGraph = graph;
116
106
  for (let level = 0; level < options.maxLevels; level++) {
117
107
  const graphAdapter: GraphAdapter =
118
108
  level === 0
119
109
  ? baseGraphAdapter
120
110
  : makeGraphAdapter(currentGraph, { directed: options.directed, ...optionsInput });
121
- const partition: Partition = makePartition(graphAdapter);
122
- partition.graph = graphAdapter;
123
- partition.initializeAggregates();
124
-
125
- const order = new Int32Array(graphAdapter.n);
126
- for (let i = 0; i < graphAdapter.n; i++) order[i] = i;
127
-
128
- let improved: boolean = true;
129
- let localPasses: number = 0;
130
- const strategyCode: CandidateStrategyCode = options.candidateStrategyCode;
131
- while (improved) {
132
- improved = false;
133
- localPasses++;
134
- shuffleArrayInPlace(order, random);
135
- for (let idx = 0; idx < order.length; idx++) {
136
- const nodeIndex: number = order[idx]!;
137
- if (level === 0 && fixedNodeMask && fixedNodeMask[nodeIndex]) continue;
138
- const candidateCount: number = partition.accumulateNeighborCommunityEdgeWeights(nodeIndex);
139
- const { bestCommunityId, bestGain } = findBestCommunityMove(
140
- partition,
141
- graphAdapter,
142
- nodeIndex,
143
- candidateCount,
144
- strategyCode,
145
- options,
146
- random,
147
- );
148
- if (bestCommunityId !== partition.nodeCommunity[nodeIndex]! && bestGain > GAIN_EPSILON) {
149
- partition.moveNodeToCommunity(nodeIndex, bestCommunityId);
150
- improved = true;
151
- }
152
- }
153
- if (localPasses >= options.maxLocalPasses) break;
154
- }
155
-
156
- renumberCommunities(partition, options.preserveLabels);
157
-
158
- let effectivePartition: Partition = partition;
159
- if (options.refine) {
160
- const refined: Partition = refineWithinCoarseCommunities(
161
- graphAdapter,
162
- partition,
163
- random,
164
- options,
165
- level === 0 ? fixedNodeMask : null,
166
- );
167
- // Post-refinement: split any disconnected communities into their
168
- // connected components. This is the cheap O(V+E) alternative to
169
- // checking gamma-connectedness on every candidate during refinement.
170
- // A disconnected community violates even basic connectivity, so
171
- // splitting is always correct.
172
- splitDisconnectedCommunities(graphAdapter, refined);
173
- renumberCommunities(refined, options.preserveLabels);
174
- effectivePartition = refined;
175
- }
111
+ const levelOutcome = runLevel(
112
+ graphAdapter,
113
+ options,
114
+ random,
115
+ level === 0 ? fixedNodeMask : null,
116
+ );
176
117
 
177
- levels.push({ graph: graphAdapter, partition: effectivePartition });
178
- const fineToCoarse: Int32Array = effectivePartition.nodeCommunity;
179
- for (let i = 0; i < originalToCurrent.length; i++) {
180
- originalToCurrent[i] = fineToCoarse[originalToCurrent[i]!]!;
181
- }
118
+ levels.push({ graph: graphAdapter, partition: levelOutcome.effectivePartition });
119
+ applyFineToCoarseMapping(originalToCurrent, levelOutcome.effectivePartition.nodeCommunity);
182
120
 
183
- // Terminate when no further coarsening is possible. Check both the
184
- // move-phase partition (did the greedy phase find merges?) and the
185
- // effective partition that feeds buildCoarseGraph (would coarsening
186
- // actually reduce the graph?). When refine is enabled the refined
187
- // partition starts from singletons and may have more communities than
188
- // the move phase found, so checking only effectivePartition would
189
- // cause premature termination.
190
- if (
191
- partition.communityCount === graphAdapter.n &&
192
- effectivePartition.communityCount === graphAdapter.n
193
- )
194
- break;
195
- currentGraph = buildCoarseGraph(graphAdapter, effectivePartition);
121
+ if (levelOutcome.terminate) break;
122
+ currentGraph = buildCoarseGraph(graphAdapter, levelOutcome.effectivePartition);
196
123
  }
197
124
 
198
125
  const last: LevelEntry = levels[levels.length - 1]!;
@@ -206,6 +133,134 @@ export function runLouvainUndirectedModularity(
206
133
  };
207
134
  }
208
135
 
136
+ /**
137
+ * Build a fixed-node mask aligned with the base graph adapter's node indices.
138
+ * Returns null when no fixed nodes are configured.
139
+ */
140
+ function buildFixedNodeMask(
141
+ baseGraphAdapter: GraphAdapter,
142
+ fixedNodes: Set<string> | string[] | undefined,
143
+ ): Uint8Array | null {
144
+ if (!fixedNodes) return null;
145
+ const mask = new Uint8Array(baseGraphAdapter.n);
146
+ const asSet: Set<string> = fixedNodes instanceof Set ? fixedNodes : new Set(fixedNodes);
147
+ for (const id of asSet) {
148
+ const idx = baseGraphAdapter.idToIndex.get(String(id));
149
+ if (idx != null) mask[idx] = 1;
150
+ }
151
+ return mask;
152
+ }
153
+
154
+ interface LevelOutcome {
155
+ effectivePartition: Partition;
156
+ terminate: boolean;
157
+ }
158
+
159
+ /**
160
+ * Run one level of the Louvain/Leiden pipeline: greedy local-move phase,
161
+ * optional Leiden refinement, and a termination check. Returns the
162
+ * partition that feeds the next coarse graph plus a `terminate` flag set
163
+ * when no further coarsening is possible.
164
+ */
165
+ function runLevel(
166
+ graphAdapter: GraphAdapter,
167
+ options: NormalizedOptions,
168
+ random: () => number,
169
+ fixedNodeMask: Uint8Array | null,
170
+ ): LevelOutcome {
171
+ const partition: Partition = makePartition(graphAdapter);
172
+ partition.graph = graphAdapter;
173
+ partition.initializeAggregates();
174
+
175
+ runLocalMovePhase(graphAdapter, partition, options, random, fixedNodeMask);
176
+ renumberCommunities(partition, options.preserveLabels);
177
+
178
+ let effectivePartition: Partition = partition;
179
+ if (options.refine) {
180
+ const refined: Partition = refineWithinCoarseCommunities(
181
+ graphAdapter,
182
+ partition,
183
+ random,
184
+ options,
185
+ fixedNodeMask,
186
+ );
187
+ // Post-refinement: split any disconnected communities into their
188
+ // connected components. This is the cheap O(V+E) alternative to
189
+ // checking gamma-connectedness on every candidate during refinement.
190
+ // A disconnected community violates even basic connectivity, so
191
+ // splitting is always correct.
192
+ splitDisconnectedCommunities(graphAdapter, refined);
193
+ renumberCommunities(refined, options.preserveLabels);
194
+ effectivePartition = refined;
195
+ }
196
+
197
+ // Terminate when no further coarsening is possible. Check both the
198
+ // move-phase partition (did the greedy phase find merges?) and the
199
+ // effective partition that feeds buildCoarseGraph (would coarsening
200
+ // actually reduce the graph?). When refine is enabled the refined
201
+ // partition starts from singletons and may have more communities than
202
+ // the move phase found, so checking only effectivePartition would
203
+ // cause premature termination.
204
+ const terminate =
205
+ partition.communityCount === graphAdapter.n &&
206
+ effectivePartition.communityCount === graphAdapter.n;
207
+ return { effectivePartition, terminate };
208
+ }
209
+
210
+ /**
211
+ * Greedy local-move phase: iterate randomly over nodes, moving each to the
212
+ * best community among the candidate set. Loops until no improvement or
213
+ * `maxLocalPasses` is reached.
214
+ */
215
+ function runLocalMovePhase(
216
+ graphAdapter: GraphAdapter,
217
+ partition: Partition,
218
+ options: NormalizedOptions,
219
+ random: () => number,
220
+ fixedNodeMask: Uint8Array | null,
221
+ ): void {
222
+ const order = new Int32Array(graphAdapter.n);
223
+ for (let i = 0; i < graphAdapter.n; i++) order[i] = i;
224
+
225
+ const strategyCode: CandidateStrategyCode = options.candidateStrategyCode;
226
+ let improved: boolean = true;
227
+ let localPasses: number = 0;
228
+ while (improved) {
229
+ improved = false;
230
+ localPasses++;
231
+ shuffleArrayInPlace(order, random);
232
+ for (let idx = 0; idx < order.length; idx++) {
233
+ const nodeIndex: number = order[idx]!;
234
+ if (fixedNodeMask?.[nodeIndex]) continue;
235
+ const candidateCount: number = partition.accumulateNeighborCommunityEdgeWeights(nodeIndex);
236
+ const { bestCommunityId, bestGain } = findBestCommunityMove(
237
+ partition,
238
+ graphAdapter,
239
+ nodeIndex,
240
+ candidateCount,
241
+ strategyCode,
242
+ options,
243
+ random,
244
+ );
245
+ if (bestCommunityId !== partition.nodeCommunity[nodeIndex]! && bestGain > GAIN_EPSILON) {
246
+ partition.moveNodeToCommunity(nodeIndex, bestCommunityId);
247
+ improved = true;
248
+ }
249
+ }
250
+ if (localPasses >= options.maxLocalPasses) break;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Compose the running `originalToCurrent` mapping with this level's
256
+ * fine→coarse community labels, in place.
257
+ */
258
+ function applyFineToCoarseMapping(originalToCurrent: Int32Array, fineToCoarse: Int32Array): void {
259
+ for (let i = 0; i < originalToCurrent.length; i++) {
260
+ originalToCurrent[i] = fineToCoarse[originalToCurrent[i]!]!;
261
+ }
262
+ }
263
+
209
264
  /**
210
265
  * Evaluate all candidate communities for a node and return the best move.
211
266
  * Encapsulates the four candidate-selection strategies (All, RandomAny,
@@ -78,6 +78,68 @@ export interface RoleClassificationNode {
78
78
  hasActiveFileSiblings?: boolean;
79
79
  }
80
80
 
81
+ /**
82
+ * Compute median fan-in and fan-out across nodes with non-zero values.
83
+ * Used as thresholds for "high" fan-in/out classification.
84
+ */
85
+ function computeFanMedians(nodes: RoleClassificationNode[]): { fanIn: number; fanOut: number } {
86
+ const nonZeroFanIn = nodes
87
+ .filter((n) => n.fanIn > 0)
88
+ .map((n) => n.fanIn)
89
+ .sort((a, b) => a - b);
90
+ const nonZeroFanOut = nodes
91
+ .filter((n) => n.fanOut > 0)
92
+ .map((n) => n.fanOut)
93
+ .sort((a, b) => a - b);
94
+ return { fanIn: median(nonZeroFanIn), fanOut: median(nonZeroFanOut) };
95
+ }
96
+
97
+ /**
98
+ * Classify a node with `fanIn === 0` that is not exported.
99
+ * Covers framework-active constants, test-only callables, and the dead-* family.
100
+ */
101
+ function classifyUnreferencedNode(node: RoleClassificationNode): Role {
102
+ if (node.kind === 'constant' && node.hasActiveFileSiblings) {
103
+ // Constants consumed via identifier reference (not calls) have no
104
+ // inbound call edges. If the same file has active callables, the
105
+ // constant is almost certainly used locally — classify as leaf.
106
+ return 'leaf';
107
+ }
108
+ if (node.testOnlyFanIn != null && node.testOnlyFanIn > 0) return 'test-only';
109
+ return classifyDeadSubRole(node);
110
+ }
111
+
112
+ /**
113
+ * Pick a role from fan-in/fan-out shape: core/utility/adapter/leaf.
114
+ * Called after entry/test-only/dead cases have been ruled out.
115
+ */
116
+ function classifyByFanShape(highIn: boolean, highOut: boolean): Role {
117
+ if (highIn && !highOut) return 'core';
118
+ if (highIn && highOut) return 'utility';
119
+ if (!highIn && highOut) return 'adapter';
120
+ return 'leaf';
121
+ }
122
+
123
+ /**
124
+ * Apply role-classification rules to a single node.
125
+ * Order matters — framework entries are tagged first, then dead/test cases,
126
+ * then the fan-in/fan-out shape decides among the structural roles.
127
+ */
128
+ function classifyNodeRole(node: RoleClassificationNode, medFanIn: number, medFanOut: number): Role {
129
+ if (FRAMEWORK_ENTRY_PREFIXES.some((p) => node.name.startsWith(p))) return 'entry';
130
+
131
+ if (node.fanIn === 0) {
132
+ return node.isExported ? 'entry' : classifyUnreferencedNode(node);
133
+ }
134
+
135
+ const hasProdFanIn = typeof node.productionFanIn === 'number';
136
+ if (hasProdFanIn && node.productionFanIn === 0 && !node.isExported) return 'test-only';
137
+
138
+ const highIn = node.fanIn >= medFanIn;
139
+ const highOut = node.fanOut >= medFanOut && node.fanOut > 0;
140
+ return classifyByFanShape(highIn, highOut);
141
+ }
142
+
81
143
  /**
82
144
  * Classify nodes into architectural roles based on fan-in/fan-out metrics.
83
145
  */
@@ -87,63 +149,11 @@ export function classifyRoles(
87
149
  ): Map<string, Role> {
88
150
  if (nodes.length === 0) return new Map();
89
151
 
90
- let medFanIn: number;
91
- let medFanOut: number;
92
- if (medianOverrides) {
93
- medFanIn = medianOverrides.fanIn;
94
- medFanOut = medianOverrides.fanOut;
95
- } else {
96
- const nonZeroFanIn = nodes
97
- .filter((n) => n.fanIn > 0)
98
- .map((n) => n.fanIn)
99
- .sort((a, b) => a - b);
100
- const nonZeroFanOut = nodes
101
- .filter((n) => n.fanOut > 0)
102
- .map((n) => n.fanOut)
103
- .sort((a, b) => a - b);
104
- medFanIn = median(nonZeroFanIn);
105
- medFanOut = median(nonZeroFanOut);
106
- }
152
+ const { fanIn: medFanIn, fanOut: medFanOut } = medianOverrides ?? computeFanMedians(nodes);
107
153
 
108
154
  const result = new Map<string, Role>();
109
-
110
155
  for (const node of nodes) {
111
- const highIn = node.fanIn >= medFanIn && node.fanIn > 0;
112
- const highOut = node.fanOut >= medFanOut && node.fanOut > 0;
113
- const hasProdFanIn = typeof node.productionFanIn === 'number';
114
-
115
- let role: Role;
116
- const isFrameworkEntry = FRAMEWORK_ENTRY_PREFIXES.some((p) => node.name.startsWith(p));
117
- if (isFrameworkEntry) {
118
- role = 'entry';
119
- } else if (node.fanIn === 0 && !node.isExported) {
120
- if (node.kind === 'constant' && node.hasActiveFileSiblings) {
121
- // Constants consumed via identifier reference (not calls) have no
122
- // inbound call edges. If the same file has active callables, the
123
- // constant is almost certainly used locally — classify as leaf.
124
- role = 'leaf';
125
- } else {
126
- role =
127
- node.testOnlyFanIn != null && node.testOnlyFanIn > 0
128
- ? 'test-only'
129
- : classifyDeadSubRole(node);
130
- }
131
- } else if (node.fanIn === 0 && node.isExported) {
132
- role = 'entry';
133
- } else if (hasProdFanIn && node.fanIn > 0 && node.productionFanIn === 0 && !node.isExported) {
134
- role = 'test-only';
135
- } else if (highIn && !highOut) {
136
- role = 'core';
137
- } else if (highIn && highOut) {
138
- role = 'utility';
139
- } else if (!highIn && highOut) {
140
- role = 'adapter';
141
- } else {
142
- role = 'leaf';
143
- }
144
-
145
- result.set(node.id, role);
156
+ result.set(node.id, classifyNodeRole(node, medFanIn, medFanOut));
146
157
  }
147
-
148
158
  return result;
149
159
  }
@@ -30,7 +30,7 @@ export const DEFAULTS = {
30
30
  defaultLimit: 20,
31
31
  excludeTests: false,
32
32
  },
33
- embeddings: { model: 'nomic-v1.5', llmProvider: null as string | null },
33
+ embeddings: { model: null as string | null, llmProvider: null as string | null },
34
34
  llm: {
35
35
  provider: null as string | null,
36
36
  model: null as string | null,
@@ -1,6 +1,8 @@
1
1
  import { cfgData, cfgToDOT, cfgToMermaid } from '../features/cfg.js';
2
2
  import { outputResult } from '../infrastructure/result-formatter.js';
3
3
 
4
+ type CfgData = ReturnType<typeof cfgData>;
5
+
4
6
  interface CfgCliOpts {
5
7
  json?: boolean;
6
8
  ndjson?: boolean;
@@ -36,13 +38,56 @@ interface CfgResultEntry {
36
38
  edges: CfgEdge[];
37
39
  }
38
40
 
41
+ function renderBlockLocation(b: CfgBlock): string {
42
+ if (!b.startLine) return '';
43
+ const endSuffix = b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : '';
44
+ return ` L${b.startLine}${endSuffix}`;
45
+ }
46
+
47
+ function printCfgBlocks(blocks: CfgBlock[]): void {
48
+ if (blocks.length === 0) return;
49
+ console.log('\n Blocks:');
50
+ for (const b of blocks) {
51
+ const label = b.label ? ` (${b.label})` : '';
52
+ console.log(` [${b.index}] ${b.type}${label}${renderBlockLocation(b)}`);
53
+ }
54
+ }
55
+
56
+ function printCfgEdges(edges: CfgEdge[]): void {
57
+ if (edges.length === 0) return;
58
+ console.log('\n Edges:');
59
+ for (const e of edges) {
60
+ console.log(` B${e.source} → B${e.target} [${e.kind}]`);
61
+ }
62
+ }
63
+
64
+ function printCfgEntry(r: CfgResultEntry): void {
65
+ console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
66
+ console.log('─'.repeat(60));
67
+ console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
68
+ printCfgBlocks(r.blocks);
69
+ printCfgEdges(r.edges);
70
+ }
71
+
72
+ function tryRenderGraphFormat(format: string, data: CfgData): boolean {
73
+ if (format === 'dot') {
74
+ console.log(cfgToDOT(data));
75
+ return true;
76
+ }
77
+ if (format === 'mermaid') {
78
+ console.log(cfgToMermaid(data));
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+
39
84
  export function cfg(name: string, customDbPath: string | undefined, opts: CfgCliOpts = {}): void {
40
85
  const data = cfgData(name, customDbPath, opts);
41
86
 
42
87
  if (outputResult(data, 'results', opts)) return;
43
88
 
44
89
  if (data.warning) {
45
- console.log(`\u26A0 ${data.warning}`);
90
+ console.log(`⚠ ${data.warning}`);
46
91
  return;
47
92
  }
48
93
  if (data.results.length === 0) {
@@ -50,38 +95,9 @@ export function cfg(name: string, customDbPath: string | undefined, opts: CfgCli
50
95
  return;
51
96
  }
52
97
 
53
- const format = opts.format || 'text';
54
- if (format === 'dot') {
55
- console.log(cfgToDOT(data));
56
- return;
57
- }
58
- if (format === 'mermaid') {
59
- console.log(cfgToMermaid(data));
60
- return;
61
- }
98
+ if (tryRenderGraphFormat(opts.format || 'text', data)) return;
62
99
 
63
- // Text format
64
100
  for (const r of data.results as CfgResultEntry[]) {
65
- console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
66
- console.log('\u2500'.repeat(60));
67
- console.log(` Blocks: ${r.summary.blockCount} Edges: ${r.summary.edgeCount}`);
68
-
69
- if (r.blocks.length > 0) {
70
- console.log('\n Blocks:');
71
- for (const b of r.blocks) {
72
- const loc = b.startLine
73
- ? ` L${b.startLine}${b.endLine && b.endLine !== b.startLine ? `-${b.endLine}` : ''}`
74
- : '';
75
- const label = b.label ? ` (${b.label})` : '';
76
- console.log(` [${b.index}] ${b.type}${label}${loc}`);
77
- }
78
- }
79
-
80
- if (r.edges.length > 0) {
81
- console.log('\n Edges:');
82
- for (const e of r.edges) {
83
- console.log(` B${e.source} \u2192 B${e.target} [${e.kind}]`);
84
- }
85
- }
101
+ printCfgEntry(r);
86
102
  }
87
103
  }
@@ -16,54 +16,65 @@ interface FlowOpts {
16
16
  csv?: boolean;
17
17
  }
18
18
 
19
- export function flow(
20
- name: string | undefined,
21
- dbPath: string | undefined,
22
- opts: FlowOpts = {},
23
- ): void {
24
- if (opts.list) {
25
- const data = listEntryPointsData(dbPath, {
26
- noTests: opts.noTests,
27
- limit: opts.limit,
28
- offset: opts.offset,
29
- }) as any;
30
- if (outputResult(data, 'entries', opts)) return;
31
- if (data.count === 0) {
32
- console.log('No entry points found. Run "codegraph build" first.');
33
- return;
34
- }
35
- console.log(`\nEntry points (${data.count} total):\n`);
36
- for (const [type, entries] of Object.entries(
37
- data.byType as Record<
38
- string,
39
- Array<{ kind: string; name: string; file: string; line: number }>
40
- >,
41
- )) {
42
- console.log(` ${type} (${entries.length}):`);
43
- for (const e of entries) {
44
- console.log(` [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
45
- }
46
- console.log();
47
- }
48
- return;
49
- }
19
+ interface EntryPoint {
20
+ kind: string;
21
+ name: string;
22
+ file: string;
23
+ line: number;
24
+ }
50
25
 
51
- if (!name) {
52
- console.log(
53
- 'Please provide a function or entry-point name. Use --list to see available entry points.',
54
- );
55
- return;
56
- }
26
+ interface FlowNode {
27
+ kind: string;
28
+ name: string;
29
+ file: string;
30
+ line: number;
31
+ }
57
32
 
58
- const data = flowData(name, dbPath, opts) as any;
59
- if (outputResult(data, 'steps', opts)) return;
33
+ interface FlowStep {
34
+ depth: number;
35
+ nodes: FlowNode[];
36
+ }
60
37
 
61
- if (!data.entry) {
62
- console.log(`No matching entry point or function found for "${name}".`);
38
+ interface FlowCycle {
39
+ from: string;
40
+ to: string;
41
+ depth: number;
42
+ }
43
+
44
+ interface FlowResult {
45
+ entry?: { kind: string; name: string; type: string; file: string; line: number };
46
+ depth: number;
47
+ totalReached: number;
48
+ leaves: Array<{ name: string; file: string }>;
49
+ steps: FlowStep[];
50
+ cycles: FlowCycle[];
51
+ truncated?: boolean;
52
+ }
53
+
54
+ function runListEntryPoints(dbPath: string | undefined, opts: FlowOpts): void {
55
+ const data = listEntryPointsData(dbPath, {
56
+ noTests: opts.noTests,
57
+ limit: opts.limit,
58
+ offset: opts.offset,
59
+ }) as { count: number; byType: Record<string, EntryPoint[]> };
60
+ if (outputResult(data, 'entries', opts)) return;
61
+ if (data.count === 0) {
62
+ console.log('No entry points found. Run "codegraph build" first.');
63
63
  return;
64
64
  }
65
+ console.log(`\nEntry points (${data.count} total):\n`);
66
+ for (const [type, entries] of Object.entries(data.byType)) {
67
+ console.log(` ${type} (${entries.length}):`);
68
+ for (const e of entries) {
69
+ console.log(` [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
70
+ }
71
+ console.log();
72
+ }
73
+ }
65
74
 
75
+ function printFlowHeader(data: FlowResult): void {
66
76
  const e = data.entry;
77
+ if (!e) return;
67
78
  const typeTag = e.type !== 'exported' ? ` (${e.type})` : '';
68
79
  console.log(`\nFlow from: [${kindIcon(e.kind)}] ${e.name}${typeTag} ${e.file}:${e.line}`);
69
80
  console.log(
@@ -73,27 +84,64 @@ export function flow(
73
84
  console.log(` (truncated at depth ${data.depth})`);
74
85
  }
75
86
  console.log();
87
+ }
88
+
89
+ function isLeafNode(n: FlowNode, leaves: Array<{ name: string; file: string }>): boolean {
90
+ return leaves.some((l) => l.name === n.name && l.file === n.file);
91
+ }
76
92
 
93
+ /** Returns true when the node is a leaf (no steps); caller should skip cycle output. */
94
+ function printFlowSteps(data: FlowResult): boolean {
77
95
  if (data.steps.length === 0) {
78
96
  console.log(' (leaf node — no callees)');
79
- return;
97
+ return true;
80
98
  }
81
-
82
99
  for (const step of data.steps) {
83
100
  console.log(` depth ${step.depth}:`);
84
101
  for (const n of step.nodes) {
85
- const isLeaf = data.leaves.some(
86
- (l: { name: string; file: string }) => l.name === n.name && l.file === n.file,
87
- );
88
- const leafTag = isLeaf ? ' [leaf]' : '';
102
+ const leafTag = isLeafNode(n, data.leaves) ? ' [leaf]' : '';
89
103
  console.log(` [${kindIcon(n.kind)}] ${n.name} ${n.file}:${n.line}${leafTag}`);
90
104
  }
91
105
  }
106
+ return false;
107
+ }
108
+
109
+ function printFlowCycles(cycles: FlowCycle[]): void {
110
+ if (cycles.length === 0) return;
111
+ console.log('\n Cycles detected:');
112
+ for (const c of cycles) {
113
+ console.log(` ${c.from} -> ${c.to} (at depth ${c.depth})`);
114
+ }
115
+ }
92
116
 
93
- if (data.cycles.length > 0) {
94
- console.log('\n Cycles detected:');
95
- for (const c of data.cycles) {
96
- console.log(` ${c.from} -> ${c.to} (at depth ${c.depth})`);
97
- }
117
+ export function flow(
118
+ name: string | undefined,
119
+ dbPath: string | undefined,
120
+ opts: FlowOpts = {},
121
+ ): void {
122
+ if (opts.list) {
123
+ runListEntryPoints(dbPath, opts);
124
+ return;
125
+ }
126
+
127
+ if (!name) {
128
+ console.log(
129
+ 'Please provide a function or entry-point name. Use --list to see available entry points.',
130
+ );
131
+ return;
132
+ }
133
+
134
+ const data = flowData(name, dbPath, opts) as unknown as FlowResult;
135
+ if (outputResult(data, 'steps', opts)) return;
136
+
137
+ if (!data.entry) {
138
+ console.log(`No matching entry point or function found for "${name}".`);
139
+ return;
140
+ }
141
+
142
+ printFlowHeader(data);
143
+ const isLeaf = printFlowSteps(data);
144
+ if (!isLeaf) {
145
+ printFlowCycles(data.cycles);
98
146
  }
99
147
  }