@optave/codegraph 3.1.3 → 3.1.5

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