@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
package/src/export.js DELETED
@@ -1,681 +0,0 @@
1
- import path from 'node:path';
2
- import { isTestFile } from './infrastructure/test-filter.js';
3
- import { paginateResult } from './paginate.js';
4
-
5
- const DEFAULT_MIN_CONFIDENCE = 0.5;
6
-
7
- /** Escape special XML characters. */
8
- function escapeXml(s) {
9
- return String(s)
10
- .replace(/&/g, '&')
11
- .replace(/</g, '&lt;')
12
- .replace(/>/g, '&gt;')
13
- .replace(/"/g, '&quot;')
14
- .replace(/'/g, '&apos;');
15
- }
16
-
17
- /** RFC 4180 CSV field escaping — quote fields containing commas, quotes, or newlines. */
18
- function escapeCsv(s) {
19
- const str = String(s);
20
- if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
21
- return `"${str.replace(/"/g, '""')}"`;
22
- }
23
- return str;
24
- }
25
-
26
- /**
27
- * Export the dependency graph in DOT (Graphviz) format.
28
- */
29
- export function exportDOT(db, opts = {}) {
30
- const fileLevel = opts.fileLevel !== false;
31
- const noTests = opts.noTests || false;
32
- const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
33
- const edgeLimit = opts.limit;
34
- const lines = [
35
- 'digraph codegraph {',
36
- ' rankdir=LR;',
37
- ' node [shape=box, fontname="monospace", fontsize=10];',
38
- ' edge [color="#666666"];',
39
- '',
40
- ];
41
-
42
- if (fileLevel) {
43
- let edges = db
44
- .prepare(`
45
- SELECT DISTINCT n1.file AS source, n2.file AS target
46
- FROM edges e
47
- JOIN nodes n1 ON e.source_id = n1.id
48
- JOIN nodes n2 ON e.target_id = n2.id
49
- WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
50
- AND e.confidence >= ?
51
- `)
52
- .all(minConf);
53
- if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
54
- const totalFileEdges = edges.length;
55
- if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
56
-
57
- // Try to use directory nodes from DB (built by structure analysis)
58
- const hasDirectoryNodes =
59
- db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'directory'").get().c > 0;
60
-
61
- const dirs = new Map();
62
- const allFiles = new Set();
63
- for (const { source, target } of edges) {
64
- allFiles.add(source);
65
- allFiles.add(target);
66
- }
67
-
68
- if (hasDirectoryNodes) {
69
- // Use DB directory structure with cohesion labels
70
- const dbDirs = db
71
- .prepare(`
72
- SELECT n.id, n.name, nm.cohesion
73
- FROM nodes n
74
- LEFT JOIN node_metrics nm ON n.id = nm.node_id
75
- WHERE n.kind = 'directory'
76
- `)
77
- .all();
78
-
79
- for (const d of dbDirs) {
80
- const containedFiles = db
81
- .prepare(`
82
- SELECT n.name FROM edges e
83
- JOIN nodes n ON e.target_id = n.id
84
- WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
85
- `)
86
- .all(d.id)
87
- .map((r) => r.name)
88
- .filter((f) => allFiles.has(f));
89
-
90
- if (containedFiles.length > 0) {
91
- dirs.set(d.name, { files: containedFiles, cohesion: d.cohesion });
92
- }
93
- }
94
- } else {
95
- // Fallback: reconstruct from path.dirname()
96
- for (const file of allFiles) {
97
- const dir = path.dirname(file) || '.';
98
- if (!dirs.has(dir)) dirs.set(dir, { files: [], cohesion: null });
99
- dirs.get(dir).files.push(file);
100
- }
101
- }
102
-
103
- let clusterIdx = 0;
104
- for (const [dir, info] of [...dirs].sort((a, b) => a[0].localeCompare(b[0]))) {
105
- lines.push(` subgraph cluster_${clusterIdx++} {`);
106
- const cohLabel = info.cohesion !== null ? ` (cohesion: ${info.cohesion.toFixed(2)})` : '';
107
- lines.push(` label="${dir}${cohLabel}";`);
108
- lines.push(` style=dashed;`);
109
- lines.push(` color="#999999";`);
110
- for (const f of info.files) {
111
- const label = path.basename(f);
112
- lines.push(` "${f}" [label="${label}"];`);
113
- }
114
- lines.push(` }`);
115
- lines.push('');
116
- }
117
-
118
- for (const { source, target } of edges) {
119
- lines.push(` "${source}" -> "${target}";`);
120
- }
121
- if (edgeLimit && totalFileEdges > edgeLimit) {
122
- lines.push(` // Truncated: showing ${edges.length} of ${totalFileEdges} edges`);
123
- }
124
- } else {
125
- let edges = db
126
- .prepare(`
127
- SELECT n1.name AS source_name, n1.kind AS source_kind, n1.file AS source_file,
128
- n2.name AS target_name, n2.kind AS target_kind, n2.file AS target_file,
129
- e.kind AS edge_kind
130
- FROM edges e
131
- JOIN nodes n1 ON e.source_id = n1.id
132
- JOIN nodes n2 ON e.target_id = n2.id
133
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
134
- AND e.kind = 'calls'
135
- AND e.confidence >= ?
136
- `)
137
- .all(minConf);
138
- if (noTests)
139
- edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
140
- const totalFnEdges = edges.length;
141
- if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
142
-
143
- for (const e of edges) {
144
- const sId = `${e.source_file}:${e.source_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
145
- const tId = `${e.target_file}:${e.target_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
146
- lines.push(` ${sId} [label="${e.source_name}\\n${path.basename(e.source_file)}"];`);
147
- lines.push(` ${tId} [label="${e.target_name}\\n${path.basename(e.target_file)}"];`);
148
- lines.push(` ${sId} -> ${tId};`);
149
- }
150
- if (edgeLimit && totalFnEdges > edgeLimit) {
151
- lines.push(` // Truncated: showing ${edges.length} of ${totalFnEdges} edges`);
152
- }
153
- }
154
-
155
- lines.push('}');
156
- return lines.join('\n');
157
- }
158
-
159
- /** Escape double quotes for Mermaid labels. */
160
- function escapeLabel(label) {
161
- return label.replace(/"/g, '#quot;');
162
- }
163
-
164
- /** Map node kind to Mermaid shape wrapper. */
165
- function mermaidShape(kind, label) {
166
- const escaped = escapeLabel(label);
167
- switch (kind) {
168
- case 'function':
169
- case 'method':
170
- return `(["${escaped}"])`;
171
- case 'class':
172
- case 'interface':
173
- case 'type':
174
- case 'struct':
175
- case 'enum':
176
- case 'trait':
177
- case 'record':
178
- return `{{"${escaped}"}}`;
179
- case 'module':
180
- return `[["${escaped}"]]`;
181
- default:
182
- return `["${escaped}"]`;
183
- }
184
- }
185
-
186
- /** Map node role to Mermaid style colors. */
187
- const ROLE_STYLES = {
188
- entry: 'fill:#e8f5e9,stroke:#4caf50',
189
- core: 'fill:#e3f2fd,stroke:#2196f3',
190
- utility: 'fill:#f5f5f5,stroke:#9e9e9e',
191
- dead: 'fill:#ffebee,stroke:#f44336',
192
- leaf: 'fill:#fffde7,stroke:#fdd835',
193
- };
194
-
195
- /**
196
- * Export the dependency graph in Mermaid format.
197
- */
198
- export function exportMermaid(db, opts = {}) {
199
- const fileLevel = opts.fileLevel !== false;
200
- const noTests = opts.noTests || false;
201
- const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
202
- const direction = opts.direction || 'LR';
203
- const edgeLimit = opts.limit;
204
- const lines = [`flowchart ${direction}`];
205
-
206
- let nodeCounter = 0;
207
- const nodeIdMap = new Map();
208
- function nodeId(key) {
209
- if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
210
- return nodeIdMap.get(key);
211
- }
212
-
213
- if (fileLevel) {
214
- let edges = db
215
- .prepare(`
216
- SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind AS edge_kind
217
- FROM edges e
218
- JOIN nodes n1 ON e.source_id = n1.id
219
- JOIN nodes n2 ON e.target_id = n2.id
220
- WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
221
- AND e.confidence >= ?
222
- `)
223
- .all(minConf);
224
- if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
225
- const totalMermaidFileEdges = edges.length;
226
- if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
227
-
228
- // Collect all files referenced in edges
229
- const allFiles = new Set();
230
- for (const { source, target } of edges) {
231
- allFiles.add(source);
232
- allFiles.add(target);
233
- }
234
-
235
- // Build directory groupings — try DB directory nodes first, fall back to path.dirname()
236
- const dirs = new Map();
237
- const hasDirectoryNodes =
238
- db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'directory'").get().c > 0;
239
-
240
- if (hasDirectoryNodes) {
241
- const dbDirs = db.prepare("SELECT id, name FROM nodes WHERE kind = 'directory'").all();
242
- for (const d of dbDirs) {
243
- const containedFiles = db
244
- .prepare(`
245
- SELECT n.name FROM edges e
246
- JOIN nodes n ON e.target_id = n.id
247
- WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
248
- `)
249
- .all(d.id)
250
- .map((r) => r.name)
251
- .filter((f) => allFiles.has(f));
252
- if (containedFiles.length > 0) dirs.set(d.name, containedFiles);
253
- }
254
- } else {
255
- for (const file of allFiles) {
256
- const dir = path.dirname(file) || '.';
257
- if (!dirs.has(dir)) dirs.set(dir, []);
258
- dirs.get(dir).push(file);
259
- }
260
- }
261
-
262
- // Emit subgraphs
263
- for (const [dir, files] of [...dirs].sort((a, b) => a[0].localeCompare(b[0]))) {
264
- const sgId = dir.replace(/[^a-zA-Z0-9]/g, '_');
265
- lines.push(` subgraph ${sgId}["${escapeLabel(dir)}"]`);
266
- for (const f of files) {
267
- const nId = nodeId(f);
268
- lines.push(` ${nId}["${escapeLabel(path.basename(f))}"]`);
269
- }
270
- lines.push(' end');
271
- }
272
-
273
- // Deduplicate edges per source-target pair, collecting all distinct kinds
274
- const edgeMap = new Map();
275
- for (const { source, target, edge_kind } of edges) {
276
- const key = `${source}|${target}`;
277
- const label = edge_kind === 'imports-type' ? 'imports' : edge_kind;
278
- if (!edgeMap.has(key)) edgeMap.set(key, { source, target, labels: new Set() });
279
- edgeMap.get(key).labels.add(label);
280
- }
281
-
282
- for (const { source, target, labels } of edgeMap.values()) {
283
- lines.push(` ${nodeId(source)} -->|${[...labels].join(', ')}| ${nodeId(target)}`);
284
- }
285
- if (edgeLimit && totalMermaidFileEdges > edgeLimit) {
286
- lines.push(` %% Truncated: showing ${edges.length} of ${totalMermaidFileEdges} edges`);
287
- }
288
- } else {
289
- let edges = db
290
- .prepare(`
291
- SELECT n1.name AS source_name, n1.kind AS source_kind, n1.file AS source_file,
292
- n2.name AS target_name, n2.kind AS target_kind, n2.file AS target_file,
293
- e.kind AS edge_kind
294
- FROM edges e
295
- JOIN nodes n1 ON e.source_id = n1.id
296
- JOIN nodes n2 ON e.target_id = n2.id
297
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
298
- AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
299
- AND e.kind = 'calls'
300
- AND e.confidence >= ?
301
- `)
302
- .all(minConf);
303
- if (noTests)
304
- edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
305
- const totalMermaidFnEdges = edges.length;
306
- if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
307
-
308
- // Group nodes by file for subgraphs
309
- const fileNodes = new Map();
310
- const nodeKinds = new Map();
311
- for (const e of edges) {
312
- const sKey = `${e.source_file}::${e.source_name}`;
313
- const tKey = `${e.target_file}::${e.target_name}`;
314
- nodeId(sKey);
315
- nodeId(tKey);
316
- nodeKinds.set(sKey, e.source_kind);
317
- nodeKinds.set(tKey, e.target_kind);
318
-
319
- if (!fileNodes.has(e.source_file)) fileNodes.set(e.source_file, new Map());
320
- fileNodes.get(e.source_file).set(sKey, e.source_name);
321
-
322
- if (!fileNodes.has(e.target_file)) fileNodes.set(e.target_file, new Map());
323
- fileNodes.get(e.target_file).set(tKey, e.target_name);
324
- }
325
-
326
- // Emit subgraphs grouped by file
327
- for (const [file, nodes] of [...fileNodes].sort((a, b) => a[0].localeCompare(b[0]))) {
328
- const sgId = file.replace(/[^a-zA-Z0-9]/g, '_');
329
- lines.push(` subgraph ${sgId}["${escapeLabel(file)}"]`);
330
- for (const [key, name] of nodes) {
331
- const kind = nodeKinds.get(key);
332
- lines.push(` ${nodeId(key)}${mermaidShape(kind, name)}`);
333
- }
334
- lines.push(' end');
335
- }
336
-
337
- // Emit edges with labels
338
- for (const e of edges) {
339
- const sId = nodeId(`${e.source_file}::${e.source_name}`);
340
- const tId = nodeId(`${e.target_file}::${e.target_name}`);
341
- lines.push(` ${sId} -->|${e.edge_kind}| ${tId}`);
342
- }
343
- if (edgeLimit && totalMermaidFnEdges > edgeLimit) {
344
- lines.push(` %% Truncated: showing ${edges.length} of ${totalMermaidFnEdges} edges`);
345
- }
346
-
347
- // Role styling — query roles for all referenced nodes
348
- const allKeys = [...nodeIdMap.keys()];
349
- const roleStyles = [];
350
- for (const key of allKeys) {
351
- const colonIdx = key.indexOf('::');
352
- if (colonIdx === -1) continue;
353
- const file = key.slice(0, colonIdx);
354
- const name = key.slice(colonIdx + 2);
355
- const row = db
356
- .prepare('SELECT role FROM nodes WHERE file = ? AND name = ? AND role IS NOT NULL LIMIT 1')
357
- .get(file, name);
358
- if (row?.role && ROLE_STYLES[row.role]) {
359
- roleStyles.push(` style ${nodeIdMap.get(key)} ${ROLE_STYLES[row.role]}`);
360
- }
361
- }
362
- lines.push(...roleStyles);
363
- }
364
-
365
- return lines.join('\n');
366
- }
367
-
368
- /**
369
- * Export as JSON adjacency list.
370
- */
371
- export function exportJSON(db, opts = {}) {
372
- const noTests = opts.noTests || false;
373
- const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
374
-
375
- let nodes = db
376
- .prepare(`
377
- SELECT id, name, kind, file, line FROM nodes WHERE kind = 'file'
378
- `)
379
- .all();
380
- if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
381
-
382
- let edges = db
383
- .prepare(`
384
- SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind, e.confidence
385
- FROM edges e
386
- JOIN nodes n1 ON e.source_id = n1.id
387
- JOIN nodes n2 ON e.target_id = n2.id
388
- WHERE n1.file != n2.file AND e.confidence >= ?
389
- `)
390
- .all(minConf);
391
- if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
392
-
393
- const base = { nodes, edges };
394
- return paginateResult(base, 'edges', { limit: opts.limit, offset: opts.offset });
395
- }
396
-
397
- /**
398
- * Export the dependency graph in GraphML (XML) format.
399
- */
400
- export function exportGraphML(db, opts = {}) {
401
- const fileLevel = opts.fileLevel !== false;
402
- const noTests = opts.noTests || false;
403
- const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
404
- const edgeLimit = opts.limit;
405
-
406
- const lines = [
407
- '<?xml version="1.0" encoding="UTF-8"?>',
408
- '<graphml xmlns="http://graphml.graphstruct.net/graphml">',
409
- ];
410
-
411
- if (fileLevel) {
412
- lines.push(' <key id="d0" for="node" attr.name="name" attr.type="string"/>');
413
- lines.push(' <key id="d1" for="node" attr.name="file" attr.type="string"/>');
414
- lines.push(' <key id="d2" for="edge" attr.name="kind" attr.type="string"/>');
415
- lines.push(' <graph id="codegraph" edgedefault="directed">');
416
-
417
- let edges = db
418
- .prepare(`
419
- SELECT DISTINCT n1.file AS source, n2.file AS target
420
- FROM edges e
421
- JOIN nodes n1 ON e.source_id = n1.id
422
- JOIN nodes n2 ON e.target_id = n2.id
423
- WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
424
- AND e.confidence >= ?
425
- `)
426
- .all(minConf);
427
- if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
428
- if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
429
-
430
- const files = new Set();
431
- for (const { source, target } of edges) {
432
- files.add(source);
433
- files.add(target);
434
- }
435
-
436
- const fileIds = new Map();
437
- let nIdx = 0;
438
- for (const f of files) {
439
- const id = `n${nIdx++}`;
440
- fileIds.set(f, id);
441
- lines.push(` <node id="${id}">`);
442
- lines.push(` <data key="d0">${escapeXml(path.basename(f))}</data>`);
443
- lines.push(` <data key="d1">${escapeXml(f)}</data>`);
444
- lines.push(' </node>');
445
- }
446
-
447
- let eIdx = 0;
448
- for (const { source, target } of edges) {
449
- lines.push(
450
- ` <edge id="e${eIdx++}" source="${fileIds.get(source)}" target="${fileIds.get(target)}">`,
451
- );
452
- lines.push(' <data key="d2">imports</data>');
453
- lines.push(' </edge>');
454
- }
455
- } else {
456
- lines.push(' <key id="d0" for="node" attr.name="name" attr.type="string"/>');
457
- lines.push(' <key id="d1" for="node" attr.name="kind" attr.type="string"/>');
458
- lines.push(' <key id="d2" for="node" attr.name="file" attr.type="string"/>');
459
- lines.push(' <key id="d3" for="node" attr.name="line" attr.type="int"/>');
460
- lines.push(' <key id="d4" for="node" attr.name="role" attr.type="string"/>');
461
- lines.push(' <key id="d5" for="edge" attr.name="kind" attr.type="string"/>');
462
- lines.push(' <key id="d6" for="edge" attr.name="confidence" attr.type="double"/>');
463
- lines.push(' <graph id="codegraph" edgedefault="directed">');
464
-
465
- let edges = db
466
- .prepare(`
467
- SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
468
- n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
469
- n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
470
- n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
471
- e.kind AS edge_kind, e.confidence
472
- FROM edges e
473
- JOIN nodes n1 ON e.source_id = n1.id
474
- JOIN nodes n2 ON e.target_id = n2.id
475
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
476
- AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
477
- AND e.kind = 'calls'
478
- AND e.confidence >= ?
479
- `)
480
- .all(minConf);
481
- if (noTests)
482
- edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
483
- if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
484
-
485
- const emittedNodes = new Set();
486
- function emitNode(id, name, kind, file, line, role) {
487
- if (emittedNodes.has(id)) return;
488
- emittedNodes.add(id);
489
- lines.push(` <node id="n${id}">`);
490
- lines.push(` <data key="d0">${escapeXml(name)}</data>`);
491
- lines.push(` <data key="d1">${escapeXml(kind)}</data>`);
492
- lines.push(` <data key="d2">${escapeXml(file)}</data>`);
493
- lines.push(` <data key="d3">${line}</data>`);
494
- if (role) lines.push(` <data key="d4">${escapeXml(role)}</data>`);
495
- lines.push(' </node>');
496
- }
497
-
498
- let eIdx = 0;
499
- for (const e of edges) {
500
- emitNode(
501
- e.source_id,
502
- e.source_name,
503
- e.source_kind,
504
- e.source_file,
505
- e.source_line,
506
- e.source_role,
507
- );
508
- emitNode(
509
- e.target_id,
510
- e.target_name,
511
- e.target_kind,
512
- e.target_file,
513
- e.target_line,
514
- e.target_role,
515
- );
516
- lines.push(` <edge id="e${eIdx++}" source="n${e.source_id}" target="n${e.target_id}">`);
517
- lines.push(` <data key="d5">${escapeXml(e.edge_kind)}</data>`);
518
- lines.push(` <data key="d6">${e.confidence}</data>`);
519
- lines.push(' </edge>');
520
- }
521
- }
522
-
523
- lines.push(' </graph>');
524
- lines.push('</graphml>');
525
- return lines.join('\n');
526
- }
527
-
528
- /**
529
- * Export the dependency graph in TinkerPop GraphSON v3 format.
530
- */
531
- export function exportGraphSON(db, opts = {}) {
532
- const noTests = opts.noTests || false;
533
- const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
534
-
535
- let nodes = db
536
- .prepare(`
537
- SELECT id, name, kind, file, line, role FROM nodes
538
- WHERE kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'file')
539
- `)
540
- .all();
541
- if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
542
-
543
- let edges = db
544
- .prepare(`
545
- SELECT e.rowid AS id, n1.id AS outV, n2.id AS inV, e.kind, e.confidence
546
- FROM edges e
547
- JOIN nodes n1 ON e.source_id = n1.id
548
- JOIN nodes n2 ON e.target_id = n2.id
549
- WHERE e.confidence >= ?
550
- `)
551
- .all(minConf);
552
- if (noTests) {
553
- const nodeIds = new Set(nodes.map((n) => n.id));
554
- edges = edges.filter((e) => nodeIds.has(e.outV) && nodeIds.has(e.inV));
555
- }
556
-
557
- const vertices = nodes.map((n) => ({
558
- id: n.id,
559
- label: n.kind,
560
- properties: {
561
- name: [{ id: 0, value: n.name }],
562
- file: [{ id: 0, value: n.file }],
563
- ...(n.line != null ? { line: [{ id: 0, value: n.line }] } : {}),
564
- ...(n.role ? { role: [{ id: 0, value: n.role }] } : {}),
565
- },
566
- }));
567
-
568
- const gEdges = edges.map((e) => ({
569
- id: e.id,
570
- label: e.kind,
571
- inV: e.inV,
572
- outV: e.outV,
573
- properties: {
574
- confidence: e.confidence,
575
- },
576
- }));
577
-
578
- const base = { vertices, edges: gEdges };
579
- return paginateResult(base, 'edges', { limit: opts.limit, offset: opts.offset });
580
- }
581
-
582
- /**
583
- * Export the dependency graph as Neo4j bulk-import CSV files.
584
- * Returns { nodes: string, relationships: string }.
585
- */
586
- export function exportNeo4jCSV(db, opts = {}) {
587
- const fileLevel = opts.fileLevel !== false;
588
- const noTests = opts.noTests || false;
589
- const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
590
- const edgeLimit = opts.limit;
591
-
592
- if (fileLevel) {
593
- let edges = db
594
- .prepare(`
595
- SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind, e.confidence
596
- FROM edges e
597
- JOIN nodes n1 ON e.source_id = n1.id
598
- JOIN nodes n2 ON e.target_id = n2.id
599
- WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
600
- AND e.confidence >= ?
601
- `)
602
- .all(minConf);
603
- if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
604
- if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
605
-
606
- const files = new Map();
607
- let idx = 0;
608
- for (const { source, target } of edges) {
609
- if (!files.has(source)) files.set(source, idx++);
610
- if (!files.has(target)) files.set(target, idx++);
611
- }
612
-
613
- const nodeLines = ['nodeId:ID,name,file:string,:LABEL'];
614
- for (const [file, id] of files) {
615
- nodeLines.push(`${id},${escapeCsv(path.basename(file))},${escapeCsv(file)},File`);
616
- }
617
-
618
- const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
619
- for (const e of edges) {
620
- const edgeType = e.kind.toUpperCase().replace(/-/g, '_');
621
- relLines.push(`${files.get(e.source)},${files.get(e.target)},${edgeType},${e.confidence}`);
622
- }
623
-
624
- return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
625
- }
626
-
627
- let edges = db
628
- .prepare(`
629
- SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
630
- n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
631
- n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
632
- n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
633
- e.kind AS edge_kind, e.confidence
634
- FROM edges e
635
- JOIN nodes n1 ON e.source_id = n1.id
636
- JOIN nodes n2 ON e.target_id = n2.id
637
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
638
- AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
639
- AND e.kind = 'calls'
640
- AND e.confidence >= ?
641
- `)
642
- .all(minConf);
643
- if (noTests)
644
- edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
645
- if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
646
-
647
- const emitted = new Set();
648
- const nodeLines = ['nodeId:ID,name,kind,file:string,line:int,role,:LABEL'];
649
- function emitNode(id, name, kind, file, line, role) {
650
- if (emitted.has(id)) return;
651
- emitted.add(id);
652
- const label = kind.charAt(0).toUpperCase() + kind.slice(1);
653
- nodeLines.push(
654
- `${id},${escapeCsv(name)},${escapeCsv(kind)},${escapeCsv(file)},${line},${escapeCsv(role || '')},${label}`,
655
- );
656
- }
657
-
658
- const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
659
- for (const e of edges) {
660
- emitNode(
661
- e.source_id,
662
- e.source_name,
663
- e.source_kind,
664
- e.source_file,
665
- e.source_line,
666
- e.source_role,
667
- );
668
- emitNode(
669
- e.target_id,
670
- e.target_name,
671
- e.target_kind,
672
- e.target_file,
673
- e.target_line,
674
- e.target_role,
675
- );
676
- const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
677
- relLines.push(`${e.source_id},${e.target_id},${edgeType},${e.confidence}`);
678
- }
679
-
680
- return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
681
- }