@optave/codegraph 3.10.0 → 3.11.1

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 (312) hide show
  1. package/README.md +40 -33
  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/rules/index.d.ts.map +1 -1
  6. package/dist/ast-analysis/rules/index.js +77 -0
  7. package/dist/ast-analysis/rules/index.js.map +1 -1
  8. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  9. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitor-utils.js +83 -49
  11. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  12. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  14. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  15. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  17. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  18. package/dist/cli/commands/audit.js +1 -1
  19. package/dist/cli/commands/audit.js.map +1 -1
  20. package/dist/cli/commands/build.d.ts.map +1 -1
  21. package/dist/cli/commands/build.js +2 -0
  22. package/dist/cli/commands/build.js.map +1 -1
  23. package/dist/cli/commands/check.js +1 -1
  24. package/dist/cli/commands/check.js.map +1 -1
  25. package/dist/cli/commands/children.js +1 -1
  26. package/dist/cli/commands/children.js.map +1 -1
  27. package/dist/cli/commands/diff-impact.js +1 -1
  28. package/dist/cli/commands/diff-impact.js.map +1 -1
  29. package/dist/cli/commands/embed.d.ts.map +1 -1
  30. package/dist/cli/commands/embed.js +49 -4
  31. package/dist/cli/commands/embed.js.map +1 -1
  32. package/dist/cli/commands/roles.js +1 -1
  33. package/dist/cli/commands/roles.js.map +1 -1
  34. package/dist/cli/commands/structure.js +1 -1
  35. package/dist/cli/commands/structure.js.map +1 -1
  36. package/dist/cli/shared/options.js +1 -1
  37. package/dist/cli/shared/options.js.map +1 -1
  38. package/dist/db/connection.d.ts.map +1 -1
  39. package/dist/db/connection.js +8 -0
  40. package/dist/db/connection.js.map +1 -1
  41. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  42. package/dist/domain/analysis/dependencies.js +106 -80
  43. package/dist/domain/analysis/dependencies.js.map +1 -1
  44. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  45. package/dist/domain/analysis/fn-impact.js +77 -52
  46. package/dist/domain/analysis/fn-impact.js.map +1 -1
  47. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  48. package/dist/domain/analysis/module-map.js +132 -121
  49. package/dist/domain/analysis/module-map.js.map +1 -1
  50. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  51. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  52. package/dist/domain/graph/builder/helpers.js +47 -33
  53. package/dist/domain/graph/builder/helpers.js.map +1 -1
  54. package/dist/domain/graph/builder/incremental.d.ts +6 -6
  55. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  56. package/dist/domain/graph/builder/incremental.js +148 -99
  57. package/dist/domain/graph/builder/incremental.js.map +1 -1
  58. package/dist/domain/graph/builder/pipeline.d.ts +1 -0
  59. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  60. package/dist/domain/graph/builder/pipeline.js +23 -637
  61. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  62. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  63. package/dist/domain/graph/builder/stages/build-edges.js +141 -98
  64. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  65. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  66. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  67. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  68. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  69. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  70. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  71. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  72. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  73. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  74. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  75. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  76. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  77. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  78. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  79. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  80. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  81. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  82. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  83. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  84. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  85. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  86. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.js +73 -22
  88. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  89. package/dist/domain/graph/cycles.d.ts +6 -4
  90. package/dist/domain/graph/cycles.d.ts.map +1 -1
  91. package/dist/domain/graph/cycles.js +50 -55
  92. package/dist/domain/graph/cycles.js.map +1 -1
  93. package/dist/domain/graph/journal.d.ts.map +1 -1
  94. package/dist/domain/graph/journal.js +89 -70
  95. package/dist/domain/graph/journal.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts.map +1 -1
  97. package/dist/domain/graph/watcher.js +28 -20
  98. package/dist/domain/graph/watcher.js.map +1 -1
  99. package/dist/domain/parser.d.ts +12 -23
  100. package/dist/domain/parser.d.ts.map +1 -1
  101. package/dist/domain/parser.js +153 -80
  102. package/dist/domain/parser.js.map +1 -1
  103. package/dist/domain/search/generator.d.ts +3 -1
  104. package/dist/domain/search/generator.d.ts.map +1 -1
  105. package/dist/domain/search/generator.js +68 -45
  106. package/dist/domain/search/generator.js.map +1 -1
  107. package/dist/domain/search/models.d.ts +18 -0
  108. package/dist/domain/search/models.d.ts.map +1 -1
  109. package/dist/domain/search/models.js +72 -4
  110. package/dist/domain/search/models.js.map +1 -1
  111. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  112. package/dist/domain/search/search/hybrid.js +49 -40
  113. package/dist/domain/search/search/hybrid.js.map +1 -1
  114. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  115. package/dist/domain/search/search/semantic.js +69 -49
  116. package/dist/domain/search/search/semantic.js.map +1 -1
  117. package/dist/domain/wasm-worker-entry.js +209 -137
  118. package/dist/domain/wasm-worker-entry.js.map +1 -1
  119. package/dist/extractors/c.js +25 -6
  120. package/dist/extractors/c.js.map +1 -1
  121. package/dist/extractors/cpp.js +47 -6
  122. package/dist/extractors/cpp.js.map +1 -1
  123. package/dist/extractors/cuda.js +90 -14
  124. package/dist/extractors/cuda.js.map +1 -1
  125. package/dist/extractors/elixir.js +108 -4
  126. package/dist/extractors/elixir.js.map +1 -1
  127. package/dist/extractors/erlang.js +56 -20
  128. package/dist/extractors/erlang.js.map +1 -1
  129. package/dist/extractors/fsharp.d.ts +7 -0
  130. package/dist/extractors/fsharp.d.ts.map +1 -1
  131. package/dist/extractors/fsharp.js +94 -0
  132. package/dist/extractors/fsharp.js.map +1 -1
  133. package/dist/extractors/gleam.d.ts.map +1 -1
  134. package/dist/extractors/gleam.js +29 -33
  135. package/dist/extractors/gleam.js.map +1 -1
  136. package/dist/extractors/groovy.js +41 -1
  137. package/dist/extractors/groovy.js.map +1 -1
  138. package/dist/extractors/haskell.js +48 -4
  139. package/dist/extractors/haskell.js.map +1 -1
  140. package/dist/extractors/helpers.d.ts +79 -1
  141. package/dist/extractors/helpers.d.ts.map +1 -1
  142. package/dist/extractors/helpers.js +137 -0
  143. package/dist/extractors/helpers.js.map +1 -1
  144. package/dist/extractors/java.d.ts.map +1 -1
  145. package/dist/extractors/java.js +37 -49
  146. package/dist/extractors/java.js.map +1 -1
  147. package/dist/extractors/javascript.d.ts.map +1 -1
  148. package/dist/extractors/javascript.js +44 -44
  149. package/dist/extractors/javascript.js.map +1 -1
  150. package/dist/extractors/julia.js +198 -74
  151. package/dist/extractors/julia.js.map +1 -1
  152. package/dist/extractors/kotlin.js +4 -0
  153. package/dist/extractors/kotlin.js.map +1 -1
  154. package/dist/extractors/objc.js +184 -47
  155. package/dist/extractors/objc.js.map +1 -1
  156. package/dist/extractors/python.js +7 -4
  157. package/dist/extractors/python.js.map +1 -1
  158. package/dist/extractors/r.d.ts.map +1 -1
  159. package/dist/extractors/r.js +103 -87
  160. package/dist/extractors/r.js.map +1 -1
  161. package/dist/extractors/scala.d.ts.map +1 -1
  162. package/dist/extractors/scala.js +18 -32
  163. package/dist/extractors/scala.js.map +1 -1
  164. package/dist/extractors/solidity.d.ts.map +1 -1
  165. package/dist/extractors/solidity.js +55 -69
  166. package/dist/extractors/solidity.js.map +1 -1
  167. package/dist/extractors/verilog.js +80 -15
  168. package/dist/extractors/verilog.js.map +1 -1
  169. package/dist/features/boundaries.d.ts.map +1 -1
  170. package/dist/features/boundaries.js +49 -39
  171. package/dist/features/boundaries.js.map +1 -1
  172. package/dist/features/cfg.d.ts.map +1 -1
  173. package/dist/features/cfg.js +90 -63
  174. package/dist/features/cfg.js.map +1 -1
  175. package/dist/features/check.d.ts.map +1 -1
  176. package/dist/features/check.js +43 -34
  177. package/dist/features/check.js.map +1 -1
  178. package/dist/features/cochange.d.ts.map +1 -1
  179. package/dist/features/cochange.js +68 -56
  180. package/dist/features/cochange.js.map +1 -1
  181. package/dist/features/complexity.d.ts.map +1 -1
  182. package/dist/features/complexity.js +105 -75
  183. package/dist/features/complexity.js.map +1 -1
  184. package/dist/features/dataflow.d.ts.map +1 -1
  185. package/dist/features/dataflow.js +37 -29
  186. package/dist/features/dataflow.js.map +1 -1
  187. package/dist/features/flow.d.ts.map +1 -1
  188. package/dist/features/flow.js +31 -22
  189. package/dist/features/flow.js.map +1 -1
  190. package/dist/features/graph-enrichment.d.ts.map +1 -1
  191. package/dist/features/graph-enrichment.js +77 -70
  192. package/dist/features/graph-enrichment.js.map +1 -1
  193. package/dist/features/owners.d.ts +17 -26
  194. package/dist/features/owners.d.ts.map +1 -1
  195. package/dist/features/owners.js +120 -109
  196. package/dist/features/owners.js.map +1 -1
  197. package/dist/features/sequence.d.ts.map +1 -1
  198. package/dist/features/sequence.js +59 -54
  199. package/dist/features/sequence.js.map +1 -1
  200. package/dist/features/structure-query.d.ts.map +1 -1
  201. package/dist/features/structure-query.js +60 -60
  202. package/dist/features/structure-query.js.map +1 -1
  203. package/dist/features/structure.js +28 -36
  204. package/dist/features/structure.js.map +1 -1
  205. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  206. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  207. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  208. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  209. package/dist/graph/classifiers/roles.js +63 -59
  210. package/dist/graph/classifiers/roles.js.map +1 -1
  211. package/dist/infrastructure/config.d.ts +1 -1
  212. package/dist/infrastructure/config.d.ts.map +1 -1
  213. package/dist/infrastructure/config.js +1 -1
  214. package/dist/infrastructure/config.js.map +1 -1
  215. package/dist/mcp/tool-registry.d.ts.map +1 -1
  216. package/dist/mcp/tool-registry.js +4 -0
  217. package/dist/mcp/tool-registry.js.map +1 -1
  218. package/dist/mcp/tools/semantic-search.d.ts +1 -0
  219. package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
  220. package/dist/mcp/tools/semantic-search.js +1 -0
  221. package/dist/mcp/tools/semantic-search.js.map +1 -1
  222. package/dist/presentation/cfg.d.ts.map +1 -1
  223. package/dist/presentation/cfg.js +44 -29
  224. package/dist/presentation/cfg.js.map +1 -1
  225. package/dist/presentation/flow.d.ts.map +1 -1
  226. package/dist/presentation/flow.js +58 -38
  227. package/dist/presentation/flow.js.map +1 -1
  228. package/dist/types.d.ts +16 -2
  229. package/dist/types.d.ts.map +1 -1
  230. package/grammars/tree-sitter-erlang.wasm +0 -0
  231. package/grammars/tree-sitter-fsharp.wasm +0 -0
  232. package/grammars/tree-sitter-fsharp_signature.wasm +0 -0
  233. package/grammars/tree-sitter-gleam.wasm +0 -0
  234. package/package.json +10 -10
  235. package/src/ast-analysis/engine.ts +145 -61
  236. package/src/ast-analysis/rules/index.ts +87 -0
  237. package/src/ast-analysis/visitor-utils.ts +86 -46
  238. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  239. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  240. package/src/cli/commands/audit.ts +1 -1
  241. package/src/cli/commands/build.ts +2 -0
  242. package/src/cli/commands/check.ts +1 -1
  243. package/src/cli/commands/children.ts +1 -1
  244. package/src/cli/commands/diff-impact.ts +1 -1
  245. package/src/cli/commands/embed.ts +54 -4
  246. package/src/cli/commands/roles.ts +1 -1
  247. package/src/cli/commands/structure.ts +1 -1
  248. package/src/cli/shared/options.ts +1 -1
  249. package/src/db/connection.ts +8 -0
  250. package/src/domain/analysis/dependencies.ts +166 -85
  251. package/src/domain/analysis/fn-impact.ts +120 -50
  252. package/src/domain/analysis/module-map.ts +175 -140
  253. package/src/domain/graph/builder/helpers.ts +85 -76
  254. package/src/domain/graph/builder/incremental.ts +223 -131
  255. package/src/domain/graph/builder/pipeline.ts +32 -785
  256. package/src/domain/graph/builder/stages/build-edges.ts +207 -142
  257. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  258. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  259. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  260. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  261. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  262. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  263. package/src/domain/graph/builder/stages/resolve-imports.ts +79 -25
  264. package/src/domain/graph/cycles.ts +51 -49
  265. package/src/domain/graph/journal.ts +84 -69
  266. package/src/domain/graph/watcher.ts +29 -25
  267. package/src/domain/parser.ts +170 -67
  268. package/src/domain/search/generator.ts +132 -74
  269. package/src/domain/search/models.ts +75 -4
  270. package/src/domain/search/search/hybrid.ts +53 -42
  271. package/src/domain/search/search/semantic.ts +105 -65
  272. package/src/domain/wasm-worker-entry.ts +243 -153
  273. package/src/extractors/c.ts +27 -8
  274. package/src/extractors/cpp.ts +50 -8
  275. package/src/extractors/cuda.ts +90 -16
  276. package/src/extractors/elixir.ts +103 -4
  277. package/src/extractors/erlang.ts +63 -20
  278. package/src/extractors/fsharp.ts +104 -0
  279. package/src/extractors/gleam.ts +40 -39
  280. package/src/extractors/groovy.ts +45 -1
  281. package/src/extractors/haskell.ts +45 -4
  282. package/src/extractors/helpers.ts +205 -1
  283. package/src/extractors/java.ts +42 -45
  284. package/src/extractors/javascript.ts +44 -43
  285. package/src/extractors/julia.ts +191 -77
  286. package/src/extractors/kotlin.ts +4 -0
  287. package/src/extractors/objc.ts +171 -47
  288. package/src/extractors/python.ts +5 -3
  289. package/src/extractors/r.ts +104 -82
  290. package/src/extractors/scala.ts +24 -36
  291. package/src/extractors/solidity.ts +59 -78
  292. package/src/extractors/verilog.ts +83 -15
  293. package/src/features/boundaries.ts +64 -46
  294. package/src/features/cfg.ts +145 -74
  295. package/src/features/check.ts +60 -43
  296. package/src/features/cochange.ts +95 -72
  297. package/src/features/complexity.ts +134 -79
  298. package/src/features/dataflow.ts +57 -34
  299. package/src/features/flow.ts +48 -24
  300. package/src/features/graph-enrichment.ts +105 -70
  301. package/src/features/owners.ts +186 -146
  302. package/src/features/sequence.ts +99 -69
  303. package/src/features/structure-query.ts +94 -79
  304. package/src/features/structure.ts +56 -56
  305. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  306. package/src/graph/classifiers/roles.ts +64 -54
  307. package/src/infrastructure/config.ts +1 -1
  308. package/src/mcp/tool-registry.ts +5 -0
  309. package/src/mcp/tools/semantic-search.ts +2 -0
  310. package/src/presentation/cfg.ts +48 -32
  311. package/src/presentation/flow.ts +100 -52
  312. package/src/types.ts +16 -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,
@@ -322,6 +322,11 @@ const BASE_TOOLS: ToolSchema[] = [
322
322
  description:
323
323
  'Search mode: hybrid (BM25 + semantic, default), semantic (embeddings only), keyword (BM25 only)',
324
324
  },
325
+ file_pattern: {
326
+ oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
327
+ description:
328
+ 'Restrict results to files matching one or more glob or substring patterns (e.g. "db/", "src/**/*.ts", or ["db/", "src/"])',
329
+ },
325
330
  ...PAGINATION_PROPS,
326
331
  },
327
332
  required: ['query'],
@@ -9,6 +9,7 @@ interface SemanticSearchArgs {
9
9
  limit?: number;
10
10
  offset?: number;
11
11
  min_score?: number;
12
+ file_pattern?: string | string[];
12
13
  }
13
14
 
14
15
  export async function handler(args: SemanticSearchArgs, ctx: McpToolContext): Promise<unknown> {
@@ -17,6 +18,7 @@ export async function handler(args: SemanticSearchArgs, ctx: McpToolContext): Pr
17
18
  limit: Math.min(args.limit ?? MCP_DEFAULTS.semantic_search ?? 100, ctx.MCP_MAX_LIMIT),
18
19
  offset: effectiveOffset(args),
19
20
  minScore: args.min_score,
21
+ filePattern: args.file_pattern,
20
22
  };
21
23
 
22
24
  if (mode === 'keyword') {
@@ -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
  }