@optave/codegraph 3.1.3 → 3.1.5

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 (232) hide show
  1. package/README.md +38 -84
  2. package/package.json +13 -8
  3. package/src/ast-analysis/engine.js +32 -12
  4. package/src/ast-analysis/shared.js +6 -5
  5. package/src/cli/commands/ast.js +22 -0
  6. package/src/cli/commands/audit.js +45 -0
  7. package/src/cli/commands/batch.js +68 -0
  8. package/src/cli/commands/branch-compare.js +21 -0
  9. package/src/cli/commands/build.js +26 -0
  10. package/src/cli/commands/cfg.js +26 -0
  11. package/src/cli/commands/check.js +74 -0
  12. package/src/cli/commands/children.js +28 -0
  13. package/src/cli/commands/co-change.js +67 -0
  14. package/src/cli/commands/communities.js +19 -0
  15. package/src/cli/commands/complexity.js +46 -0
  16. package/src/cli/commands/context.js +30 -0
  17. package/src/cli/commands/cycles.js +32 -0
  18. package/src/cli/commands/dataflow.js +28 -0
  19. package/src/cli/commands/deps.js +12 -0
  20. package/src/cli/commands/diff-impact.js +26 -0
  21. package/src/cli/commands/embed.js +30 -0
  22. package/src/cli/commands/export.js +78 -0
  23. package/src/cli/commands/exports.js +14 -0
  24. package/src/cli/commands/flow.js +32 -0
  25. package/src/cli/commands/fn-impact.js +26 -0
  26. package/src/cli/commands/impact.js +12 -0
  27. package/src/cli/commands/info.js +76 -0
  28. package/src/cli/commands/map.js +19 -0
  29. package/src/cli/commands/mcp.js +18 -0
  30. package/src/cli/commands/models.js +19 -0
  31. package/src/cli/commands/owners.js +25 -0
  32. package/src/cli/commands/path.js +36 -0
  33. package/src/cli/commands/plot.js +89 -0
  34. package/src/cli/commands/query.js +45 -0
  35. package/src/cli/commands/registry.js +100 -0
  36. package/src/cli/commands/roles.js +30 -0
  37. package/src/cli/commands/search.js +42 -0
  38. package/src/cli/commands/sequence.js +28 -0
  39. package/src/cli/commands/snapshot.js +66 -0
  40. package/src/cli/commands/stats.js +15 -0
  41. package/src/cli/commands/structure.js +33 -0
  42. package/src/cli/commands/triage.js +78 -0
  43. package/src/cli/commands/watch.js +12 -0
  44. package/src/cli/commands/where.js +20 -0
  45. package/src/cli/index.js +124 -0
  46. package/src/cli/shared/open-graph.js +13 -0
  47. package/src/cli/shared/options.js +59 -0
  48. package/src/cli/shared/output.js +1 -0
  49. package/src/cli.js +11 -1522
  50. package/src/db/connection.js +130 -7
  51. package/src/{db.js → db/index.js} +17 -5
  52. package/src/db/migrations.js +42 -1
  53. package/src/db/query-builder.js +20 -12
  54. package/src/db/repository/base.js +201 -0
  55. package/src/db/repository/graph-read.js +7 -4
  56. package/src/db/repository/in-memory-repository.js +575 -0
  57. package/src/db/repository/index.js +5 -1
  58. package/src/db/repository/nodes.js +60 -6
  59. package/src/db/repository/sqlite-repository.js +219 -0
  60. package/src/domain/analysis/context.js +408 -0
  61. package/src/domain/analysis/dependencies.js +341 -0
  62. package/src/domain/analysis/exports.js +134 -0
  63. package/src/domain/analysis/impact.js +466 -0
  64. package/src/domain/analysis/module-map.js +322 -0
  65. package/src/domain/analysis/roles.js +45 -0
  66. package/src/domain/analysis/symbol-lookup.js +238 -0
  67. package/src/domain/graph/builder/context.js +85 -0
  68. package/src/domain/graph/builder/helpers.js +218 -0
  69. package/src/domain/graph/builder/incremental.js +178 -0
  70. package/src/domain/graph/builder/pipeline.js +130 -0
  71. package/src/domain/graph/builder/stages/build-edges.js +297 -0
  72. package/src/domain/graph/builder/stages/build-structure.js +113 -0
  73. package/src/domain/graph/builder/stages/collect-files.js +44 -0
  74. package/src/domain/graph/builder/stages/detect-changes.js +413 -0
  75. package/src/domain/graph/builder/stages/finalize.js +139 -0
  76. package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
  77. package/src/domain/graph/builder/stages/parse-files.js +28 -0
  78. package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
  79. package/src/domain/graph/builder/stages/run-analyses.js +44 -0
  80. package/src/domain/graph/builder.js +11 -0
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/domain/graph/cycles.js +82 -0
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +3 -3
  85. package/src/{watcher.js → domain/graph/watcher.js} +10 -150
  86. package/src/{parser.js → domain/parser.js} +5 -5
  87. package/src/domain/queries.js +48 -0
  88. package/src/domain/search/generator.js +163 -0
  89. package/src/domain/search/index.js +13 -0
  90. package/src/domain/search/models.js +218 -0
  91. package/src/domain/search/search/cli-formatter.js +151 -0
  92. package/src/domain/search/search/filters.js +46 -0
  93. package/src/domain/search/search/hybrid.js +121 -0
  94. package/src/domain/search/search/keyword.js +68 -0
  95. package/src/domain/search/search/prepare.js +66 -0
  96. package/src/domain/search/search/semantic.js +145 -0
  97. package/src/domain/search/stores/fts5.js +27 -0
  98. package/src/domain/search/stores/sqlite-blob.js +24 -0
  99. package/src/domain/search/strategies/source.js +14 -0
  100. package/src/domain/search/strategies/structured.js +43 -0
  101. package/src/domain/search/strategies/text-utils.js +43 -0
  102. package/src/extractors/csharp.js +10 -2
  103. package/src/extractors/go.js +3 -1
  104. package/src/extractors/helpers.js +71 -0
  105. package/src/extractors/java.js +9 -2
  106. package/src/extractors/javascript.js +39 -2
  107. package/src/extractors/php.js +3 -1
  108. package/src/extractors/python.js +14 -3
  109. package/src/extractors/rust.js +3 -1
  110. package/src/{ast.js → features/ast.js} +8 -8
  111. package/src/{audit.js → features/audit.js} +16 -44
  112. package/src/{batch.js → features/batch.js} +6 -5
  113. package/src/{boundaries.js → features/boundaries.js} +2 -2
  114. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  115. package/src/{cfg.js → features/cfg.js} +11 -12
  116. package/src/{check.js → features/check.js} +13 -30
  117. package/src/{cochange.js → features/cochange.js} +5 -5
  118. package/src/{communities.js → features/communities.js} +18 -90
  119. package/src/{complexity.js → features/complexity.js} +13 -13
  120. package/src/{dataflow.js → features/dataflow.js} +12 -13
  121. package/src/features/export.js +378 -0
  122. package/src/{flow.js → features/flow.js} +4 -4
  123. package/src/features/graph-enrichment.js +327 -0
  124. package/src/{manifesto.js → features/manifesto.js} +6 -6
  125. package/src/{owners.js → features/owners.js} +2 -2
  126. package/src/{sequence.js → features/sequence.js} +16 -52
  127. package/src/{snapshot.js → features/snapshot.js} +8 -7
  128. package/src/{structure.js → features/structure.js} +20 -45
  129. package/src/{triage.js → features/triage.js} +27 -79
  130. package/src/graph/algorithms/bfs.js +49 -0
  131. package/src/graph/algorithms/centrality.js +16 -0
  132. package/src/graph/algorithms/index.js +5 -0
  133. package/src/graph/algorithms/louvain.js +26 -0
  134. package/src/graph/algorithms/shortest-path.js +41 -0
  135. package/src/graph/algorithms/tarjan.js +49 -0
  136. package/src/graph/builders/dependency.js +110 -0
  137. package/src/graph/builders/index.js +3 -0
  138. package/src/graph/builders/structure.js +40 -0
  139. package/src/graph/builders/temporal.js +33 -0
  140. package/src/graph/classifiers/index.js +2 -0
  141. package/src/graph/classifiers/risk.js +85 -0
  142. package/src/graph/classifiers/roles.js +64 -0
  143. package/src/graph/index.js +13 -0
  144. package/src/graph/model.js +230 -0
  145. package/src/index.cjs +16 -0
  146. package/src/index.js +42 -219
  147. package/src/{native.js → infrastructure/native.js} +3 -1
  148. package/src/infrastructure/result-formatter.js +2 -21
  149. package/src/mcp/index.js +2 -0
  150. package/src/mcp/middleware.js +26 -0
  151. package/src/mcp/server.js +128 -0
  152. package/src/{mcp.js → mcp/tool-registry.js} +6 -675
  153. package/src/mcp/tools/ast-query.js +14 -0
  154. package/src/mcp/tools/audit.js +21 -0
  155. package/src/mcp/tools/batch-query.js +11 -0
  156. package/src/mcp/tools/branch-compare.js +12 -0
  157. package/src/mcp/tools/cfg.js +21 -0
  158. package/src/mcp/tools/check.js +43 -0
  159. package/src/mcp/tools/co-changes.js +20 -0
  160. package/src/mcp/tools/code-owners.js +12 -0
  161. package/src/mcp/tools/communities.js +15 -0
  162. package/src/mcp/tools/complexity.js +18 -0
  163. package/src/mcp/tools/context.js +17 -0
  164. package/src/mcp/tools/dataflow.js +26 -0
  165. package/src/mcp/tools/diff-impact.js +24 -0
  166. package/src/mcp/tools/execution-flow.js +26 -0
  167. package/src/mcp/tools/export-graph.js +57 -0
  168. package/src/mcp/tools/file-deps.js +12 -0
  169. package/src/mcp/tools/file-exports.js +13 -0
  170. package/src/mcp/tools/find-cycles.js +15 -0
  171. package/src/mcp/tools/fn-impact.js +15 -0
  172. package/src/mcp/tools/impact-analysis.js +12 -0
  173. package/src/mcp/tools/index.js +71 -0
  174. package/src/mcp/tools/list-functions.js +14 -0
  175. package/src/mcp/tools/list-repos.js +11 -0
  176. package/src/mcp/tools/module-map.js +6 -0
  177. package/src/mcp/tools/node-roles.js +14 -0
  178. package/src/mcp/tools/path.js +12 -0
  179. package/src/mcp/tools/query.js +30 -0
  180. package/src/mcp/tools/semantic-search.js +65 -0
  181. package/src/mcp/tools/sequence.js +17 -0
  182. package/src/mcp/tools/structure.js +15 -0
  183. package/src/mcp/tools/symbol-children.js +14 -0
  184. package/src/mcp/tools/triage.js +35 -0
  185. package/src/mcp/tools/where.js +13 -0
  186. package/src/{commands → presentation}/audit.js +2 -2
  187. package/src/{commands → presentation}/batch.js +1 -1
  188. package/src/{commands → presentation}/branch-compare.js +2 -2
  189. package/src/{commands → presentation}/cfg.js +1 -1
  190. package/src/{commands → presentation}/check.js +6 -6
  191. package/src/presentation/colors.js +44 -0
  192. package/src/{commands → presentation}/communities.js +1 -1
  193. package/src/{commands → presentation}/complexity.js +1 -1
  194. package/src/{commands → presentation}/dataflow.js +1 -1
  195. package/src/presentation/export.js +444 -0
  196. package/src/{commands → presentation}/flow.js +2 -2
  197. package/src/{commands → presentation}/manifesto.js +4 -4
  198. package/src/{commands → presentation}/owners.js +1 -1
  199. package/src/presentation/queries-cli/exports.js +46 -0
  200. package/src/presentation/queries-cli/impact.js +198 -0
  201. package/src/presentation/queries-cli/index.js +5 -0
  202. package/src/presentation/queries-cli/inspect.js +334 -0
  203. package/src/presentation/queries-cli/overview.js +197 -0
  204. package/src/presentation/queries-cli/path.js +58 -0
  205. package/src/presentation/queries-cli.js +27 -0
  206. package/src/{commands → presentation}/query.js +1 -1
  207. package/src/presentation/result-formatter.js +144 -0
  208. package/src/presentation/sequence-renderer.js +43 -0
  209. package/src/{commands → presentation}/sequence.js +2 -2
  210. package/src/{commands → presentation}/structure.js +2 -2
  211. package/src/presentation/table.js +47 -0
  212. package/src/{commands → presentation}/triage.js +1 -1
  213. package/src/{viewer.js → presentation/viewer.js} +68 -382
  214. package/src/{constants.js → shared/constants.js} +1 -1
  215. package/src/shared/errors.js +78 -0
  216. package/src/shared/file-utils.js +153 -0
  217. package/src/shared/generators.js +125 -0
  218. package/src/shared/hierarchy.js +27 -0
  219. package/src/shared/normalize.js +59 -0
  220. package/src/builder.js +0 -1486
  221. package/src/cycles.js +0 -137
  222. package/src/embedder.js +0 -1097
  223. package/src/export.js +0 -681
  224. package/src/queries-cli.js +0 -866
  225. package/src/queries.js +0 -2289
  226. /package/src/{config.js → infrastructure/config.js} +0 -0
  227. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  228. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  229. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  230. /package/src/{commands → presentation}/cochange.js +0 -0
  231. /package/src/{kinds.js → shared/kinds.js} +0 -0
  232. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,17 @@
1
+ import { effectiveOffset, MCP_DEFAULTS } from '../middleware.js';
2
+
3
+ export const name = 'sequence';
4
+
5
+ export async function handler(args, ctx) {
6
+ const { sequenceData, sequenceToMermaid } = await import('../../features/sequence.js');
7
+ const seqResult = sequenceData(args.name, ctx.dbPath, {
8
+ depth: args.depth,
9
+ file: args.file,
10
+ kind: args.kind,
11
+ dataflow: args.dataflow,
12
+ noTests: args.no_tests,
13
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, ctx.MCP_MAX_LIMIT),
14
+ offset: effectiveOffset(args),
15
+ });
16
+ return args.format === 'json' ? seqResult : { text: sequenceToMermaid(seqResult), ...seqResult };
17
+ }
@@ -0,0 +1,15 @@
1
+ import { effectiveLimit, effectiveOffset } from '../middleware.js';
2
+
3
+ export const name = 'structure';
4
+
5
+ export async function handler(args, ctx) {
6
+ const { structureData } = await import('../../features/structure.js');
7
+ return structureData(ctx.dbPath, {
8
+ directory: args.directory,
9
+ depth: args.depth,
10
+ sort: args.sort,
11
+ full: args.full,
12
+ limit: effectiveLimit(args, name),
13
+ offset: effectiveOffset(args),
14
+ });
15
+ }
@@ -0,0 +1,14 @@
1
+ import { effectiveOffset, MCP_DEFAULTS } from '../middleware.js';
2
+
3
+ export const name = 'symbol_children';
4
+
5
+ export async function handler(args, ctx) {
6
+ const { childrenData } = await ctx.getQueries();
7
+ return childrenData(args.name, ctx.dbPath, {
8
+ file: args.file,
9
+ kind: args.kind,
10
+ noTests: args.no_tests,
11
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.context, ctx.MCP_MAX_LIMIT),
12
+ offset: effectiveOffset(args),
13
+ });
14
+ }
@@ -0,0 +1,35 @@
1
+ import { effectiveLimit, effectiveOffset, MCP_DEFAULTS, MCP_MAX_LIMIT } from '../middleware.js';
2
+
3
+ export const name = 'triage';
4
+
5
+ export async function handler(args, ctx) {
6
+ if (args.level === 'file' || args.level === 'directory') {
7
+ const { hotspotsData } = await import('../../features/structure.js');
8
+ const TRIAGE_TO_HOTSPOT = {
9
+ risk: 'fan-in',
10
+ complexity: 'density',
11
+ churn: 'coupling',
12
+ mi: 'fan-in',
13
+ };
14
+ const metric = TRIAGE_TO_HOTSPOT[args.sort] ?? args.sort;
15
+ return hotspotsData(ctx.dbPath, {
16
+ metric,
17
+ level: args.level,
18
+ limit: Math.min(args.limit ?? MCP_DEFAULTS.hotspots, MCP_MAX_LIMIT),
19
+ offset: effectiveOffset(args),
20
+ noTests: args.no_tests,
21
+ });
22
+ }
23
+ const { triageData } = await import('../../features/triage.js');
24
+ return triageData(ctx.dbPath, {
25
+ sort: args.sort,
26
+ minScore: args.min_score,
27
+ role: args.role,
28
+ file: args.file,
29
+ kind: args.kind,
30
+ noTests: args.no_tests,
31
+ weights: args.weights,
32
+ limit: effectiveLimit(args, name),
33
+ offset: effectiveOffset(args),
34
+ });
35
+ }
@@ -0,0 +1,13 @@
1
+ import { effectiveLimit, effectiveOffset } from '../middleware.js';
2
+
3
+ export const name = 'where';
4
+
5
+ export async function handler(args, ctx) {
6
+ const { whereData } = await ctx.getQueries();
7
+ return whereData(args.target, ctx.dbPath, {
8
+ file: args.file_mode,
9
+ noTests: args.no_tests,
10
+ limit: effectiveLimit(args, name),
11
+ offset: effectiveOffset(args),
12
+ });
13
+ }
@@ -1,6 +1,6 @@
1
- import { auditData } from '../audit.js';
1
+ import { kindIcon } from '../domain/queries.js';
2
+ import { auditData } from '../features/audit.js';
2
3
  import { outputResult } from '../infrastructure/result-formatter.js';
3
- import { kindIcon } from '../queries.js';
4
4
 
5
5
  /**
6
6
  * CLI formatter for the audit command.
@@ -1,4 +1,4 @@
1
- import { batchData, multiBatchData } from '../batch.js';
1
+ import { batchData, multiBatchData } from '../features/batch.js';
2
2
 
3
3
  /**
4
4
  * CLI wrapper — calls batchData and prints JSON to stdout.
@@ -1,6 +1,6 @@
1
- import { branchCompareData, branchCompareMermaid } from '../branch-compare.js';
1
+ import { kindIcon } from '../domain/queries.js';
2
+ import { branchCompareData, branchCompareMermaid } from '../features/branch-compare.js';
2
3
  import { outputResult } from '../infrastructure/result-formatter.js';
3
- import { kindIcon } from '../queries.js';
4
4
 
5
5
  // ─── Text Formatting ────────────────────────────────────────────────────
6
6
 
@@ -1,4 +1,4 @@
1
- import { cfgData, cfgToDOT, cfgToMermaid } from '../cfg.js';
1
+ import { cfgData, cfgToDOT, cfgToMermaid } from '../features/cfg.js';
2
2
  import { outputResult } from '../infrastructure/result-formatter.js';
3
3
 
4
4
  /**
@@ -1,19 +1,19 @@
1
- import { checkData } from '../check.js';
1
+ import { checkData } from '../features/check.js';
2
2
  import { outputResult } from '../infrastructure/result-formatter.js';
3
+ import { AnalysisError } from '../shared/errors.js';
3
4
 
4
5
  /**
5
- * CLI formatter — prints check results and exits with code 1 on failure.
6
+ * CLI formatter — prints check results and sets exitCode 1 on failure.
6
7
  */
7
8
  export function check(customDbPath, opts = {}) {
8
9
  const data = checkData(customDbPath, opts);
9
10
 
10
11
  if (data.error) {
11
- console.error(data.error);
12
- process.exit(1);
12
+ throw new AnalysisError(data.error);
13
13
  }
14
14
 
15
15
  if (outputResult(data, null, opts)) {
16
- if (!data.passed) process.exit(1);
16
+ if (!data.passed) process.exitCode = 1;
17
17
  return;
18
18
  }
19
19
 
@@ -77,6 +77,6 @@ export function check(customDbPath, opts = {}) {
77
77
  console.log(`\n ${s.total} predicates | ${s.passed} passed | ${s.failed} failed\n`);
78
78
 
79
79
  if (!data.passed) {
80
- process.exit(1);
80
+ process.exitCode = 1;
81
81
  }
82
82
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared color constants for the graph viewer.
3
+ *
4
+ * These live in a standalone module so both the domain layer (src/viewer.js)
5
+ * and the presentation layer (src/presentation/viewer.js) can import them
6
+ * without creating a cross-layer dependency.
7
+ */
8
+
9
+ export const DEFAULT_NODE_COLORS = {
10
+ function: '#4CAF50',
11
+ method: '#66BB6A',
12
+ class: '#2196F3',
13
+ interface: '#42A5F5',
14
+ type: '#7E57C2',
15
+ struct: '#FF7043',
16
+ enum: '#FFA726',
17
+ trait: '#26A69A',
18
+ record: '#EC407A',
19
+ module: '#78909C',
20
+ file: '#90A4AE',
21
+ };
22
+
23
+ export const DEFAULT_ROLE_COLORS = {
24
+ entry: '#e8f5e9',
25
+ core: '#e3f2fd',
26
+ utility: '#f5f5f5',
27
+ dead: '#ffebee',
28
+ leaf: '#fffde7',
29
+ };
30
+
31
+ export const COMMUNITY_COLORS = [
32
+ '#4CAF50',
33
+ '#2196F3',
34
+ '#FF9800',
35
+ '#9C27B0',
36
+ '#F44336',
37
+ '#00BCD4',
38
+ '#CDDC39',
39
+ '#E91E63',
40
+ '#3F51B5',
41
+ '#FF5722',
42
+ '#009688',
43
+ '#795548',
44
+ ];
@@ -1,4 +1,4 @@
1
- import { communitiesData } from '../communities.js';
1
+ import { communitiesData } from '../features/communities.js';
2
2
  import { outputResult } from '../infrastructure/result-formatter.js';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { complexityData } from '../complexity.js';
1
+ import { complexityData } from '../features/complexity.js';
2
2
  import { outputResult } from '../infrastructure/result-formatter.js';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { dataflowData, dataflowImpactData } from '../dataflow.js';
1
+ import { dataflowData, dataflowImpactData } from '../features/dataflow.js';
2
2
  import { outputResult } from '../infrastructure/result-formatter.js';
3
3
 
4
4
  /**
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Graph export serializers — pure data → formatted string transforms.
3
+ *
4
+ * Each function receives pre-loaded graph data and returns a formatted string
5
+ * (or structured object for CSV). No DB access — all data must be pre-loaded.
6
+ */
7
+
8
+ import path from 'node:path';
9
+
10
+ // ─── Escape Helpers ──────────────────────────────────────────────────
11
+
12
+ /** Escape special XML characters. */
13
+ export function escapeXml(s) {
14
+ return String(s)
15
+ .replace(/&/g, '&')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;')
18
+ .replace(/"/g, '&quot;')
19
+ .replace(/'/g, '&apos;');
20
+ }
21
+
22
+ /** RFC 4180 CSV field escaping — quote fields containing commas, quotes, or newlines. */
23
+ export function escapeCsv(s) {
24
+ const str = String(s);
25
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
26
+ return `"${str.replace(/"/g, '""')}"`;
27
+ }
28
+ return str;
29
+ }
30
+
31
+ /** Escape double quotes for Mermaid labels. */
32
+ export function escapeLabel(label) {
33
+ return label.replace(/"/g, '#quot;');
34
+ }
35
+
36
+ /** Map node kind to Mermaid shape wrapper. */
37
+ export function mermaidShape(kind, label) {
38
+ const escaped = escapeLabel(label);
39
+ switch (kind) {
40
+ case 'function':
41
+ case 'method':
42
+ return `(["${escaped}"])`;
43
+ case 'class':
44
+ case 'interface':
45
+ case 'type':
46
+ case 'struct':
47
+ case 'enum':
48
+ case 'trait':
49
+ case 'record':
50
+ return `{{"${escaped}"}}`;
51
+ case 'module':
52
+ return `[["${escaped}"]]`;
53
+ default:
54
+ return `["${escaped}"]`;
55
+ }
56
+ }
57
+
58
+ /** Map node role to Mermaid style colors. */
59
+ export const ROLE_STYLES = {
60
+ entry: 'fill:#e8f5e9,stroke:#4caf50',
61
+ core: 'fill:#e3f2fd,stroke:#2196f3',
62
+ utility: 'fill:#f5f5f5,stroke:#9e9e9e',
63
+ dead: 'fill:#ffebee,stroke:#f44336',
64
+ leaf: 'fill:#fffde7,stroke:#fdd835',
65
+ };
66
+
67
+ // ─── DOT Serializer ──────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Render file-level graph data as DOT (Graphviz) format.
71
+ *
72
+ * @param {{ dirs: Array<{ name: string, files: Array<{ path: string, basename: string }>, cohesion: number|null }>, edges: Array<{ source: string, target: string }>, totalEdges: number, limit?: number }} data
73
+ * @returns {string}
74
+ */
75
+ export function renderFileLevelDOT(data) {
76
+ const lines = [
77
+ 'digraph codegraph {',
78
+ ' rankdir=LR;',
79
+ ' node [shape=box, fontname="monospace", fontsize=10];',
80
+ ' edge [color="#666666"];',
81
+ '',
82
+ ];
83
+
84
+ let clusterIdx = 0;
85
+ for (const dir of data.dirs) {
86
+ lines.push(` subgraph cluster_${clusterIdx++} {`);
87
+ const cohLabel = dir.cohesion !== null ? ` (cohesion: ${dir.cohesion.toFixed(2)})` : '';
88
+ lines.push(` label="${dir.name}${cohLabel}";`);
89
+ lines.push(` style=dashed;`);
90
+ lines.push(` color="#999999";`);
91
+ for (const f of dir.files) {
92
+ lines.push(` "${f.path}" [label="${f.basename}"];`);
93
+ }
94
+ lines.push(` }`);
95
+ lines.push('');
96
+ }
97
+
98
+ for (const { source, target } of data.edges) {
99
+ lines.push(` "${source}" -> "${target}";`);
100
+ }
101
+ if (data.limit && data.totalEdges > data.limit) {
102
+ lines.push(` // Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
103
+ }
104
+
105
+ lines.push('}');
106
+ return lines.join('\n');
107
+ }
108
+
109
+ /**
110
+ * Render function-level graph data as DOT (Graphviz) format.
111
+ *
112
+ * @param {{ edges: Array<{ source_name: string, source_file: string, target_name: string, target_file: string }>, totalEdges: number, limit?: number }} data
113
+ * @returns {string}
114
+ */
115
+ export function renderFunctionLevelDOT(data) {
116
+ const lines = [
117
+ 'digraph codegraph {',
118
+ ' rankdir=LR;',
119
+ ' node [shape=box, fontname="monospace", fontsize=10];',
120
+ ' edge [color="#666666"];',
121
+ '',
122
+ ];
123
+
124
+ const emittedNodes = new Set();
125
+ for (const e of data.edges) {
126
+ const sId = `${e.source_file}:${e.source_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
127
+ const tId = `${e.target_file}:${e.target_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
128
+ if (!emittedNodes.has(sId)) {
129
+ lines.push(` ${sId} [label="${e.source_name}\\n${path.basename(e.source_file)}"];`);
130
+ emittedNodes.add(sId);
131
+ }
132
+ if (!emittedNodes.has(tId)) {
133
+ lines.push(` ${tId} [label="${e.target_name}\\n${path.basename(e.target_file)}"];`);
134
+ emittedNodes.add(tId);
135
+ }
136
+ lines.push(` ${sId} -> ${tId};`);
137
+ }
138
+ if (data.limit && data.totalEdges > data.limit) {
139
+ lines.push(` // Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
140
+ }
141
+
142
+ lines.push('}');
143
+ return lines.join('\n');
144
+ }
145
+
146
+ // ─── Mermaid Serializer ──────────────────────────────────────────────
147
+
148
+ /**
149
+ * Render file-level graph data as Mermaid flowchart format.
150
+ *
151
+ * @param {{ direction: string, dirs: Array<{ name: string, files: string[] }>, edges: Array<{ source: string, target: string, edge_kind: string }>, totalEdges: number, limit?: number }} data
152
+ * @returns {string}
153
+ */
154
+ export function renderFileLevelMermaid(data) {
155
+ const lines = [`flowchart ${data.direction || 'LR'}`];
156
+
157
+ let nodeCounter = 0;
158
+ const nodeIdMap = new Map();
159
+ function nodeId(key) {
160
+ if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
161
+ return nodeIdMap.get(key);
162
+ }
163
+
164
+ // Emit subgraphs
165
+ for (const dir of data.dirs) {
166
+ const sgId = dir.name.replace(/[^a-zA-Z0-9]/g, '_');
167
+ lines.push(` subgraph ${sgId}["${escapeLabel(dir.name)}"]`);
168
+ for (const f of dir.files) {
169
+ const nId = nodeId(f);
170
+ lines.push(` ${nId}["${escapeLabel(path.basename(f))}"]`);
171
+ }
172
+ lines.push(' end');
173
+ }
174
+
175
+ // Deduplicate edges per source-target pair, collecting all distinct kinds
176
+ const edgeMap = new Map();
177
+ for (const { source, target, edge_kind } of data.edges) {
178
+ const key = `${source}|${target}`;
179
+ const label = edge_kind === 'imports-type' ? 'imports' : edge_kind;
180
+ if (!edgeMap.has(key)) edgeMap.set(key, { source, target, labels: new Set() });
181
+ edgeMap.get(key).labels.add(label);
182
+ }
183
+
184
+ for (const { source, target, labels } of edgeMap.values()) {
185
+ lines.push(` ${nodeId(source)} -->|${[...labels].join(', ')}| ${nodeId(target)}`);
186
+ }
187
+ if (data.limit && data.totalEdges > data.limit) {
188
+ lines.push(` %% Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
189
+ }
190
+
191
+ return lines.join('\n');
192
+ }
193
+
194
+ /**
195
+ * Render function-level graph data as Mermaid flowchart format.
196
+ *
197
+ * @param {{ direction: string, edges: Array, roles: Map<string, string>, totalEdges: number, limit?: number }} data
198
+ * @returns {string}
199
+ */
200
+ export function renderFunctionLevelMermaid(data) {
201
+ const lines = [`flowchart ${data.direction || 'LR'}`];
202
+
203
+ let nodeCounter = 0;
204
+ const nodeIdMap = new Map();
205
+ function nodeId(key) {
206
+ if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
207
+ return nodeIdMap.get(key);
208
+ }
209
+
210
+ // Group nodes by file for subgraphs
211
+ const fileNodes = new Map();
212
+ const nodeKinds = new Map();
213
+ for (const e of data.edges) {
214
+ const sKey = `${e.source_file}::${e.source_name}`;
215
+ const tKey = `${e.target_file}::${e.target_name}`;
216
+ nodeId(sKey);
217
+ nodeId(tKey);
218
+ nodeKinds.set(sKey, e.source_kind);
219
+ nodeKinds.set(tKey, e.target_kind);
220
+
221
+ if (!fileNodes.has(e.source_file)) fileNodes.set(e.source_file, new Map());
222
+ fileNodes.get(e.source_file).set(sKey, e.source_name);
223
+
224
+ if (!fileNodes.has(e.target_file)) fileNodes.set(e.target_file, new Map());
225
+ fileNodes.get(e.target_file).set(tKey, e.target_name);
226
+ }
227
+
228
+ // Emit subgraphs grouped by file
229
+ for (const [file, nodes] of [...fileNodes].sort((a, b) => a[0].localeCompare(b[0]))) {
230
+ const sgId = file.replace(/[^a-zA-Z0-9]/g, '_');
231
+ lines.push(` subgraph ${sgId}["${escapeLabel(file)}"]`);
232
+ for (const [key, name] of nodes) {
233
+ const kind = nodeKinds.get(key);
234
+ lines.push(` ${nodeId(key)}${mermaidShape(kind, name)}`);
235
+ }
236
+ lines.push(' end');
237
+ }
238
+
239
+ // Emit edges with labels
240
+ for (const e of data.edges) {
241
+ const sId = nodeId(`${e.source_file}::${e.source_name}`);
242
+ const tId = nodeId(`${e.target_file}::${e.target_name}`);
243
+ lines.push(` ${sId} -->|${e.edge_kind}| ${tId}`);
244
+ }
245
+ if (data.limit && data.totalEdges > data.limit) {
246
+ lines.push(` %% Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
247
+ }
248
+
249
+ // Role styling
250
+ const roleStyles = [];
251
+ for (const [key, nid] of nodeIdMap) {
252
+ const role = data.roles?.get(key);
253
+ if (role && ROLE_STYLES[role]) {
254
+ roleStyles.push(` style ${nid} ${ROLE_STYLES[role]}`);
255
+ }
256
+ }
257
+ lines.push(...roleStyles);
258
+
259
+ return lines.join('\n');
260
+ }
261
+
262
+ // ─── GraphML Serializer ──────────────────────────────────────────────
263
+
264
+ /**
265
+ * Render file-level graph data as GraphML (XML) format.
266
+ *
267
+ * @param {{ edges: Array<{ source: string, target: string }> }} data
268
+ * @returns {string}
269
+ */
270
+ export function renderFileLevelGraphML(data) {
271
+ const lines = [
272
+ '<?xml version="1.0" encoding="UTF-8"?>',
273
+ '<graphml xmlns="http://graphml.graphstruct.net/graphml">',
274
+ ' <key id="d0" for="node" attr.name="name" attr.type="string"/>',
275
+ ' <key id="d1" for="node" attr.name="file" attr.type="string"/>',
276
+ ' <key id="d2" for="edge" attr.name="kind" attr.type="string"/>',
277
+ ' <graph id="codegraph" edgedefault="directed">',
278
+ ];
279
+
280
+ const files = new Set();
281
+ for (const { source, target } of data.edges) {
282
+ files.add(source);
283
+ files.add(target);
284
+ }
285
+
286
+ const fileIds = new Map();
287
+ let nIdx = 0;
288
+ for (const f of files) {
289
+ const id = `n${nIdx++}`;
290
+ fileIds.set(f, id);
291
+ lines.push(` <node id="${id}">`);
292
+ lines.push(` <data key="d0">${escapeXml(path.basename(f))}</data>`);
293
+ lines.push(` <data key="d1">${escapeXml(f)}</data>`);
294
+ lines.push(' </node>');
295
+ }
296
+
297
+ let eIdx = 0;
298
+ for (const { source, target } of data.edges) {
299
+ lines.push(
300
+ ` <edge id="e${eIdx++}" source="${fileIds.get(source)}" target="${fileIds.get(target)}">`,
301
+ );
302
+ lines.push(' <data key="d2">imports</data>');
303
+ lines.push(' </edge>');
304
+ }
305
+
306
+ lines.push(' </graph>');
307
+ lines.push('</graphml>');
308
+ return lines.join('\n');
309
+ }
310
+
311
+ /**
312
+ * Render function-level graph data as GraphML (XML) format.
313
+ *
314
+ * @param {{ edges: Array }} data
315
+ * @returns {string}
316
+ */
317
+ export function renderFunctionLevelGraphML(data) {
318
+ const lines = [
319
+ '<?xml version="1.0" encoding="UTF-8"?>',
320
+ '<graphml xmlns="http://graphml.graphstruct.net/graphml">',
321
+ ' <key id="d0" for="node" attr.name="name" attr.type="string"/>',
322
+ ' <key id="d1" for="node" attr.name="kind" attr.type="string"/>',
323
+ ' <key id="d2" for="node" attr.name="file" attr.type="string"/>',
324
+ ' <key id="d3" for="node" attr.name="line" attr.type="int"/>',
325
+ ' <key id="d4" for="node" attr.name="role" attr.type="string"/>',
326
+ ' <key id="d5" for="edge" attr.name="kind" attr.type="string"/>',
327
+ ' <key id="d6" for="edge" attr.name="confidence" attr.type="double"/>',
328
+ ' <graph id="codegraph" edgedefault="directed">',
329
+ ];
330
+
331
+ const emittedNodes = new Set();
332
+ function emitNode(id, name, kind, file, line, role) {
333
+ if (emittedNodes.has(id)) return;
334
+ emittedNodes.add(id);
335
+ lines.push(` <node id="n${id}">`);
336
+ lines.push(` <data key="d0">${escapeXml(name)}</data>`);
337
+ lines.push(` <data key="d1">${escapeXml(kind)}</data>`);
338
+ lines.push(` <data key="d2">${escapeXml(file)}</data>`);
339
+ lines.push(` <data key="d3">${line}</data>`);
340
+ if (role) lines.push(` <data key="d4">${escapeXml(role)}</data>`);
341
+ lines.push(' </node>');
342
+ }
343
+
344
+ let eIdx = 0;
345
+ for (const e of data.edges) {
346
+ emitNode(
347
+ e.source_id,
348
+ e.source_name,
349
+ e.source_kind,
350
+ e.source_file,
351
+ e.source_line,
352
+ e.source_role,
353
+ );
354
+ emitNode(
355
+ e.target_id,
356
+ e.target_name,
357
+ e.target_kind,
358
+ e.target_file,
359
+ e.target_line,
360
+ e.target_role,
361
+ );
362
+ lines.push(` <edge id="e${eIdx++}" source="n${e.source_id}" target="n${e.target_id}">`);
363
+ lines.push(` <data key="d5">${escapeXml(e.edge_kind)}</data>`);
364
+ lines.push(` <data key="d6">${e.confidence}</data>`);
365
+ lines.push(' </edge>');
366
+ }
367
+
368
+ lines.push(' </graph>');
369
+ lines.push('</graphml>');
370
+ return lines.join('\n');
371
+ }
372
+
373
+ // ─── Neo4j CSV Serializer ────────────────────────────────────────────
374
+
375
+ /**
376
+ * Render file-level graph data as Neo4j bulk-import CSV.
377
+ *
378
+ * @param {{ edges: Array<{ source: string, target: string, edge_kind: string, confidence: number }> }} data
379
+ * @returns {{ nodes: string, relationships: string }}
380
+ */
381
+ export function renderFileLevelNeo4jCSV(data) {
382
+ const files = new Map();
383
+ let idx = 0;
384
+ for (const { source, target } of data.edges) {
385
+ if (!files.has(source)) files.set(source, idx++);
386
+ if (!files.has(target)) files.set(target, idx++);
387
+ }
388
+
389
+ const nodeLines = ['nodeId:ID,name,file:string,:LABEL'];
390
+ for (const [file, id] of files) {
391
+ nodeLines.push(`${id},${escapeCsv(path.basename(file))},${escapeCsv(file)},File`);
392
+ }
393
+
394
+ const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
395
+ for (const e of data.edges) {
396
+ const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
397
+ relLines.push(`${files.get(e.source)},${files.get(e.target)},${edgeType},${e.confidence}`);
398
+ }
399
+
400
+ return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
401
+ }
402
+
403
+ /**
404
+ * Render function-level graph data as Neo4j bulk-import CSV.
405
+ *
406
+ * @param {{ edges: Array }} data
407
+ * @returns {{ nodes: string, relationships: string }}
408
+ */
409
+ export function renderFunctionLevelNeo4jCSV(data) {
410
+ const emitted = new Set();
411
+ const nodeLines = ['nodeId:ID,name,kind,file:string,line:int,role,:LABEL'];
412
+ function emitNode(id, name, kind, file, line, role) {
413
+ if (emitted.has(id)) return;
414
+ emitted.add(id);
415
+ const label = kind.charAt(0).toUpperCase() + kind.slice(1);
416
+ nodeLines.push(
417
+ `${id},${escapeCsv(name)},${escapeCsv(kind)},${escapeCsv(file)},${line},${escapeCsv(role || '')},${label}`,
418
+ );
419
+ }
420
+
421
+ const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
422
+ for (const e of data.edges) {
423
+ emitNode(
424
+ e.source_id,
425
+ e.source_name,
426
+ e.source_kind,
427
+ e.source_file,
428
+ e.source_line,
429
+ e.source_role,
430
+ );
431
+ emitNode(
432
+ e.target_id,
433
+ e.target_name,
434
+ e.target_kind,
435
+ e.target_file,
436
+ e.target_line,
437
+ e.target_role,
438
+ );
439
+ const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
440
+ relLines.push(`${e.source_id},${e.target_id},${edgeType},${e.confidence}`);
441
+ }
442
+
443
+ return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
444
+ }