@optave/codegraph 3.1.3 → 3.1.4

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 (185) hide show
  1. package/README.md +17 -19
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1485
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1522
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/cycles.js +30 -85
  76. package/src/dataflow.js +1 -2
  77. package/src/db/connection.js +4 -4
  78. package/src/db/migrations.js +41 -0
  79. package/src/db/query-builder.js +6 -5
  80. package/src/db/repository/base.js +201 -0
  81. package/src/db/repository/graph-read.js +5 -2
  82. package/src/db/repository/in-memory-repository.js +584 -0
  83. package/src/db/repository/index.js +5 -1
  84. package/src/db/repository/nodes.js +63 -4
  85. package/src/db/repository/sqlite-repository.js +219 -0
  86. package/src/db.js +5 -0
  87. package/src/embeddings/generator.js +163 -0
  88. package/src/embeddings/index.js +13 -0
  89. package/src/embeddings/models.js +218 -0
  90. package/src/embeddings/search/cli-formatter.js +151 -0
  91. package/src/embeddings/search/filters.js +46 -0
  92. package/src/embeddings/search/hybrid.js +121 -0
  93. package/src/embeddings/search/keyword.js +68 -0
  94. package/src/embeddings/search/prepare.js +66 -0
  95. package/src/embeddings/search/semantic.js +145 -0
  96. package/src/embeddings/stores/fts5.js +27 -0
  97. package/src/embeddings/stores/sqlite-blob.js +24 -0
  98. package/src/embeddings/strategies/source.js +14 -0
  99. package/src/embeddings/strategies/structured.js +43 -0
  100. package/src/embeddings/strategies/text-utils.js +43 -0
  101. package/src/errors.js +78 -0
  102. package/src/export.js +217 -520
  103. package/src/extractors/csharp.js +10 -2
  104. package/src/extractors/go.js +3 -1
  105. package/src/extractors/helpers.js +71 -0
  106. package/src/extractors/java.js +9 -2
  107. package/src/extractors/javascript.js +38 -1
  108. package/src/extractors/php.js +3 -1
  109. package/src/extractors/python.js +14 -3
  110. package/src/extractors/rust.js +3 -1
  111. package/src/graph/algorithms/bfs.js +49 -0
  112. package/src/graph/algorithms/centrality.js +16 -0
  113. package/src/graph/algorithms/index.js +5 -0
  114. package/src/graph/algorithms/louvain.js +26 -0
  115. package/src/graph/algorithms/shortest-path.js +41 -0
  116. package/src/graph/algorithms/tarjan.js +49 -0
  117. package/src/graph/builders/dependency.js +91 -0
  118. package/src/graph/builders/index.js +3 -0
  119. package/src/graph/builders/structure.js +40 -0
  120. package/src/graph/builders/temporal.js +33 -0
  121. package/src/graph/classifiers/index.js +2 -0
  122. package/src/graph/classifiers/risk.js +85 -0
  123. package/src/graph/classifiers/roles.js +64 -0
  124. package/src/graph/index.js +13 -0
  125. package/src/graph/model.js +230 -0
  126. package/src/index.js +33 -210
  127. package/src/infrastructure/result-formatter.js +2 -21
  128. package/src/mcp/index.js +2 -0
  129. package/src/mcp/middleware.js +26 -0
  130. package/src/mcp/server.js +128 -0
  131. package/src/mcp/tool-registry.js +801 -0
  132. package/src/mcp/tools/ast-query.js +14 -0
  133. package/src/mcp/tools/audit.js +21 -0
  134. package/src/mcp/tools/batch-query.js +11 -0
  135. package/src/mcp/tools/branch-compare.js +10 -0
  136. package/src/mcp/tools/cfg.js +21 -0
  137. package/src/mcp/tools/check.js +43 -0
  138. package/src/mcp/tools/co-changes.js +20 -0
  139. package/src/mcp/tools/code-owners.js +12 -0
  140. package/src/mcp/tools/communities.js +15 -0
  141. package/src/mcp/tools/complexity.js +18 -0
  142. package/src/mcp/tools/context.js +17 -0
  143. package/src/mcp/tools/dataflow.js +26 -0
  144. package/src/mcp/tools/diff-impact.js +24 -0
  145. package/src/mcp/tools/execution-flow.js +26 -0
  146. package/src/mcp/tools/export-graph.js +57 -0
  147. package/src/mcp/tools/file-deps.js +12 -0
  148. package/src/mcp/tools/file-exports.js +13 -0
  149. package/src/mcp/tools/find-cycles.js +15 -0
  150. package/src/mcp/tools/fn-impact.js +15 -0
  151. package/src/mcp/tools/impact-analysis.js +12 -0
  152. package/src/mcp/tools/index.js +71 -0
  153. package/src/mcp/tools/list-functions.js +14 -0
  154. package/src/mcp/tools/list-repos.js +11 -0
  155. package/src/mcp/tools/module-map.js +6 -0
  156. package/src/mcp/tools/node-roles.js +14 -0
  157. package/src/mcp/tools/path.js +12 -0
  158. package/src/mcp/tools/query.js +30 -0
  159. package/src/mcp/tools/semantic-search.js +65 -0
  160. package/src/mcp/tools/sequence.js +17 -0
  161. package/src/mcp/tools/structure.js +15 -0
  162. package/src/mcp/tools/symbol-children.js +14 -0
  163. package/src/mcp/tools/triage.js +35 -0
  164. package/src/mcp/tools/where.js +13 -0
  165. package/src/mcp.js +2 -1470
  166. package/src/native.js +3 -1
  167. package/src/presentation/colors.js +44 -0
  168. package/src/presentation/export.js +444 -0
  169. package/src/presentation/result-formatter.js +21 -0
  170. package/src/presentation/sequence-renderer.js +43 -0
  171. package/src/presentation/table.js +47 -0
  172. package/src/presentation/viewer.js +634 -0
  173. package/src/queries.js +35 -2276
  174. package/src/resolve.js +1 -1
  175. package/src/sequence.js +2 -38
  176. package/src/shared/file-utils.js +153 -0
  177. package/src/shared/generators.js +125 -0
  178. package/src/shared/hierarchy.js +27 -0
  179. package/src/shared/normalize.js +59 -0
  180. package/src/snapshot.js +6 -5
  181. package/src/structure.js +15 -40
  182. package/src/triage.js +20 -72
  183. package/src/viewer.js +35 -656
  184. package/src/watcher.js +8 -148
  185. package/src/embedder.js +0 -1097
package/src/native.js CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { createRequire } from 'node:module';
10
10
  import os from 'node:os';
11
+ import { EngineError } from './errors.js';
11
12
 
12
13
  let _cached; // undefined = not yet tried, null = failed, object = module
13
14
  let _loadError = null;
@@ -101,9 +102,10 @@ export function getNativePackageVersion() {
101
102
  export function getNative() {
102
103
  const mod = loadNative();
103
104
  if (!mod) {
104
- throw new Error(
105
+ throw new EngineError(
105
106
  `Native codegraph-core not available: ${_loadError?.message || 'unknown error'}. ` +
106
107
  'Install the platform package or use --engine wasm.',
108
+ { cause: _loadError },
107
109
  );
108
110
  }
109
111
  return mod;
@@ -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
+ ];
@@ -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
+ }
@@ -0,0 +1,21 @@
1
+ import { printNdjson } from '../paginate.js';
2
+
3
+ /**
4
+ * Shared JSON / NDJSON output dispatch for CLI wrappers.
5
+ *
6
+ * @param {object} data - Result object from a *Data() function
7
+ * @param {string} field - Array field name for NDJSON streaming (e.g. 'results')
8
+ * @param {object} opts - CLI options ({ json?, ndjson? })
9
+ * @returns {boolean} true if output was handled (caller should return early)
10
+ */
11
+ export function outputResult(data, field, opts) {
12
+ if (opts.ndjson) {
13
+ printNdjson(data, field);
14
+ return true;
15
+ }
16
+ if (opts.json) {
17
+ console.log(JSON.stringify(data, null, 2));
18
+ return true;
19
+ }
20
+ return false;
21
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Mermaid sequence diagram renderer — pure data → string transform.
3
+ *
4
+ * Converts sequenceData() output into Mermaid sequenceDiagram syntax.
5
+ * No DB access, no I/O — just data in, formatted string out.
6
+ */
7
+
8
+ /**
9
+ * Escape special Mermaid characters in labels.
10
+ */
11
+ function escapeMermaid(str) {
12
+ return str
13
+ .replace(/</g, '&lt;')
14
+ .replace(/>/g, '&gt;')
15
+ .replace(/:/g, '#colon;')
16
+ .replace(/"/g, '#quot;');
17
+ }
18
+
19
+ /**
20
+ * Convert sequenceData result to Mermaid sequenceDiagram syntax.
21
+ * @param {{ participants, messages, truncated, depth }} seqResult
22
+ * @returns {string}
23
+ */
24
+ export function sequenceToMermaid(seqResult) {
25
+ const lines = ['sequenceDiagram'];
26
+
27
+ for (const p of seqResult.participants) {
28
+ lines.push(` participant ${p.id} as ${escapeMermaid(p.label)}`);
29
+ }
30
+
31
+ for (const msg of seqResult.messages) {
32
+ const arrow = msg.type === 'return' ? '-->>' : '->>';
33
+ lines.push(` ${msg.from}${arrow}${msg.to}: ${escapeMermaid(msg.label)}`);
34
+ }
35
+
36
+ if (seqResult.truncated && seqResult.participants.length > 0) {
37
+ lines.push(
38
+ ` note right of ${seqResult.participants[0].id}: Truncated at depth ${seqResult.depth}`,
39
+ );
40
+ }
41
+
42
+ return lines.join('\n');
43
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Shared table formatting utilities for CLI output.
3
+ *
4
+ * Pure data → formatted string transforms. No I/O — callers handle printing.
5
+ */
6
+
7
+ /**
8
+ * Format a table with aligned columns.
9
+ *
10
+ * @param {object} opts
11
+ * @param {Array<{ header: string, width: number, align?: 'left'|'right' }>} opts.columns
12
+ * @param {string[][]} opts.rows - Each row is an array of string cell values
13
+ * @param {number} [opts.indent=2] - Leading spaces per line
14
+ * @returns {string} Formatted table string (header + separator + data rows)
15
+ */
16
+ export function formatTable({ columns, rows, indent = 2 }) {
17
+ const prefix = ' '.repeat(indent);
18
+ const header = columns
19
+ .map((c) => (c.align === 'right' ? c.header.padStart(c.width) : c.header.padEnd(c.width)))
20
+ .join(' ');
21
+ const separator = columns.map((c) => '\u2500'.repeat(c.width)).join(' ');
22
+ const lines = [`${prefix}${header}`, `${prefix}${separator}`];
23
+ for (const row of rows) {
24
+ const cells = columns.map((c, i) => {
25
+ const val = row[i] ?? '';
26
+ return c.align === 'right' ? val.padStart(c.width) : val.padEnd(c.width);
27
+ });
28
+ lines.push(`${prefix}${cells.join(' ')}`);
29
+ }
30
+ return lines.join('\n');
31
+ }
32
+
33
+ /**
34
+ * Truncate a string from the end, appending '\u2026' if truncated.
35
+ */
36
+ export function truncEnd(str, maxLen) {
37
+ if (str.length <= maxLen) return str;
38
+ return `${str.slice(0, maxLen - 1)}\u2026`;
39
+ }
40
+
41
+ /**
42
+ * Truncate a string from the start, prepending '\u2026' if truncated.
43
+ */
44
+ export function truncStart(str, maxLen) {
45
+ if (str.length <= maxLen) return str;
46
+ return `\u2026${str.slice(-(maxLen - 1))}`;
47
+ }