@optave/codegraph 3.8.0 → 3.8.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 (296) hide show
  1. package/README.md +9 -8
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +95 -87
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/metrics.d.ts +0 -3
  6. package/dist/ast-analysis/metrics.d.ts.map +1 -1
  7. package/dist/ast-analysis/metrics.js +30 -13
  8. package/dist/ast-analysis/metrics.js.map +1 -1
  9. package/dist/ast-analysis/shared.d.ts.map +1 -1
  10. package/dist/ast-analysis/shared.js +24 -19
  11. package/dist/ast-analysis/shared.js.map +1 -1
  12. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitor-utils.js +55 -39
  14. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  15. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  16. package/dist/ast-analysis/visitor.js +91 -70
  17. package/dist/ast-analysis/visitor.js.map +1 -1
  18. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  19. package/dist/ast-analysis/visitors/ast-store-visitor.js +54 -58
  20. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  21. package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
  22. package/dist/ast-analysis/visitors/complexity-visitor.js +32 -39
  23. package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
  24. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  25. package/dist/ast-analysis/visitors/dataflow-visitor.js +57 -38
  26. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  27. package/dist/cli/commands/watch.d.ts.map +1 -1
  28. package/dist/cli/commands/watch.js +16 -2
  29. package/dist/cli/commands/watch.js.map +1 -1
  30. package/dist/db/connection.d.ts.map +1 -1
  31. package/dist/db/connection.js +29 -26
  32. package/dist/db/connection.js.map +1 -1
  33. package/dist/db/query-builder.d.ts.map +1 -1
  34. package/dist/db/query-builder.js +16 -5
  35. package/dist/db/query-builder.js.map +1 -1
  36. package/dist/db/repository/base.d.ts +10 -0
  37. package/dist/db/repository/base.d.ts.map +1 -1
  38. package/dist/db/repository/base.js +17 -0
  39. package/dist/db/repository/base.js.map +1 -1
  40. package/dist/db/repository/native-repository.d.ts +6 -1
  41. package/dist/db/repository/native-repository.d.ts.map +1 -1
  42. package/dist/db/repository/native-repository.js +77 -1
  43. package/dist/db/repository/native-repository.js.map +1 -1
  44. package/dist/db/repository/nodes.d.ts.map +1 -1
  45. package/dist/db/repository/nodes.js +8 -4
  46. package/dist/db/repository/nodes.js.map +1 -1
  47. package/dist/db/repository/sqlite-repository.d.ts +3 -0
  48. package/dist/db/repository/sqlite-repository.d.ts.map +1 -1
  49. package/dist/db/repository/sqlite-repository.js +26 -0
  50. package/dist/db/repository/sqlite-repository.js.map +1 -1
  51. package/dist/domain/analysis/brief.d.ts.map +1 -1
  52. package/dist/domain/analysis/brief.js +13 -17
  53. package/dist/domain/analysis/brief.js.map +1 -1
  54. package/dist/domain/analysis/context.d.ts.map +1 -1
  55. package/dist/domain/analysis/context.js +14 -11
  56. package/dist/domain/analysis/context.js.map +1 -1
  57. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  58. package/dist/domain/analysis/dependencies.js +53 -52
  59. package/dist/domain/analysis/dependencies.js.map +1 -1
  60. package/dist/domain/analysis/fn-impact.d.ts +2 -7
  61. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  62. package/dist/domain/analysis/fn-impact.js +33 -31
  63. package/dist/domain/analysis/fn-impact.js.map +1 -1
  64. package/dist/domain/analysis/implementations.d.ts.map +1 -1
  65. package/dist/domain/analysis/implementations.js +11 -19
  66. package/dist/domain/analysis/implementations.js.map +1 -1
  67. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  68. package/dist/domain/analysis/module-map.js +55 -76
  69. package/dist/domain/analysis/module-map.js.map +1 -1
  70. package/dist/domain/analysis/query-helpers.d.ts +7 -0
  71. package/dist/domain/analysis/query-helpers.d.ts.map +1 -1
  72. package/dist/domain/analysis/query-helpers.js +15 -1
  73. package/dist/domain/analysis/query-helpers.js.map +1 -1
  74. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  75. package/dist/domain/graph/builder/pipeline.js +255 -105
  76. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  77. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  78. package/dist/domain/graph/builder/stages/build-edges.js +22 -17
  79. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  80. package/dist/domain/graph/builder/stages/detect-changes.js +2 -2
  81. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  82. package/dist/domain/graph/builder/stages/finalize.js +2 -2
  83. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  84. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  85. package/dist/domain/graph/builder/stages/insert-nodes.js +32 -21
  86. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  87. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  88. package/dist/domain/graph/builder/stages/resolve-imports.js +95 -84
  89. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  90. package/dist/domain/graph/cycles.d.ts +6 -0
  91. package/dist/domain/graph/cycles.d.ts.map +1 -1
  92. package/dist/domain/graph/cycles.js +114 -22
  93. package/dist/domain/graph/cycles.js.map +1 -1
  94. package/dist/domain/graph/resolve.js +1 -1
  95. package/dist/domain/graph/resolve.js.map +1 -1
  96. package/dist/domain/graph/watcher.d.ts +2 -0
  97. package/dist/domain/graph/watcher.d.ts.map +1 -1
  98. package/dist/domain/graph/watcher.js +170 -75
  99. package/dist/domain/graph/watcher.js.map +1 -1
  100. package/dist/domain/parser.d.ts +0 -5
  101. package/dist/domain/parser.d.ts.map +1 -1
  102. package/dist/domain/parser.js +13 -28
  103. package/dist/domain/parser.js.map +1 -1
  104. package/dist/domain/search/generator.js +1 -1
  105. package/dist/domain/search/generator.js.map +1 -1
  106. package/dist/domain/search/models.d.ts +4 -3
  107. package/dist/domain/search/models.d.ts.map +1 -1
  108. package/dist/domain/search/models.js +18 -5
  109. package/dist/domain/search/models.js.map +1 -1
  110. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  111. package/dist/domain/search/search/hybrid.js +29 -18
  112. package/dist/domain/search/search/hybrid.js.map +1 -1
  113. package/dist/extractors/go.js +36 -33
  114. package/dist/extractors/go.js.map +1 -1
  115. package/dist/extractors/helpers.d.ts.map +1 -1
  116. package/dist/extractors/helpers.js +40 -29
  117. package/dist/extractors/helpers.js.map +1 -1
  118. package/dist/extractors/java.js +58 -46
  119. package/dist/extractors/java.js.map +1 -1
  120. package/dist/extractors/javascript.js +46 -45
  121. package/dist/extractors/javascript.js.map +1 -1
  122. package/dist/extractors/kotlin.js +84 -78
  123. package/dist/extractors/kotlin.js.map +1 -1
  124. package/dist/extractors/python.js +29 -24
  125. package/dist/extractors/python.js.map +1 -1
  126. package/dist/extractors/rust.js +41 -32
  127. package/dist/extractors/rust.js.map +1 -1
  128. package/dist/extractors/solidity.js +58 -67
  129. package/dist/extractors/solidity.js.map +1 -1
  130. package/dist/extractors/swift.js +83 -81
  131. package/dist/extractors/swift.js.map +1 -1
  132. package/dist/extractors/zig.js +58 -60
  133. package/dist/extractors/zig.js.map +1 -1
  134. package/dist/features/ast.d.ts +16 -14
  135. package/dist/features/ast.d.ts.map +1 -1
  136. package/dist/features/ast.js +83 -81
  137. package/dist/features/ast.js.map +1 -1
  138. package/dist/features/audit.d.ts.map +1 -1
  139. package/dist/features/audit.js +8 -6
  140. package/dist/features/audit.js.map +1 -1
  141. package/dist/features/branch-compare.d.ts.map +1 -1
  142. package/dist/features/branch-compare.js +69 -72
  143. package/dist/features/branch-compare.js.map +1 -1
  144. package/dist/features/communities.d.ts.map +1 -1
  145. package/dist/features/communities.js +19 -7
  146. package/dist/features/communities.js.map +1 -1
  147. package/dist/features/complexity.d.ts.map +1 -1
  148. package/dist/features/complexity.js +120 -125
  149. package/dist/features/complexity.js.map +1 -1
  150. package/dist/features/dataflow.d.ts.map +1 -1
  151. package/dist/features/dataflow.js +136 -137
  152. package/dist/features/dataflow.js.map +1 -1
  153. package/dist/features/flow.d.ts.map +1 -1
  154. package/dist/features/flow.js +84 -79
  155. package/dist/features/flow.js.map +1 -1
  156. package/dist/features/structure-query.d.ts.map +1 -1
  157. package/dist/features/structure-query.js +69 -65
  158. package/dist/features/structure-query.js.map +1 -1
  159. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  160. package/dist/graph/algorithms/leiden/optimiser.js +70 -55
  161. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  162. package/dist/graph/algorithms/leiden/partition.d.ts.map +1 -1
  163. package/dist/graph/algorithms/leiden/partition.js +288 -266
  164. package/dist/graph/algorithms/leiden/partition.js.map +1 -1
  165. package/dist/graph/model.d.ts.map +1 -1
  166. package/dist/graph/model.js +5 -1
  167. package/dist/graph/model.js.map +1 -1
  168. package/dist/infrastructure/config.d.ts.map +1 -1
  169. package/dist/infrastructure/config.js +6 -4
  170. package/dist/infrastructure/config.js.map +1 -1
  171. package/dist/infrastructure/suppress.d.ts +25 -0
  172. package/dist/infrastructure/suppress.d.ts.map +1 -0
  173. package/dist/infrastructure/suppress.js +43 -0
  174. package/dist/infrastructure/suppress.js.map +1 -0
  175. package/dist/mcp/server.d.ts.map +1 -1
  176. package/dist/mcp/server.js +29 -24
  177. package/dist/mcp/server.js.map +1 -1
  178. package/dist/presentation/dataflow.d.ts.map +1 -1
  179. package/dist/presentation/dataflow.js +47 -38
  180. package/dist/presentation/dataflow.js.map +1 -1
  181. package/dist/presentation/diff-impact-mermaid.d.ts.map +1 -1
  182. package/dist/presentation/diff-impact-mermaid.js +60 -51
  183. package/dist/presentation/diff-impact-mermaid.js.map +1 -1
  184. package/dist/presentation/queries-cli/exports.d.ts.map +1 -1
  185. package/dist/presentation/queries-cli/exports.js +20 -14
  186. package/dist/presentation/queries-cli/exports.js.map +1 -1
  187. package/dist/presentation/queries-cli/impact.d.ts.map +1 -1
  188. package/dist/presentation/queries-cli/impact.js +15 -13
  189. package/dist/presentation/queries-cli/impact.js.map +1 -1
  190. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  191. package/dist/presentation/queries-cli/inspect.js +101 -79
  192. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  193. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  194. package/dist/presentation/queries-cli/overview.js +25 -16
  195. package/dist/presentation/queries-cli/overview.js.map +1 -1
  196. package/dist/presentation/queries-cli/path.js +26 -20
  197. package/dist/presentation/queries-cli/path.js.map +1 -1
  198. package/dist/presentation/result-formatter.d.ts +10 -0
  199. package/dist/presentation/result-formatter.d.ts.map +1 -1
  200. package/dist/presentation/result-formatter.js +16 -1
  201. package/dist/presentation/result-formatter.js.map +1 -1
  202. package/dist/presentation/viewer.d.ts.map +1 -1
  203. package/dist/presentation/viewer.js +18 -12
  204. package/dist/presentation/viewer.js.map +1 -1
  205. package/dist/shared/errors.d.ts +5 -0
  206. package/dist/shared/errors.d.ts.map +1 -1
  207. package/dist/shared/errors.js +5 -0
  208. package/dist/shared/errors.js.map +1 -1
  209. package/dist/shared/hierarchy.d.ts +8 -2
  210. package/dist/shared/hierarchy.d.ts.map +1 -1
  211. package/dist/shared/hierarchy.js +42 -1
  212. package/dist/shared/hierarchy.js.map +1 -1
  213. package/dist/shared/normalize.d.ts +6 -1
  214. package/dist/shared/normalize.d.ts.map +1 -1
  215. package/dist/shared/normalize.js +20 -12
  216. package/dist/shared/normalize.js.map +1 -1
  217. package/dist/shared/paginate.d.ts +0 -9
  218. package/dist/shared/paginate.d.ts.map +1 -1
  219. package/dist/shared/paginate.js +0 -15
  220. package/dist/shared/paginate.js.map +1 -1
  221. package/dist/types.d.ts +10 -4
  222. package/dist/types.d.ts.map +1 -1
  223. package/package.json +7 -7
  224. package/src/ast-analysis/engine.ts +126 -105
  225. package/src/ast-analysis/metrics.ts +33 -11
  226. package/src/ast-analysis/shared.ts +33 -24
  227. package/src/ast-analysis/visitor-utils.ts +52 -32
  228. package/src/ast-analysis/visitor.ts +132 -71
  229. package/src/ast-analysis/visitors/ast-store-visitor.ts +53 -50
  230. package/src/ast-analysis/visitors/complexity-visitor.ts +35 -40
  231. package/src/ast-analysis/visitors/dataflow-visitor.ts +87 -43
  232. package/src/cli/commands/watch.ts +16 -2
  233. package/src/db/connection.ts +29 -28
  234. package/src/db/query-builder.ts +15 -3
  235. package/src/db/repository/base.ts +20 -0
  236. package/src/db/repository/native-repository.ts +79 -1
  237. package/src/db/repository/nodes.ts +13 -8
  238. package/src/db/repository/sqlite-repository.ts +29 -0
  239. package/src/domain/analysis/brief.ts +15 -25
  240. package/src/domain/analysis/context.ts +17 -10
  241. package/src/domain/analysis/dependencies.ts +67 -76
  242. package/src/domain/analysis/fn-impact.ts +36 -43
  243. package/src/domain/analysis/implementations.ts +11 -17
  244. package/src/domain/analysis/module-map.ts +58 -92
  245. package/src/domain/analysis/query-helpers.ts +18 -1
  246. package/src/domain/graph/builder/pipeline.ts +286 -97
  247. package/src/domain/graph/builder/stages/build-edges.ts +22 -18
  248. package/src/domain/graph/builder/stages/detect-changes.ts +2 -2
  249. package/src/domain/graph/builder/stages/finalize.ts +2 -2
  250. package/src/domain/graph/builder/stages/insert-nodes.ts +59 -34
  251. package/src/domain/graph/builder/stages/resolve-imports.ts +122 -100
  252. package/src/domain/graph/cycles.ts +110 -23
  253. package/src/domain/graph/resolve.ts +1 -1
  254. package/src/domain/graph/watcher.ts +202 -96
  255. package/src/domain/parser.ts +14 -26
  256. package/src/domain/search/generator.ts +1 -1
  257. package/src/domain/search/models.ts +17 -4
  258. package/src/domain/search/search/hybrid.ts +69 -51
  259. package/src/extractors/go.ts +43 -33
  260. package/src/extractors/helpers.ts +37 -23
  261. package/src/extractors/java.ts +66 -47
  262. package/src/extractors/javascript.ts +45 -46
  263. package/src/extractors/kotlin.ts +84 -77
  264. package/src/extractors/python.ts +31 -25
  265. package/src/extractors/rust.ts +37 -29
  266. package/src/extractors/solidity.ts +57 -61
  267. package/src/extractors/swift.ts +81 -80
  268. package/src/extractors/zig.ts +58 -61
  269. package/src/features/ast.ts +130 -110
  270. package/src/features/audit.ts +8 -6
  271. package/src/features/branch-compare.ts +105 -79
  272. package/src/features/communities.ts +25 -10
  273. package/src/features/complexity.ts +171 -134
  274. package/src/features/dataflow.ts +165 -175
  275. package/src/features/flow.ts +129 -92
  276. package/src/features/structure-query.ts +79 -64
  277. package/src/graph/algorithms/leiden/optimiser.ts +99 -55
  278. package/src/graph/algorithms/leiden/partition.ts +359 -294
  279. package/src/graph/model.ts +6 -1
  280. package/src/infrastructure/config.ts +6 -4
  281. package/src/infrastructure/suppress.ts +47 -0
  282. package/src/mcp/server.ts +53 -37
  283. package/src/presentation/dataflow.ts +50 -44
  284. package/src/presentation/diff-impact-mermaid.ts +104 -62
  285. package/src/presentation/queries-cli/exports.ts +21 -13
  286. package/src/presentation/queries-cli/impact.ts +15 -13
  287. package/src/presentation/queries-cli/inspect.ts +100 -81
  288. package/src/presentation/queries-cli/overview.ts +26 -16
  289. package/src/presentation/queries-cli/path.ts +33 -25
  290. package/src/presentation/result-formatter.ts +19 -1
  291. package/src/presentation/viewer.ts +42 -14
  292. package/src/shared/errors.ts +6 -0
  293. package/src/shared/hierarchy.ts +50 -2
  294. package/src/shared/normalize.ts +31 -12
  295. package/src/shared/paginate.ts +0 -17
  296. package/src/types.ts +24 -4
@@ -17,6 +17,11 @@ export interface CodeGraphOpts {
17
17
  directed?: boolean;
18
18
  }
19
19
 
20
+ /** Canonical key for an undirected edge — smallest ID first, null-byte separated. */
21
+ function undirectedEdgeKey(a: string, b: string): string {
22
+ return a < b ? `${a}\0${b}` : `${b}\0${a}`;
23
+ }
24
+
20
25
  export class CodeGraph {
21
26
  private _directed: boolean;
22
27
  private _nodes: Map<string, NodeAttrs>;
@@ -121,7 +126,7 @@ export class CodeGraph {
121
126
  const seen = new Set<string>();
122
127
  for (const [src, targets] of this._successors) {
123
128
  for (const [tgt, attrs] of targets) {
124
- const key = src < tgt ? `${src}\0${tgt}` : `${tgt}\0${src}`;
129
+ const key = undirectedEdgeKey(src, tgt);
125
130
  if (seen.has(key)) continue;
126
131
  seen.add(key);
127
132
  yield [src, tgt, attrs];
@@ -178,7 +178,7 @@ export function loadConfig(cwd?: string): CodegraphConfig {
178
178
  _configCache.set(cwd, structuredClone(result));
179
179
  return result;
180
180
  } catch (err: unknown) {
181
- debug(`Failed to parse config ${filePath}: ${(err as Error).message}`);
181
+ debug(`Failed to parse config ${filePath}: ${toErrorMessage(err)}`);
182
182
  }
183
183
  }
184
184
  }
@@ -229,7 +229,7 @@ export function resolveSecrets(config: CodegraphConfig): CodegraphConfig {
229
229
  (config.llm as Record<string, unknown>).apiKey = result;
230
230
  }
231
231
  } catch (err: unknown) {
232
- warn(`apiKeyCommand failed: ${(err as Error).message}`);
232
+ warn(`apiKeyCommand failed: ${toErrorMessage(err)}`);
233
233
  }
234
234
  return config;
235
235
  }
@@ -252,7 +252,8 @@ function expandWorkspaceGlob(pattern: string, rootDir: string): string[] {
252
252
  .filter((e) => e.isDirectory())
253
253
  .map((e) => path.join(baseDir, e.name))
254
254
  .filter((d) => fs.existsSync(path.join(d, 'package.json')));
255
- } catch {
255
+ } catch (e) {
256
+ debug(`expandGlobDirs: failed to read ${baseDir}: ${toErrorMessage(e)}`);
256
257
  return [];
257
258
  }
258
259
  }
@@ -265,7 +266,8 @@ function readPackageName(pkgDir: string): string | null {
265
266
  const raw = fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf-8');
266
267
  const pkg = JSON.parse(raw);
267
268
  return pkg.name || null;
268
- } catch {
269
+ } catch (e) {
270
+ debug(`readPackageName: failed for ${pkgDir}: ${toErrorMessage(e)}`);
269
271
  return null;
270
272
  }
271
273
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Catch-suppression helpers for intentional error swallowing.
3
+ *
4
+ * Lives in `infrastructure/` (not `shared/`) because it depends on the
5
+ * structured logger — keeping `shared/errors.ts` dependency-free.
6
+ */
7
+
8
+ import { toErrorMessage } from '../shared/errors.js';
9
+ import { debug } from './logger.js';
10
+
11
+ /**
12
+ * Run `fn` and return its result. If it throws, log a debug message and
13
+ * return `fallback` instead. Use this for intentional catch suppression
14
+ * where the error is expected and non-fatal (e.g. optional file reads,
15
+ * graceful feature probes, cleanup that may fail).
16
+ *
17
+ * @example
18
+ * const version = suppressError(() => readPkgVersion(), 'read package version', '');
19
+ */
20
+ export function suppressError<T>(fn: () => T, context: string, fallback: T): T {
21
+ try {
22
+ return fn();
23
+ } catch (e: unknown) {
24
+ debug(`${context}: ${toErrorMessage(e)}`);
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Async variant of {@link suppressError}. Awaits `fn()` and returns `fallback`
31
+ * on rejection, logging the error via `debug()`.
32
+ *
33
+ * @example
34
+ * const data = await suppressErrorAsync(() => fetchOptionalData(), 'fetch data', null);
35
+ */
36
+ export async function suppressErrorAsync<T>(
37
+ fn: () => Promise<T>,
38
+ context: string,
39
+ fallback: T,
40
+ ): Promise<T> {
41
+ try {
42
+ return await fn();
43
+ } catch (e: unknown) {
44
+ debug(`${context}: ${toErrorMessage(e)}`);
45
+ return fallback;
46
+ }
47
+ }
package/src/mcp/server.ts CHANGED
@@ -13,7 +13,8 @@ const { version: PKG_VERSION } = require('../../package.json') as { version: str
13
13
  import { getDatabase } from '../db/better-sqlite3.js';
14
14
  import { findDbPath } from '../db/index.js';
15
15
  import { loadConfig } from '../infrastructure/config.js';
16
- import { CodegraphError, ConfigError } from '../shared/errors.js';
16
+ import { debug } from '../infrastructure/logger.js';
17
+ import { CodegraphError, ConfigError, toErrorMessage } from '../shared/errors.js';
17
18
  import { MCP_MAX_LIMIT } from '../shared/paginate.js';
18
19
  import type { CodegraphConfig, MCPServerOptions } from '../types.js';
19
20
  import { initMcpDefaults } from './middleware.js';
@@ -56,7 +57,8 @@ async function loadMCPSdk(): Promise<{
56
57
  ListToolsRequestSchema: types.ListToolsRequestSchema,
57
58
  CallToolRequestSchema: types.CallToolRequestSchema,
58
59
  };
59
- } catch {
60
+ } catch (e) {
61
+ debug(`MCP SDK import failed: ${toErrorMessage(e)}`);
60
62
  throw new ConfigError(
61
63
  'MCP server requires @modelcontextprotocol/sdk.\nInstall it with: npm install @modelcontextprotocol/sdk',
62
64
  );
@@ -123,8 +125,10 @@ function registerShutdownHandlers(): void {
123
125
  const shutdown = async () => {
124
126
  try {
125
127
  await _activeServer?.close();
126
- } catch (_shutdownErr: unknown) {
127
- // Ignore close errors during shutdown — the transport may already be gone.
128
+ } catch (shutdownErr: unknown) {
129
+ debug(
130
+ `MCP shutdown close failed (transport may already be gone): ${toErrorMessage(shutdownErr)}`,
131
+ );
128
132
  }
129
133
  process.exit(0);
130
134
  };
@@ -154,36 +158,13 @@ function registerShutdownHandlers(): void {
154
158
  process.on('unhandledRejection', silentReject);
155
159
  }
156
160
 
157
- export async function startMCPServer(
158
- customDbPath?: string,
159
- options: MCPServerOptionsInternal = {},
160
- ): Promise<void> {
161
- const { allowedRepos } = options;
162
- const multiRepo = options.multiRepo || !!allowedRepos;
163
-
164
- // Apply config-based MCP page-size overrides
165
- const config = options.config || loadConfig();
166
- initMcpDefaults(config.mcp?.defaults ? { ...config.mcp.defaults } : undefined);
167
-
168
- const { Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema } =
169
- await loadMCPSdk();
170
-
171
- // Connect transport FIRST so the server can receive the client's
172
- // `initialize` request while heavy modules (queries, better-sqlite3)
173
- // are still loading. These are lazy-loaded on the first tool call
174
- // and cached for subsequent calls.
175
- const { getQueries } = createLazyLoaders();
176
-
177
- const server = new (Server as any)(
178
- { name: 'codegraph', version: PKG_VERSION },
179
- { capabilities: { tools: {} } },
180
- );
181
-
182
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
183
- tools: buildToolList(multiRepo),
184
- }));
185
-
186
- server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
161
+ function createCallToolHandler(
162
+ multiRepo: boolean,
163
+ customDbPath: string | undefined,
164
+ allowedRepos: string[] | undefined,
165
+ getQueries: () => Promise<unknown>,
166
+ ) {
167
+ return async (request: any) => {
187
168
  const { name, arguments: args } = request.params;
188
169
  try {
189
170
  validateMultiRepoAccess(multiRepo, name, args);
@@ -212,10 +193,45 @@ export async function startMCPServer(
212
193
  const text =
213
194
  err instanceof CodegraphError
214
195
  ? `[${code}] ${err.message}`
215
- : `Error: ${(err as Error).message}`;
196
+ : `Error: ${toErrorMessage(err)}`;
216
197
  return { content: [{ type: 'text', text }], isError: true };
217
198
  }
218
- });
199
+ };
200
+ }
201
+
202
+ export async function startMCPServer(
203
+ customDbPath?: string,
204
+ options: MCPServerOptionsInternal = {},
205
+ ): Promise<void> {
206
+ const { allowedRepos } = options;
207
+ const multiRepo = options.multiRepo || !!allowedRepos;
208
+
209
+ // Apply config-based MCP page-size overrides
210
+ const config = options.config || loadConfig();
211
+ initMcpDefaults(config.mcp?.defaults ? { ...config.mcp.defaults } : undefined);
212
+
213
+ const { Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema } =
214
+ await loadMCPSdk();
215
+
216
+ // Connect transport FIRST so the server can receive the client's
217
+ // `initialize` request while heavy modules (queries, better-sqlite3)
218
+ // are still loading. These are lazy-loaded on the first tool call
219
+ // and cached for subsequent calls.
220
+ const { getQueries } = createLazyLoaders();
221
+
222
+ const server = new (Server as any)(
223
+ { name: 'codegraph', version: PKG_VERSION },
224
+ { capabilities: { tools: {} } },
225
+ );
226
+
227
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
228
+ tools: buildToolList(multiRepo),
229
+ }));
230
+
231
+ server.setRequestHandler(
232
+ CallToolRequestSchema,
233
+ createCallToolHandler(multiRepo, customDbPath, allowedRepos, getQueries),
234
+ );
219
235
 
220
236
  const transport = new (StdioServerTransport as any)();
221
237
 
@@ -235,7 +251,7 @@ export async function startMCPServer(
235
251
  process.exit(0);
236
252
  }
237
253
  process.stderr.write(
238
- `MCP transport connect failed: ${(err as Error).stack ?? (err as Error).message}\n`,
254
+ `MCP transport connect failed: ${err instanceof Error ? (err.stack ?? err.message) : toErrorMessage(err)}\n`,
239
255
  );
240
256
  process.exit(1);
241
257
  }
@@ -58,6 +58,53 @@ interface DataflowImpactEntry {
58
58
  levels: Record<string, Array<{ name: string; file: string; line: number }>>;
59
59
  }
60
60
 
61
+ function printDataflowFlows(r: DataflowResultEntry): void {
62
+ if (r.flowsTo.length > 0) {
63
+ console.log('\n Data flows TO:');
64
+ for (const f of r.flowsTo) {
65
+ const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
66
+ console.log(` \u2192 ${f.target} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
67
+ }
68
+ }
69
+ if (r.flowsFrom.length > 0) {
70
+ console.log('\n Data flows FROM:');
71
+ for (const f of r.flowsFrom) {
72
+ const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
73
+ console.log(` \u2190 ${f.source} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
74
+ }
75
+ }
76
+ }
77
+
78
+ function printDataflowReturns(r: DataflowResultEntry): void {
79
+ if (r.returns.length > 0) {
80
+ console.log('\n Return value consumed by:');
81
+ for (const c of r.returns) {
82
+ console.log(` \u2192 ${c.consumer} (${c.file}:${c.line}) ${c.expression}`);
83
+ }
84
+ }
85
+ if (r.returnedBy.length > 0) {
86
+ console.log('\n Uses return value of:');
87
+ for (const p of r.returnedBy) {
88
+ console.log(` \u2190 ${p.producer} (${p.file}:${p.line}) ${p.expression}`);
89
+ }
90
+ }
91
+ }
92
+
93
+ function printDataflowMutations(r: DataflowResultEntry): void {
94
+ if (r.mutates.length > 0) {
95
+ console.log('\n Mutates:');
96
+ for (const m of r.mutates) {
97
+ console.log(` \u270E ${m.expression} (line ${m.line})`);
98
+ }
99
+ }
100
+ if (r.mutatedBy.length > 0) {
101
+ console.log('\n Mutated by:');
102
+ for (const m of r.mutatedBy) {
103
+ console.log(` \u270E ${m.source} \u2014 ${m.expression} (line ${m.line})`);
104
+ }
105
+ }
106
+ }
107
+
61
108
  export function dataflow(
62
109
  name: string,
63
110
  customDbPath: string | undefined,
@@ -87,50 +134,9 @@ export function dataflow(
87
134
  for (const r of data.results) {
88
135
  console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
89
136
  console.log('\u2500'.repeat(60));
90
-
91
- if (r.flowsTo.length > 0) {
92
- console.log('\n Data flows TO:');
93
- for (const f of r.flowsTo) {
94
- const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
95
- console.log(` \u2192 ${f.target} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
96
- }
97
- }
98
-
99
- if (r.flowsFrom.length > 0) {
100
- console.log('\n Data flows FROM:');
101
- for (const f of r.flowsFrom) {
102
- const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
103
- console.log(` \u2190 ${f.source} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
104
- }
105
- }
106
-
107
- if (r.returns.length > 0) {
108
- console.log('\n Return value consumed by:');
109
- for (const c of r.returns) {
110
- console.log(` \u2192 ${c.consumer} (${c.file}:${c.line}) ${c.expression}`);
111
- }
112
- }
113
-
114
- if (r.returnedBy.length > 0) {
115
- console.log('\n Uses return value of:');
116
- for (const p of r.returnedBy) {
117
- console.log(` \u2190 ${p.producer} (${p.file}:${p.line}) ${p.expression}`);
118
- }
119
- }
120
-
121
- if (r.mutates.length > 0) {
122
- console.log('\n Mutates:');
123
- for (const m of r.mutates) {
124
- console.log(` \u270E ${m.expression} (line ${m.line})`);
125
- }
126
- }
127
-
128
- if (r.mutatedBy.length > 0) {
129
- console.log('\n Mutated by:');
130
- for (const m of r.mutatedBy) {
131
- console.log(` \u270E ${m.source} \u2014 ${m.expression} (line ${m.line})`);
132
- }
133
- }
137
+ printDataflowFlows(r);
138
+ printDataflowReturns(r);
139
+ printDataflowMutations(r);
134
140
  }
135
141
  }
136
142
 
@@ -1,56 +1,48 @@
1
1
  import { diffImpactData } from '../domain/analysis/diff-impact.js';
2
2
 
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
+ }
21
8
 
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
+ }
24
15
 
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);
35
24
  }
25
+ return reg.nodeIdMap.get(key)!;
26
+ }
36
27
 
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);
40
31
  for (const callers of Object.values(fn.levels || {})) {
41
32
  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);
43
34
  }
44
35
  }
45
36
  }
37
+ }
46
38
 
47
- // Collect all edges and determine blast radius
39
+ function collectEdges(affectedFunctions: any[]): ImpactEdgeSets {
48
40
  const allEdges = new Set<string>();
49
41
  const edgeFromNodes = new Set<string>();
50
42
  const edgeToNodes = new Set<string>();
51
43
  const changedKeys = new Set<string>();
52
44
 
53
- for (const fn of data.affectedFunctions) {
45
+ for (const fn of affectedFunctions) {
54
46
  changedKeys.add(`${fn.file}::${fn.name}:${fn.line}`);
55
47
  for (const edge of fn.edges || []) {
56
48
  const edgeKey = `${edge.from}|${edge.to}`;
@@ -62,30 +54,42 @@ export function diffImpactMermaid(
62
54
  }
63
55
  }
64
56
 
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
+ } {
66
64
  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)) {
69
67
  blastRadiusKeys.add(key);
70
68
  }
71
69
  }
72
70
 
73
- // Intermediate callers: not changed, not blast radius
74
71
  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)) {
77
74
  intermediateKeys.add(key);
78
75
  }
79
76
  }
80
77
 
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) {
84
89
  if (!fileGroups.has(fn.file)) fileGroups.set(fn.file, []);
85
90
  fileGroups.get(fn.file)!.push(fn);
86
91
  }
87
92
 
88
- // Emit changed-file subgraphs
89
93
  let sgCounter = 0;
90
94
  for (const [file, fns] of fileGroups) {
91
95
  const isNew = newFileSet.has(file);
@@ -94,33 +98,71 @@ export function diffImpactMermaid(
94
98
  lines.push(` subgraph ${sgId}["${file} **(${tag})**"]`);
95
99
  for (const fn of fns) {
96
100
  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}"]`);
98
102
  }
99
103
  lines.push(' end');
100
104
  const style = isNew ? 'fill:#e8f5e9,stroke:#4caf50' : 'fill:#fff3e0,stroke:#ff9800';
101
105
  lines.push(` style ${sgId} ${style}`);
102
106
  }
103
107
 
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)}"]`);
107
122
  }
123
+ lines.push(' end');
124
+ lines.push(` style ${sgId} fill:#f3e5f5,stroke:#9c27b0`);
125
+ }
108
126
 
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"]';
118
144
  }
119
145
 
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) {
122
164
  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)}`);
124
166
  }
125
167
 
126
168
  return lines.join('\n');
@@ -71,28 +71,36 @@ function printExportSymbols(results: ExportSymbol[]): void {
71
71
  }
72
72
  }
73
73
 
74
- function printReexportedSymbols(reexportedSymbols: ReexportedSymbol[]): void {
75
- // Group by origin file
74
+ function groupByOriginFile(reexportedSymbols: ReexportedSymbol[]): Map<string, ReexportedSymbol[]> {
76
75
  const byOrigin = new Map<string, ReexportedSymbol[]>();
77
76
  for (const sym of reexportedSymbols) {
78
77
  if (!byOrigin.has(sym.originFile)) byOrigin.set(sym.originFile, []);
79
78
  byOrigin.get(sym.originFile)!.push(sym);
80
79
  }
80
+ return byOrigin;
81
+ }
82
+
83
+ function printReexportSymbol(sym: ExportSymbol, indent: string): void {
84
+ const icon = kindIcon(sym.kind);
85
+ const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
86
+ const role = sym.role ? ` [${sym.role}]` : '';
87
+ console.log(`${indent}${icon} ${sym.name}${sig}${role} :${sym.line}`);
88
+ if (sym.consumers.length === 0) {
89
+ console.log(`${indent} (no consumers)`);
90
+ } else {
91
+ for (const c of sym.consumers) {
92
+ console.log(`${indent} <- ${c.name} (${c.file}:${c.line})`);
93
+ }
94
+ }
95
+ }
96
+
97
+ function printReexportedSymbols(reexportedSymbols: ReexportedSymbol[]): void {
98
+ const byOrigin = groupByOriginFile(reexportedSymbols);
81
99
 
82
100
  for (const [originFile, syms] of byOrigin) {
83
101
  console.log(`\n from ${originFile}:`);
84
102
  for (const sym of syms) {
85
- const icon = kindIcon(sym.kind);
86
- const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
87
- const role = sym.role ? ` [${sym.role}]` : '';
88
- console.log(` ${icon} ${sym.name}${sig}${role} :${sym.line}`);
89
- if (sym.consumers.length === 0) {
90
- console.log(' (no consumers)');
91
- } else {
92
- for (const c of sym.consumers) {
93
- console.log(` <- ${c.name} (${c.file}:${c.line})`);
94
- }
95
- }
103
+ printReexportSymbol(sym, ' ');
96
104
  }
97
105
  }
98
106
  }
@@ -228,6 +228,20 @@ export function impactAnalysis(file: string, customDbPath: string, opts: OutputO
228
228
  console.log(`\n Total: ${data.totalDependents} files transitively depend on "${file}"\n`);
229
229
  }
230
230
 
231
+ function printFnImpactLevels(levels: Record<string, SymbolRef[]>): void {
232
+ if (Object.keys(levels).length === 0) {
233
+ console.log(` No callers found.`);
234
+ return;
235
+ }
236
+ for (const [level, fns] of Object.entries(levels).sort((a, b) => Number(a[0]) - Number(b[0]))) {
237
+ const l = parseInt(level, 10);
238
+ console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`);
239
+ for (const f of fns.slice(0, 20))
240
+ console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
241
+ if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
242
+ }
243
+ }
244
+
231
245
  export function fnImpact(name: string, customDbPath: string, opts: OutputOpts = {}): void {
232
246
  const data = fnImpactData(name, customDbPath, opts) as unknown as FnImpactData;
233
247
  if (outputResult(data as unknown as Record<string, unknown>, 'results', opts)) return;
@@ -239,19 +253,7 @@ export function fnImpact(name: string, customDbPath: string, opts: OutputOpts =
239
253
 
240
254
  for (const r of data.results) {
241
255
  console.log(`\nFunction impact: ${kindIcon(r.kind)} ${r.name} -- ${r.file}:${r.line}\n`);
242
- if (Object.keys(r.levels).length === 0) {
243
- console.log(` No callers found.`);
244
- } else {
245
- for (const [level, fns] of Object.entries(r.levels).sort(
246
- (a, b) => Number(a[0]) - Number(b[0]),
247
- )) {
248
- const l = parseInt(level, 10);
249
- console.log(` ${'--'.repeat(l)} Level ${level} (${fns.length} functions):`);
250
- for (const f of fns.slice(0, 20))
251
- console.log(` ${' '.repeat(l)}^ ${kindIcon(f.kind)} ${f.name} ${f.file}:${f.line}`);
252
- if (fns.length > 20) console.log(` ... and ${fns.length - 20} more`);
253
- }
254
- }
256
+ printFnImpactLevels(r.levels);
255
257
  console.log(`\n Total: ${r.totalDependents} functions transitively depend on ${r.name}\n`);
256
258
  }
257
259
  }