@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
@@ -0,0 +1,584 @@
1
+ import { ConfigError } from '../../errors.js';
2
+ import { CORE_SYMBOL_KINDS, EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
3
+ import { Repository } from './base.js';
4
+
5
+ /**
6
+ * Escape LIKE special characters so they are treated as literals.
7
+ * Mirrors the `escapeLike` function in `nodes.js`.
8
+ * @param {string} s
9
+ * @returns {string}
10
+ */
11
+ function escapeLike(s) {
12
+ return s.replace(/[%_\\]/g, '\\$&');
13
+ }
14
+
15
+ /**
16
+ * Convert a SQL LIKE pattern to a RegExp (case-insensitive).
17
+ * Supports `%` (any chars) and `_` (single char).
18
+ * @param {string} pattern
19
+ * @returns {RegExp}
20
+ */
21
+ function likeToRegex(pattern) {
22
+ let regex = '';
23
+ for (let i = 0; i < pattern.length; i++) {
24
+ const ch = pattern[i];
25
+ if (ch === '\\' && i + 1 < pattern.length) {
26
+ // Escaped literal
27
+ regex += pattern[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ } else if (ch === '%') {
29
+ regex += '.*';
30
+ } else if (ch === '_') {
31
+ regex += '.';
32
+ } else {
33
+ regex += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
34
+ }
35
+ }
36
+ return new RegExp(`^${regex}$`, 'i');
37
+ }
38
+
39
+ /**
40
+ * In-memory Repository implementation backed by Maps.
41
+ * No SQLite dependency — suitable for fast unit tests.
42
+ */
43
+ export class InMemoryRepository extends Repository {
44
+ #nodes = new Map(); // id → node object
45
+ #edges = new Map(); // id → edge object
46
+ #complexity = new Map(); // node_id → complexity metrics
47
+ #nextNodeId = 1;
48
+ #nextEdgeId = 1;
49
+
50
+ // ── Mutation (test setup only) ────────────────────────────────────
51
+
52
+ /**
53
+ * Add a node. Returns the auto-assigned id.
54
+ * @param {object} attrs - { name, kind, file, line, end_line?, parent_id?, exported?, qualified_name?, scope?, visibility?, role? }
55
+ * @returns {number}
56
+ */
57
+ addNode(attrs) {
58
+ const id = this.#nextNodeId++;
59
+ this.#nodes.set(id, {
60
+ id,
61
+ name: attrs.name,
62
+ kind: attrs.kind,
63
+ file: attrs.file,
64
+ line: attrs.line,
65
+ end_line: attrs.end_line ?? null,
66
+ parent_id: attrs.parent_id ?? null,
67
+ exported: attrs.exported ?? null,
68
+ qualified_name: attrs.qualified_name ?? null,
69
+ scope: attrs.scope ?? null,
70
+ visibility: attrs.visibility ?? null,
71
+ role: attrs.role ?? null,
72
+ });
73
+ return id;
74
+ }
75
+
76
+ /**
77
+ * Add an edge. Returns the auto-assigned id.
78
+ * @param {object} attrs - { source_id, target_id, kind, confidence?, dynamic? }
79
+ * @returns {number}
80
+ */
81
+ addEdge(attrs) {
82
+ const id = this.#nextEdgeId++;
83
+ this.#edges.set(id, {
84
+ id,
85
+ source_id: attrs.source_id,
86
+ target_id: attrs.target_id,
87
+ kind: attrs.kind,
88
+ confidence: attrs.confidence ?? null,
89
+ dynamic: attrs.dynamic ?? 0,
90
+ });
91
+ return id;
92
+ }
93
+
94
+ /**
95
+ * Add complexity metrics for a node.
96
+ * @param {number} nodeId
97
+ * @param {object} metrics - { cognitive, cyclomatic, max_nesting, maintainability_index?, halstead_volume? }
98
+ */
99
+ addComplexity(nodeId, metrics) {
100
+ this.#complexity.set(nodeId, {
101
+ cognitive: metrics.cognitive ?? 0,
102
+ cyclomatic: metrics.cyclomatic ?? 0,
103
+ max_nesting: metrics.max_nesting ?? 0,
104
+ maintainability_index: metrics.maintainability_index ?? 0,
105
+ halstead_volume: metrics.halstead_volume ?? 0,
106
+ });
107
+ }
108
+
109
+ // ── Node lookups ──────────────────────────────────────────────────
110
+
111
+ findNodeById(id) {
112
+ return this.#nodes.get(id) ?? undefined;
113
+ }
114
+
115
+ findNodesByFile(file) {
116
+ return [...this.#nodes.values()]
117
+ .filter((n) => n.file === file && n.kind !== 'file')
118
+ .sort((a, b) => a.line - b.line);
119
+ }
120
+
121
+ findFileNodes(fileLike) {
122
+ const re = likeToRegex(fileLike);
123
+ return [...this.#nodes.values()].filter((n) => n.kind === 'file' && re.test(n.file));
124
+ }
125
+
126
+ findNodesWithFanIn(namePattern, opts = {}) {
127
+ const re = likeToRegex(namePattern);
128
+ let nodes = [...this.#nodes.values()].filter((n) => re.test(n.name));
129
+
130
+ if (opts.kinds) {
131
+ nodes = nodes.filter((n) => opts.kinds.includes(n.kind));
132
+ }
133
+ if (opts.file) {
134
+ const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
135
+ nodes = nodes.filter((n) => fileRe.test(n.file));
136
+ }
137
+
138
+ // Compute fan-in per node
139
+ const fanInMap = this.#computeFanIn();
140
+ return nodes.map((n) => ({ ...n, fan_in: fanInMap.get(n.id) ?? 0 }));
141
+ }
142
+
143
+ countNodes() {
144
+ return this.#nodes.size;
145
+ }
146
+
147
+ countEdges() {
148
+ return this.#edges.size;
149
+ }
150
+
151
+ countFiles() {
152
+ const files = new Set();
153
+ for (const n of this.#nodes.values()) {
154
+ files.add(n.file);
155
+ }
156
+ return files.size;
157
+ }
158
+
159
+ getNodeId(name, kind, file, line) {
160
+ for (const n of this.#nodes.values()) {
161
+ if (n.name === name && n.kind === kind && n.file === file && n.line === line) {
162
+ return n.id;
163
+ }
164
+ }
165
+ return undefined;
166
+ }
167
+
168
+ getFunctionNodeId(name, file, line) {
169
+ for (const n of this.#nodes.values()) {
170
+ if (
171
+ n.name === name &&
172
+ (n.kind === 'function' || n.kind === 'method') &&
173
+ n.file === file &&
174
+ n.line === line
175
+ ) {
176
+ return n.id;
177
+ }
178
+ }
179
+ return undefined;
180
+ }
181
+
182
+ bulkNodeIdsByFile(file) {
183
+ return [...this.#nodes.values()]
184
+ .filter((n) => n.file === file)
185
+ .map((n) => ({ id: n.id, name: n.name, kind: n.kind, line: n.line }));
186
+ }
187
+
188
+ findNodeChildren(parentId) {
189
+ return [...this.#nodes.values()]
190
+ .filter((n) => n.parent_id === parentId)
191
+ .sort((a, b) => a.line - b.line)
192
+ .map((n) => ({
193
+ name: n.name,
194
+ kind: n.kind,
195
+ line: n.line,
196
+ end_line: n.end_line,
197
+ qualified_name: n.qualified_name,
198
+ scope: n.scope,
199
+ visibility: n.visibility,
200
+ }));
201
+ }
202
+
203
+ findNodesByScope(scopeName, opts = {}) {
204
+ let nodes = [...this.#nodes.values()].filter((n) => n.scope === scopeName);
205
+
206
+ if (opts.kind) {
207
+ nodes = nodes.filter((n) => n.kind === opts.kind);
208
+ }
209
+ if (opts.file) {
210
+ const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
211
+ nodes = nodes.filter((n) => fileRe.test(n.file));
212
+ }
213
+
214
+ return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
215
+ }
216
+
217
+ findNodeByQualifiedName(qualifiedName, opts = {}) {
218
+ let nodes = [...this.#nodes.values()].filter((n) => n.qualified_name === qualifiedName);
219
+
220
+ if (opts.file) {
221
+ const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
222
+ nodes = nodes.filter((n) => fileRe.test(n.file));
223
+ }
224
+
225
+ return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
226
+ }
227
+
228
+ listFunctionNodes(opts = {}) {
229
+ return [...this.#iterateFunctionNodesImpl(opts)];
230
+ }
231
+
232
+ *iterateFunctionNodes(opts = {}) {
233
+ yield* this.#iterateFunctionNodesImpl(opts);
234
+ }
235
+
236
+ findNodesForTriage(opts = {}) {
237
+ if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
238
+ throw new ConfigError(
239
+ `Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`,
240
+ );
241
+ }
242
+ if (opts.role && !VALID_ROLES.includes(opts.role)) {
243
+ throw new ConfigError(
244
+ `Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`,
245
+ );
246
+ }
247
+ const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
248
+ let nodes = [...this.#nodes.values()].filter((n) => kindsToUse.includes(n.kind));
249
+
250
+ if (opts.noTests) {
251
+ nodes = nodes.filter(
252
+ (n) =>
253
+ !n.file.includes('.test.') &&
254
+ !n.file.includes('.spec.') &&
255
+ !n.file.includes('__test__') &&
256
+ !n.file.includes('__tests__') &&
257
+ !n.file.includes('.stories.'),
258
+ );
259
+ }
260
+ if (opts.file) {
261
+ const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
262
+ nodes = nodes.filter((n) => fileRe.test(n.file));
263
+ }
264
+ if (opts.role) {
265
+ nodes = nodes.filter((n) => n.role === opts.role);
266
+ }
267
+
268
+ const fanInMap = this.#computeFanIn();
269
+ return nodes
270
+ .sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
271
+ .map((n) => {
272
+ const cx = this.#complexity.get(n.id);
273
+ return {
274
+ id: n.id,
275
+ name: n.name,
276
+ kind: n.kind,
277
+ file: n.file,
278
+ line: n.line,
279
+ end_line: n.end_line,
280
+ role: n.role,
281
+ fan_in: fanInMap.get(n.id) ?? 0,
282
+ cognitive: cx?.cognitive ?? 0,
283
+ mi: cx?.maintainability_index ?? 0,
284
+ cyclomatic: cx?.cyclomatic ?? 0,
285
+ max_nesting: cx?.max_nesting ?? 0,
286
+ churn: 0, // no co-change data in-memory
287
+ };
288
+ });
289
+ }
290
+
291
+ // ── Edge queries ──────────────────────────────────────────────────
292
+
293
+ findCallees(nodeId) {
294
+ const seen = new Set();
295
+ const results = [];
296
+ for (const e of this.#edges.values()) {
297
+ if (e.source_id === nodeId && e.kind === 'calls' && !seen.has(e.target_id)) {
298
+ seen.add(e.target_id);
299
+ const n = this.#nodes.get(e.target_id);
300
+ if (n)
301
+ results.push({
302
+ id: n.id,
303
+ name: n.name,
304
+ kind: n.kind,
305
+ file: n.file,
306
+ line: n.line,
307
+ end_line: n.end_line,
308
+ });
309
+ }
310
+ }
311
+ return results;
312
+ }
313
+
314
+ findCallers(nodeId) {
315
+ const results = [];
316
+ for (const e of this.#edges.values()) {
317
+ if (e.target_id === nodeId && e.kind === 'calls') {
318
+ const n = this.#nodes.get(e.source_id);
319
+ if (n) results.push({ id: n.id, name: n.name, kind: n.kind, file: n.file, line: n.line });
320
+ }
321
+ }
322
+ return results;
323
+ }
324
+
325
+ findDistinctCallers(nodeId) {
326
+ const seen = new Set();
327
+ const results = [];
328
+ for (const e of this.#edges.values()) {
329
+ if (e.target_id === nodeId && e.kind === 'calls' && !seen.has(e.source_id)) {
330
+ seen.add(e.source_id);
331
+ const n = this.#nodes.get(e.source_id);
332
+ if (n) results.push({ id: n.id, name: n.name, kind: n.kind, file: n.file, line: n.line });
333
+ }
334
+ }
335
+ return results;
336
+ }
337
+
338
+ findAllOutgoingEdges(nodeId) {
339
+ const results = [];
340
+ for (const e of this.#edges.values()) {
341
+ if (e.source_id === nodeId) {
342
+ const n = this.#nodes.get(e.target_id);
343
+ if (n)
344
+ results.push({
345
+ name: n.name,
346
+ kind: n.kind,
347
+ file: n.file,
348
+ line: n.line,
349
+ edge_kind: e.kind,
350
+ });
351
+ }
352
+ }
353
+ return results;
354
+ }
355
+
356
+ findAllIncomingEdges(nodeId) {
357
+ const results = [];
358
+ for (const e of this.#edges.values()) {
359
+ if (e.target_id === nodeId) {
360
+ const n = this.#nodes.get(e.source_id);
361
+ if (n)
362
+ results.push({
363
+ name: n.name,
364
+ kind: n.kind,
365
+ file: n.file,
366
+ line: n.line,
367
+ edge_kind: e.kind,
368
+ });
369
+ }
370
+ }
371
+ return results;
372
+ }
373
+
374
+ findCalleeNames(nodeId) {
375
+ const names = new Set();
376
+ for (const e of this.#edges.values()) {
377
+ if (e.source_id === nodeId && e.kind === 'calls') {
378
+ const n = this.#nodes.get(e.target_id);
379
+ if (n) names.add(n.name);
380
+ }
381
+ }
382
+ return [...names].sort();
383
+ }
384
+
385
+ findCallerNames(nodeId) {
386
+ const names = new Set();
387
+ for (const e of this.#edges.values()) {
388
+ if (e.target_id === nodeId && e.kind === 'calls') {
389
+ const n = this.#nodes.get(e.source_id);
390
+ if (n) names.add(n.name);
391
+ }
392
+ }
393
+ return [...names].sort();
394
+ }
395
+
396
+ findImportTargets(nodeId) {
397
+ const results = [];
398
+ for (const e of this.#edges.values()) {
399
+ if (e.source_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) {
400
+ const n = this.#nodes.get(e.target_id);
401
+ if (n) results.push({ file: n.file, edge_kind: e.kind });
402
+ }
403
+ }
404
+ return results;
405
+ }
406
+
407
+ findImportSources(nodeId) {
408
+ const results = [];
409
+ for (const e of this.#edges.values()) {
410
+ if (e.target_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) {
411
+ const n = this.#nodes.get(e.source_id);
412
+ if (n) results.push({ file: n.file, edge_kind: e.kind });
413
+ }
414
+ }
415
+ return results;
416
+ }
417
+
418
+ findImportDependents(nodeId) {
419
+ const results = [];
420
+ for (const e of this.#edges.values()) {
421
+ if (e.target_id === nodeId && (e.kind === 'imports' || e.kind === 'imports-type')) {
422
+ const n = this.#nodes.get(e.source_id);
423
+ if (n) results.push({ ...n });
424
+ }
425
+ }
426
+ return results;
427
+ }
428
+
429
+ findCrossFileCallTargets(file) {
430
+ const targets = new Set();
431
+ for (const e of this.#edges.values()) {
432
+ if (e.kind !== 'calls') continue;
433
+ const caller = this.#nodes.get(e.source_id);
434
+ const target = this.#nodes.get(e.target_id);
435
+ if (caller && target && target.file === file && caller.file !== file) {
436
+ targets.add(e.target_id);
437
+ }
438
+ }
439
+ return targets;
440
+ }
441
+
442
+ countCrossFileCallers(nodeId, file) {
443
+ let count = 0;
444
+ for (const e of this.#edges.values()) {
445
+ if (e.target_id === nodeId && e.kind === 'calls') {
446
+ const caller = this.#nodes.get(e.source_id);
447
+ if (caller && caller.file !== file) count++;
448
+ }
449
+ }
450
+ return count;
451
+ }
452
+
453
+ getClassHierarchy(classNodeId) {
454
+ const ancestors = new Set();
455
+ const queue = [classNodeId];
456
+ while (queue.length > 0) {
457
+ const current = queue.shift();
458
+ for (const e of this.#edges.values()) {
459
+ if (e.source_id === current && e.kind === 'extends') {
460
+ const target = this.#nodes.get(e.target_id);
461
+ if (target && !ancestors.has(target.id)) {
462
+ ancestors.add(target.id);
463
+ queue.push(target.id);
464
+ }
465
+ }
466
+ }
467
+ }
468
+ return ancestors;
469
+ }
470
+
471
+ findIntraFileCallEdges(file) {
472
+ const results = [];
473
+ for (const e of this.#edges.values()) {
474
+ if (e.kind !== 'calls') continue;
475
+ const caller = this.#nodes.get(e.source_id);
476
+ const callee = this.#nodes.get(e.target_id);
477
+ if (caller && callee && caller.file === file && callee.file === file) {
478
+ results.push({ caller_name: caller.name, callee_name: callee.name });
479
+ }
480
+ }
481
+ const lineByName = new Map();
482
+ for (const n of this.#nodes.values()) {
483
+ if (n.file === file) lineByName.set(n.name, n.line);
484
+ }
485
+ return results.sort((a, b) => {
486
+ return (lineByName.get(a.caller_name) ?? 0) - (lineByName.get(b.caller_name) ?? 0);
487
+ });
488
+ }
489
+
490
+ // ── Graph-read queries ────────────────────────────────────────────
491
+
492
+ getCallableNodes() {
493
+ return [...this.#nodes.values()]
494
+ .filter((n) => CORE_SYMBOL_KINDS.includes(n.kind))
495
+ .map((n) => ({ id: n.id, name: n.name, kind: n.kind, file: n.file }));
496
+ }
497
+
498
+ getCallEdges() {
499
+ return [...this.#edges.values()]
500
+ .filter((e) => e.kind === 'calls')
501
+ .map((e) => ({ source_id: e.source_id, target_id: e.target_id }));
502
+ }
503
+
504
+ getFileNodesAll() {
505
+ return [...this.#nodes.values()]
506
+ .filter((n) => n.kind === 'file')
507
+ .map((n) => ({ id: n.id, name: n.name, file: n.file }));
508
+ }
509
+
510
+ getImportEdges() {
511
+ return [...this.#edges.values()]
512
+ .filter((e) => e.kind === 'imports' || e.kind === 'imports-type')
513
+ .map((e) => ({ source_id: e.source_id, target_id: e.target_id }));
514
+ }
515
+
516
+ // ── Optional table checks ─────────────────────────────────────────
517
+
518
+ hasCfgTables() {
519
+ return false;
520
+ }
521
+
522
+ hasEmbeddings() {
523
+ return false;
524
+ }
525
+
526
+ hasDataflowTable() {
527
+ return false;
528
+ }
529
+
530
+ getComplexityForNode(nodeId) {
531
+ return this.#complexity.get(nodeId);
532
+ }
533
+
534
+ // ── Private helpers ───────────────────────────────────────────────
535
+
536
+ /** Compute fan-in (incoming 'calls' edge count) for all nodes. */
537
+ #computeFanIn() {
538
+ const fanIn = new Map();
539
+ for (const e of this.#edges.values()) {
540
+ if (e.kind === 'calls') {
541
+ fanIn.set(e.target_id, (fanIn.get(e.target_id) ?? 0) + 1);
542
+ }
543
+ }
544
+ return fanIn;
545
+ }
546
+
547
+ /** Internal generator for function/method/class listing with filters. */
548
+ *#iterateFunctionNodesImpl(opts = {}) {
549
+ let nodes = [...this.#nodes.values()].filter((n) =>
550
+ ['function', 'method', 'class'].includes(n.kind),
551
+ );
552
+
553
+ if (opts.file) {
554
+ const fileRe = likeToRegex(`%${escapeLike(opts.file)}%`);
555
+ nodes = nodes.filter((n) => fileRe.test(n.file));
556
+ }
557
+ if (opts.pattern) {
558
+ const patternRe = likeToRegex(`%${escapeLike(opts.pattern)}%`);
559
+ nodes = nodes.filter((n) => patternRe.test(n.name));
560
+ }
561
+ if (opts.noTests) {
562
+ nodes = nodes.filter(
563
+ (n) =>
564
+ !n.file.includes('.test.') &&
565
+ !n.file.includes('.spec.') &&
566
+ !n.file.includes('__test__') &&
567
+ !n.file.includes('__tests__') &&
568
+ !n.file.includes('.stories.'),
569
+ );
570
+ }
571
+
572
+ nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
573
+ for (const n of nodes) {
574
+ yield {
575
+ name: n.name,
576
+ kind: n.kind,
577
+ file: n.file,
578
+ line: n.line,
579
+ end_line: n.end_line,
580
+ role: n.role,
581
+ };
582
+ }
583
+ }
584
+ }
@@ -1,10 +1,10 @@
1
1
  // Barrel re-export for repository/ modules.
2
2
 
3
+ export { Repository } from './base.js';
3
4
  export { purgeFileData, purgeFilesData } from './build-stmts.js';
4
5
  export { cachedStmt } from './cached-stmt.js';
5
6
  export { deleteCfgForNode, getCfgBlocks, getCfgEdges, hasCfgTables } from './cfg.js';
6
7
  export { getCoChangeMeta, hasCoChanges, upsertCoChangeMeta } from './cochange.js';
7
-
8
8
  export { getComplexityForNode } from './complexity.js';
9
9
  export { hasDataflowTable } from './dataflow.js';
10
10
  export {
@@ -25,6 +25,7 @@ export {
25
25
  } from './edges.js';
26
26
  export { getEmbeddingCount, getEmbeddingMeta, hasEmbeddings } from './embeddings.js';
27
27
  export { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from './graph-read.js';
28
+ export { InMemoryRepository } from './in-memory-repository.js';
28
29
  export {
29
30
  bulkNodeIdsByFile,
30
31
  countEdges,
@@ -32,8 +33,10 @@ export {
32
33
  countNodes,
33
34
  findFileNodes,
34
35
  findNodeById,
36
+ findNodeByQualifiedName,
35
37
  findNodeChildren,
36
38
  findNodesByFile,
39
+ findNodesByScope,
37
40
  findNodesForTriage,
38
41
  findNodesWithFanIn,
39
42
  getFunctionNodeId,
@@ -41,3 +44,4 @@ export {
41
44
  iterateFunctionNodes,
42
45
  listFunctionNodes,
43
46
  } from './nodes.js';
47
+ export { SqliteRepository } from './sqlite-repository.js';
@@ -1,3 +1,4 @@
1
+ import { ConfigError } from '../../errors.js';
1
2
  import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
2
3
  import { NodeQuery } from '../query-builder.js';
3
4
  import { cachedStmt } from './cached-stmt.js';
@@ -37,10 +38,12 @@ export function findNodesWithFanIn(db, namePattern, opts = {}) {
37
38
  */
38
39
  export function findNodesForTriage(db, opts = {}) {
39
40
  if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
40
- throw new Error(`Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`);
41
+ throw new ConfigError(
42
+ `Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`,
43
+ );
41
44
  }
42
45
  if (opts.role && !VALID_ROLES.includes(opts.role)) {
43
- throw new Error(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
46
+ throw new ConfigError(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
44
47
  }
45
48
 
46
49
  const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
@@ -113,6 +116,7 @@ const _getNodeIdStmt = new WeakMap();
113
116
  const _getFunctionNodeIdStmt = new WeakMap();
114
117
  const _bulkNodeIdsByFileStmt = new WeakMap();
115
118
  const _findNodeChildrenStmt = new WeakMap();
119
+ const _findNodeByQualifiedNameStmt = new WeakMap();
116
120
 
117
121
  /**
118
122
  * Count total nodes.
@@ -236,12 +240,67 @@ export function bulkNodeIdsByFile(db, file) {
236
240
  * Find child nodes (parameters, properties, constants) of a parent.
237
241
  * @param {object} db
238
242
  * @param {number} parentId
239
- * @returns {{ name: string, kind: string, line: number, end_line: number|null }[]}
243
+ * @returns {{ name: string, kind: string, line: number, end_line: number|null, qualified_name: string|null, scope: string|null, visibility: string|null }[]}
240
244
  */
241
245
  export function findNodeChildren(db, parentId) {
242
246
  return cachedStmt(
243
247
  _findNodeChildrenStmt,
244
248
  db,
245
- 'SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line',
249
+ 'SELECT name, kind, line, end_line, qualified_name, scope, visibility FROM nodes WHERE parent_id = ? ORDER BY line',
246
250
  ).all(parentId);
247
251
  }
252
+
253
+ /** Escape LIKE wildcards in a literal string segment. */
254
+ function escapeLike(s) {
255
+ return s.replace(/[%_\\]/g, '\\$&');
256
+ }
257
+
258
+ /**
259
+ * Find all nodes that belong to a given scope (by scope column).
260
+ * Enables "all methods of class X" without traversing edges.
261
+ * @param {object} db
262
+ * @param {string} scopeName - The scope to search for (e.g., class name)
263
+ * @param {object} [opts]
264
+ * @param {string} [opts.kind] - Filter by node kind
265
+ * @param {string} [opts.file] - Filter by file path (LIKE match)
266
+ * @returns {object[]}
267
+ */
268
+ export function findNodesByScope(db, scopeName, opts = {}) {
269
+ let sql = 'SELECT * FROM nodes WHERE scope = ?';
270
+ const params = [scopeName];
271
+ if (opts.kind) {
272
+ sql += ' AND kind = ?';
273
+ params.push(opts.kind);
274
+ }
275
+ if (opts.file) {
276
+ sql += " AND file LIKE ? ESCAPE '\\'";
277
+ params.push(`%${escapeLike(opts.file)}%`);
278
+ }
279
+ sql += ' ORDER BY file, line';
280
+ return db.prepare(sql).all(...params);
281
+ }
282
+
283
+ /**
284
+ * Find nodes by qualified name. Returns all matches since the same
285
+ * qualified_name can exist in different files (e.g., two classes named
286
+ * `DateHelper.format` in separate modules). Pass `opts.file` to narrow.
287
+ * @param {object} db
288
+ * @param {string} qualifiedName - e.g., 'DateHelper.format'
289
+ * @param {object} [opts]
290
+ * @param {string} [opts.file] - Filter by file path (LIKE match)
291
+ * @returns {object[]}
292
+ */
293
+ export function findNodeByQualifiedName(db, qualifiedName, opts = {}) {
294
+ if (opts.file) {
295
+ return db
296
+ .prepare(
297
+ "SELECT * FROM nodes WHERE qualified_name = ? AND file LIKE ? ESCAPE '\\' ORDER BY file, line",
298
+ )
299
+ .all(qualifiedName, `%${escapeLike(opts.file)}%`);
300
+ }
301
+ return cachedStmt(
302
+ _findNodeByQualifiedNameStmt,
303
+ db,
304
+ 'SELECT * FROM nodes WHERE qualified_name = ? ORDER BY file, line',
305
+ ).all(qualifiedName);
306
+ }