@optave/codegraph 3.1.4 → 3.2.0

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 (210) hide show
  1. package/README.md +29 -72
  2. package/package.json +10 -8
  3. package/src/ast-analysis/engine.js +260 -246
  4. package/src/ast-analysis/shared.js +2 -14
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +4 -7
  9. package/src/cli/commands/audit.js +11 -11
  10. package/src/cli/commands/batch.js +6 -5
  11. package/src/cli/commands/branch-compare.js +1 -1
  12. package/src/cli/commands/brief.js +12 -0
  13. package/src/cli/commands/build.js +1 -1
  14. package/src/cli/commands/cfg.js +5 -8
  15. package/src/cli/commands/check.js +28 -36
  16. package/src/cli/commands/children.js +9 -7
  17. package/src/cli/commands/co-change.js +5 -3
  18. package/src/cli/commands/communities.js +2 -6
  19. package/src/cli/commands/complexity.js +5 -3
  20. package/src/cli/commands/context.js +9 -8
  21. package/src/cli/commands/cycles.js +12 -8
  22. package/src/cli/commands/dataflow.js +5 -8
  23. package/src/cli/commands/deps.js +9 -8
  24. package/src/cli/commands/diff-impact.js +2 -6
  25. package/src/cli/commands/embed.js +1 -1
  26. package/src/cli/commands/export.js +34 -31
  27. package/src/cli/commands/exports.js +2 -6
  28. package/src/cli/commands/flow.js +5 -8
  29. package/src/cli/commands/fn-impact.js +9 -8
  30. package/src/cli/commands/impact.js +2 -6
  31. package/src/cli/commands/info.js +2 -2
  32. package/src/cli/commands/map.js +1 -1
  33. package/src/cli/commands/mcp.js +1 -1
  34. package/src/cli/commands/models.js +1 -1
  35. package/src/cli/commands/owners.js +5 -3
  36. package/src/cli/commands/path.js +2 -2
  37. package/src/cli/commands/plot.js +40 -31
  38. package/src/cli/commands/query.js +9 -8
  39. package/src/cli/commands/registry.js +2 -2
  40. package/src/cli/commands/roles.js +5 -8
  41. package/src/cli/commands/search.js +9 -3
  42. package/src/cli/commands/sequence.js +5 -8
  43. package/src/cli/commands/snapshot.js +6 -1
  44. package/src/cli/commands/stats.js +1 -1
  45. package/src/cli/commands/structure.js +5 -4
  46. package/src/cli/commands/triage.js +41 -30
  47. package/src/cli/commands/watch.js +1 -1
  48. package/src/cli/commands/where.js +2 -6
  49. package/src/cli/index.js +11 -5
  50. package/src/cli/shared/open-graph.js +13 -0
  51. package/src/cli/shared/options.js +22 -2
  52. package/src/cli.js +1 -1
  53. package/src/db/connection.js +140 -11
  54. package/src/{db.js → db/index.js} +12 -5
  55. package/src/db/migrations.js +42 -65
  56. package/src/db/query-builder.js +72 -9
  57. package/src/db/repository/base.js +1 -1
  58. package/src/db/repository/graph-read.js +3 -3
  59. package/src/db/repository/in-memory-repository.js +30 -28
  60. package/src/db/repository/nodes.js +10 -17
  61. package/src/domain/analysis/brief.js +155 -0
  62. package/src/domain/analysis/context.js +392 -0
  63. package/src/domain/analysis/dependencies.js +395 -0
  64. package/src/{analysis → domain/analysis}/exports.js +11 -6
  65. package/src/domain/analysis/impact.js +581 -0
  66. package/src/domain/analysis/module-map.js +348 -0
  67. package/src/{analysis → domain/analysis}/roles.js +12 -9
  68. package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
  69. package/src/{builder → domain/graph/builder}/helpers.js +4 -4
  70. package/src/{builder → domain/graph/builder}/incremental.js +119 -93
  71. package/src/domain/graph/builder/pipeline.js +156 -0
  72. package/src/domain/graph/builder/stages/build-edges.js +376 -0
  73. package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
  74. package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
  75. package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
  76. package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
  77. package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
  78. package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
  79. package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
  80. package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/{cycles.js → domain/graph/cycles.js} +4 -4
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +2 -2
  85. package/src/{watcher.js → domain/graph/watcher.js} +7 -7
  86. package/src/{parser.js → domain/parser.js} +24 -15
  87. package/src/{queries.js → domain/queries.js} +17 -16
  88. package/src/{embeddings → domain/search}/generator.js +3 -3
  89. package/src/{embeddings → domain/search}/models.js +2 -2
  90. package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
  91. package/src/{embeddings → domain/search}/search/filters.js +9 -5
  92. package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
  93. package/src/{embeddings → domain/search}/search/keyword.js +13 -6
  94. package/src/{embeddings → domain/search}/search/prepare.js +15 -7
  95. package/src/{embeddings → domain/search}/search/semantic.js +1 -1
  96. package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
  97. package/src/extractors/csharp.js +224 -207
  98. package/src/extractors/go.js +176 -172
  99. package/src/extractors/hcl.js +94 -78
  100. package/src/extractors/java.js +213 -207
  101. package/src/extractors/javascript.js +275 -305
  102. package/src/extractors/php.js +234 -221
  103. package/src/extractors/python.js +252 -250
  104. package/src/extractors/ruby.js +192 -185
  105. package/src/extractors/rust.js +182 -167
  106. package/src/{ast.js → features/ast.js} +13 -11
  107. package/src/{audit.js → features/audit.js} +20 -46
  108. package/src/{batch.js → features/batch.js} +5 -5
  109. package/src/{boundaries.js → features/boundaries.js} +100 -85
  110. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  111. package/src/{cfg.js → features/cfg.js} +141 -150
  112. package/src/{check.js → features/check.js} +13 -30
  113. package/src/{cochange.js → features/cochange.js} +5 -5
  114. package/src/{communities.js → features/communities.js} +72 -57
  115. package/src/{complexity.js → features/complexity.js} +154 -143
  116. package/src/{dataflow.js → features/dataflow.js} +155 -158
  117. package/src/{export.js → features/export.js} +6 -6
  118. package/src/{flow.js → features/flow.js} +4 -4
  119. package/src/{viewer.js → features/graph-enrichment.js} +8 -8
  120. package/src/{manifesto.js → features/manifesto.js} +15 -12
  121. package/src/{owners.js → features/owners.js} +6 -5
  122. package/src/features/sequence.js +300 -0
  123. package/src/features/shared/find-nodes.js +31 -0
  124. package/src/{snapshot.js → features/snapshot.js} +3 -3
  125. package/src/{structure.js → features/structure.js} +139 -108
  126. package/src/features/triage.js +141 -0
  127. package/src/graph/builders/dependency.js +33 -14
  128. package/src/graph/classifiers/risk.js +3 -2
  129. package/src/graph/classifiers/roles.js +6 -3
  130. package/src/index.cjs +16 -0
  131. package/src/index.js +40 -39
  132. package/src/{native.js → infrastructure/native.js} +1 -1
  133. package/src/mcp/middleware.js +1 -1
  134. package/src/mcp/server.js +68 -59
  135. package/src/mcp/tool-registry.js +15 -2
  136. package/src/mcp/tools/ast-query.js +1 -1
  137. package/src/mcp/tools/audit.js +1 -1
  138. package/src/mcp/tools/batch-query.js +1 -1
  139. package/src/mcp/tools/branch-compare.js +3 -1
  140. package/src/mcp/tools/brief.js +8 -0
  141. package/src/mcp/tools/cfg.js +1 -1
  142. package/src/mcp/tools/check.js +3 -3
  143. package/src/mcp/tools/co-changes.js +1 -1
  144. package/src/mcp/tools/code-owners.js +1 -1
  145. package/src/mcp/tools/communities.js +1 -1
  146. package/src/mcp/tools/complexity.js +1 -1
  147. package/src/mcp/tools/dataflow.js +2 -2
  148. package/src/mcp/tools/execution-flow.js +2 -2
  149. package/src/mcp/tools/export-graph.js +2 -2
  150. package/src/mcp/tools/find-cycles.js +2 -2
  151. package/src/mcp/tools/index.js +2 -0
  152. package/src/mcp/tools/list-repos.js +1 -1
  153. package/src/mcp/tools/sequence.js +1 -1
  154. package/src/mcp/tools/structure.js +1 -1
  155. package/src/mcp/tools/triage.js +2 -2
  156. package/src/{commands → presentation}/audit.js +2 -2
  157. package/src/{commands → presentation}/batch.js +1 -1
  158. package/src/{commands → presentation}/branch-compare.js +2 -2
  159. package/src/presentation/brief.js +51 -0
  160. package/src/{commands → presentation}/cfg.js +1 -1
  161. package/src/{commands → presentation}/check.js +2 -2
  162. package/src/{commands → presentation}/communities.js +1 -1
  163. package/src/{commands → presentation}/complexity.js +1 -1
  164. package/src/{commands → presentation}/dataflow.js +1 -1
  165. package/src/{commands → presentation}/flow.js +2 -2
  166. package/src/{commands → presentation}/manifesto.js +1 -1
  167. package/src/{commands → presentation}/owners.js +1 -1
  168. package/src/presentation/queries-cli/exports.js +53 -0
  169. package/src/presentation/queries-cli/impact.js +214 -0
  170. package/src/presentation/queries-cli/index.js +5 -0
  171. package/src/presentation/queries-cli/inspect.js +329 -0
  172. package/src/presentation/queries-cli/overview.js +196 -0
  173. package/src/presentation/queries-cli/path.js +65 -0
  174. package/src/presentation/queries-cli.js +27 -0
  175. package/src/{commands → presentation}/query.js +1 -1
  176. package/src/presentation/result-formatter.js +126 -3
  177. package/src/{commands → presentation}/sequence.js +2 -2
  178. package/src/{commands → presentation}/structure.js +1 -1
  179. package/src/presentation/table.js +0 -8
  180. package/src/{commands → presentation}/triage.js +1 -1
  181. package/src/{constants.js → shared/constants.js} +1 -1
  182. package/src/shared/file-utils.js +2 -2
  183. package/src/shared/generators.js +9 -5
  184. package/src/shared/hierarchy.js +1 -1
  185. package/src/{kinds.js → shared/kinds.js} +1 -1
  186. package/src/analysis/context.js +0 -408
  187. package/src/analysis/dependencies.js +0 -341
  188. package/src/analysis/impact.js +0 -463
  189. package/src/analysis/module-map.js +0 -322
  190. package/src/builder/pipeline.js +0 -130
  191. package/src/builder/stages/build-edges.js +0 -297
  192. package/src/builder/stages/insert-nodes.js +0 -195
  193. package/src/mcp.js +0 -2
  194. package/src/queries-cli.js +0 -866
  195. package/src/sequence.js +0 -289
  196. package/src/triage.js +0 -126
  197. /package/src/{builder → domain/graph/builder}/context.js +0 -0
  198. /package/src/{builder.js → domain/graph/builder.js} +0 -0
  199. /package/src/{embeddings → domain/search}/index.js +0 -0
  200. /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
  201. /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
  202. /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
  203. /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
  204. /package/src/{config.js → infrastructure/config.js} +0 -0
  205. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  206. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  207. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  208. /package/src/{commands → presentation}/cochange.js +0 -0
  209. /package/src/{errors.js → shared/errors.js} +0 -0
  210. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Stage: insertNodes
3
+ *
4
+ * Batch-inserts file nodes, definitions, exports, children, and contains/parameter_of edges.
5
+ * Updates file hashes for incremental builds.
6
+ */
7
+ import path from 'node:path';
8
+ import { performance } from 'node:perf_hooks';
9
+ import { bulkNodeIdsByFile } from '../../../../db/index.js';
10
+ import {
11
+ batchInsertEdges,
12
+ batchInsertNodes,
13
+ fileHash,
14
+ fileStat,
15
+ readFileSafe,
16
+ } from '../helpers.js';
17
+
18
+ // ── Phase 1: Insert file nodes, definitions, exports ────────────────────
19
+
20
+ function insertDefinitionsAndExports(db, allSymbols) {
21
+ const phase1Rows = [];
22
+ for (const [relPath, symbols] of allSymbols) {
23
+ phase1Rows.push([relPath, 'file', relPath, 0, null, null, null, null, null]);
24
+ for (const def of symbols.definitions) {
25
+ const dotIdx = def.name.lastIndexOf('.');
26
+ const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;
27
+ phase1Rows.push([
28
+ def.name,
29
+ def.kind,
30
+ relPath,
31
+ def.line,
32
+ def.endLine || null,
33
+ null,
34
+ def.name,
35
+ scope,
36
+ def.visibility || null,
37
+ ]);
38
+ }
39
+ for (const exp of symbols.exports) {
40
+ phase1Rows.push([exp.name, exp.kind, relPath, exp.line, null, null, exp.name, null, null]);
41
+ }
42
+ }
43
+ batchInsertNodes(db, phase1Rows);
44
+
45
+ // Mark exported symbols
46
+ const markExported = db.prepare(
47
+ 'UPDATE nodes SET exported = 1 WHERE name = ? AND kind = ? AND file = ? AND line = ?',
48
+ );
49
+ for (const [relPath, symbols] of allSymbols) {
50
+ for (const exp of symbols.exports) {
51
+ markExported.run(exp.name, exp.kind, relPath, exp.line);
52
+ }
53
+ }
54
+ }
55
+
56
+ // ── Phase 2: Insert children (needs parent IDs) ────────────────────────
57
+
58
+ function insertChildren(db, allSymbols) {
59
+ const childRows = [];
60
+ for (const [relPath, symbols] of allSymbols) {
61
+ const nodeIdMap = new Map();
62
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
63
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
64
+ }
65
+ for (const def of symbols.definitions) {
66
+ if (!def.children?.length) continue;
67
+ const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
68
+ if (!defId) continue;
69
+ for (const child of def.children) {
70
+ const qualifiedName = `${def.name}.${child.name}`;
71
+ childRows.push([
72
+ child.name,
73
+ child.kind,
74
+ relPath,
75
+ child.line,
76
+ child.endLine || null,
77
+ defId,
78
+ qualifiedName,
79
+ def.name,
80
+ child.visibility || null,
81
+ ]);
82
+ }
83
+ }
84
+ }
85
+ batchInsertNodes(db, childRows);
86
+ }
87
+
88
+ // ── Phase 3: Insert containment + parameter_of edges ────────────────────
89
+
90
+ function insertContainmentEdges(db, allSymbols) {
91
+ const edgeRows = [];
92
+ for (const [relPath, symbols] of allSymbols) {
93
+ const nodeIdMap = new Map();
94
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
95
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
96
+ }
97
+ const fileId = nodeIdMap.get(`${relPath}|file|0`);
98
+ for (const def of symbols.definitions) {
99
+ const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
100
+ if (fileId && defId) {
101
+ edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
102
+ }
103
+ if (def.children?.length && defId) {
104
+ for (const child of def.children) {
105
+ const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
106
+ if (childId) {
107
+ edgeRows.push([defId, childId, 'contains', 1.0, 0]);
108
+ if (child.kind === 'parameter') {
109
+ edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ batchInsertEdges(db, edgeRows);
117
+ }
118
+
119
+ // ── Phase 4: Update file hashes ─────────────────────────────────────────
120
+
121
+ function updateFileHashes(_db, allSymbols, precomputedData, metadataUpdates, rootDir, upsertHash) {
122
+ if (!upsertHash) return;
123
+
124
+ for (const [relPath] of allSymbols) {
125
+ const precomputed = precomputedData.get(relPath);
126
+ if (precomputed?._reverseDepOnly) {
127
+ // no-op: file unchanged, hash already correct
128
+ } else if (precomputed?.hash) {
129
+ const stat = precomputed.stat || fileStat(path.join(rootDir, relPath));
130
+ const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
131
+ const size = stat ? stat.size : 0;
132
+ upsertHash.run(relPath, precomputed.hash, mtime, size);
133
+ } else {
134
+ const absPath = path.join(rootDir, relPath);
135
+ let code;
136
+ try {
137
+ code = readFileSafe(absPath);
138
+ } catch {
139
+ code = null;
140
+ }
141
+ if (code !== null) {
142
+ const stat = fileStat(absPath);
143
+ const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
144
+ const size = stat ? stat.size : 0;
145
+ upsertHash.run(relPath, fileHash(code), mtime, size);
146
+ }
147
+ }
148
+ }
149
+
150
+ // Also update metadata-only entries (self-heal mtime/size without re-parse)
151
+ for (const item of metadataUpdates) {
152
+ const mtime = item.stat ? Math.floor(item.stat.mtimeMs) : 0;
153
+ const size = item.stat ? item.stat.size : 0;
154
+ upsertHash.run(item.relPath, item.hash, mtime, size);
155
+ }
156
+ }
157
+
158
+ // ── Main entry point ────────────────────────────────────────────────────
159
+
160
+ /**
161
+ * @param {import('../context.js').PipelineContext} ctx
162
+ */
163
+ export async function insertNodes(ctx) {
164
+ const { db, allSymbols, filesToParse, metadataUpdates, rootDir, removed } = ctx;
165
+
166
+ const precomputedData = new Map();
167
+ for (const item of filesToParse) {
168
+ if (item.relPath) precomputedData.set(item.relPath, item);
169
+ }
170
+
171
+ let upsertHash;
172
+ try {
173
+ upsertHash = db.prepare(
174
+ 'INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)',
175
+ );
176
+ } catch {
177
+ upsertHash = null;
178
+ }
179
+
180
+ // Populate fileSymbols before the transaction so it is a pure input
181
+ for (const [relPath, symbols] of allSymbols) {
182
+ ctx.fileSymbols.set(relPath, symbols);
183
+ }
184
+
185
+ const insertAll = db.transaction(() => {
186
+ insertDefinitionsAndExports(db, allSymbols);
187
+ insertChildren(db, allSymbols);
188
+ insertContainmentEdges(db, allSymbols);
189
+ updateFileHashes(db, allSymbols, precomputedData, metadataUpdates, rootDir, upsertHash);
190
+ });
191
+
192
+ const t0 = performance.now();
193
+ insertAll();
194
+ ctx.timing.insertMs = performance.now() - t0;
195
+
196
+ // Clean up removed file hashes
197
+ if (upsertHash && removed.length > 0) {
198
+ const deleteHash = db.prepare('DELETE FROM file_hashes WHERE file = ?');
199
+ for (const relPath of removed) {
200
+ deleteHash.run(relPath);
201
+ }
202
+ }
203
+ }
@@ -5,8 +5,8 @@
5
5
  * Populates ctx.allSymbols, ctx.fileSymbols, ctx.filesToParse.
6
6
  */
7
7
  import { performance } from 'node:perf_hooks';
8
- import { info } from '../../logger.js';
9
- import { parseFilesAuto } from '../../parser.js';
8
+ import { info } from '../../../../infrastructure/logger.js';
9
+ import { parseFilesAuto } from '../../../parser.js';
10
10
 
11
11
  /**
12
12
  * @param {import('../context.js').PipelineContext} ctx
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import path from 'node:path';
8
8
  import { performance } from 'node:perf_hooks';
9
- import { parseFilesAuto } from '../../parser.js';
9
+ import { parseFilesAuto } from '../../../parser.js';
10
10
  import { resolveImportPath, resolveImportsBatch } from '../../resolve.js';
11
11
 
12
12
  /**
@@ -4,7 +4,7 @@
4
4
  * Dispatches to the unified AST analysis engine (AST nodes, complexity, CFG, dataflow).
5
5
  * Filters out reverse-dep files for incremental builds.
6
6
  */
7
- import { debug, warn } from '../../logger.js';
7
+ import { debug, warn } from '../../../../infrastructure/logger.js';
8
8
 
9
9
  /**
10
10
  * @param {import('../context.js').PipelineContext} ctx
@@ -31,7 +31,7 @@ export async function runAnalyses(ctx) {
31
31
  }
32
32
  }
33
33
 
34
- const { runAnalyses: runAnalysesFn } = await import('../../ast-analysis/engine.js');
34
+ const { runAnalyses: runAnalysesFn } = await import('../../../../ast-analysis/engine.js');
35
35
  try {
36
36
  const analysisTiming = await runAnalysesFn(db, astComplexitySymbols, rootDir, opts, engineOpts);
37
37
  ctx.timing.astMs = analysisTiming.astMs;
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { debug, warn } from './logger.js';
3
+ import { debug, warn } from '../../infrastructure/logger.js';
4
4
 
5
5
  export const CHANGE_EVENTS_FILENAME = 'change-events.ndjson';
6
6
  export const DEFAULT_MAX_BYTES = 1024 * 1024; // 1 MB
@@ -1,7 +1,7 @@
1
- import { tarjan } from './graph/algorithms/tarjan.js';
2
- import { buildDependencyGraph } from './graph/builders/dependency.js';
3
- import { CodeGraph } from './graph/model.js';
4
- import { loadNative } from './native.js';
1
+ import { tarjan } from '../../graph/algorithms/tarjan.js';
2
+ import { buildDependencyGraph } from '../../graph/builders/dependency.js';
3
+ import { CodeGraph } from '../../graph/model.js';
4
+ import { loadNative } from '../../infrastructure/native.js';
5
5
 
6
6
  /**
7
7
  * Detect circular dependencies in the codebase using Tarjan's SCC algorithm.
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { debug, warn } from './logger.js';
3
+ import { debug, warn } from '../../infrastructure/logger.js';
4
4
 
5
5
  export const JOURNAL_FILENAME = 'changes.journal';
6
6
  const HEADER_PREFIX = '# codegraph-journal v1 ';
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { normalizePath } from './constants.js';
4
- import { loadNative } from './native.js';
3
+ import { loadNative } from '../../infrastructure/native.js';
4
+ import { normalizePath } from '../../shared/constants.js';
5
5
 
6
6
  // ── Alias format conversion ─────────────────────────────────────────
7
7
 
@@ -1,13 +1,13 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
4
+ import { info } from '../../infrastructure/logger.js';
5
+ import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../shared/constants.js';
6
+ import { DbError } from '../../shared/errors.js';
7
+ import { createParseTreeCache, getActiveEngine } from '../parser.js';
3
8
  import { rebuildFile } from './builder/incremental.js';
4
9
  import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
5
- import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
6
- import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from './db.js';
7
- import { DbError } from './errors.js';
8
10
  import { appendJournalEntries } from './journal.js';
9
- import { info } from './logger.js';
10
- import { createParseTreeCache, getActiveEngine } from './parser.js';
11
11
 
12
12
  function shouldIgnore(filePath) {
13
13
  const parts = filePath.split(path.sep);
@@ -57,10 +57,10 @@ export async function watchProject(rootDir, opts = {}) {
57
57
  countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
58
58
  countEdgesForFile: null,
59
59
  findNodeInFile: db.prepare(
60
- "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND file = ?",
60
+ "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
61
61
  ),
62
62
  findNodeByName: db.prepare(
63
- "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')",
63
+ "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')",
64
64
  ),
65
65
  listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
66
66
  };
@@ -2,8 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { Language, Parser, Query } from 'web-tree-sitter';
5
- import { warn } from './logger.js';
6
- import { getNative, getNativePackageVersion, loadNative } from './native.js';
5
+ import { debug, warn } from '../infrastructure/logger.js';
6
+ import { getNative, getNativePackageVersion, loadNative } from '../infrastructure/native.js';
7
7
 
8
8
  // Re-export all extractors for backward compatibility
9
9
  export {
@@ -16,7 +16,7 @@ export {
16
16
  extractRubySymbols,
17
17
  extractRustSymbols,
18
18
  extractSymbols,
19
- } from './extractors/index.js';
19
+ } from '../extractors/index.js';
20
20
 
21
21
  import {
22
22
  extractCSharpSymbols,
@@ -28,12 +28,12 @@ import {
28
28
  extractRubySymbols,
29
29
  extractRustSymbols,
30
30
  extractSymbols,
31
- } from './extractors/index.js';
31
+ } from '../extractors/index.js';
32
32
 
33
33
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
34
 
35
35
  function grammarPath(name) {
36
- return path.join(__dirname, '..', 'grammars', name);
36
+ return path.join(__dirname, '..', '..', 'grammars', name);
37
37
  }
38
38
 
39
39
  let _initialized = false;
@@ -116,29 +116,35 @@ export async function createParsers() {
116
116
  */
117
117
  export function disposeParsers() {
118
118
  if (_cachedParsers) {
119
- for (const [, parser] of _cachedParsers) {
119
+ for (const [id, parser] of _cachedParsers) {
120
120
  if (parser && typeof parser.delete === 'function') {
121
121
  try {
122
122
  parser.delete();
123
- } catch {}
123
+ } catch (e) {
124
+ debug(`Failed to dispose parser ${id}: ${e.message}`);
125
+ }
124
126
  }
125
127
  }
126
128
  _cachedParsers = null;
127
129
  }
128
- for (const [, query] of _queryCache) {
130
+ for (const [id, query] of _queryCache) {
129
131
  if (query && typeof query.delete === 'function') {
130
132
  try {
131
133
  query.delete();
132
- } catch {}
134
+ } catch (e) {
135
+ debug(`Failed to dispose query ${id}: ${e.message}`);
136
+ }
133
137
  }
134
138
  }
135
139
  _queryCache.clear();
136
140
  if (_cachedLanguages) {
137
- for (const [, lang] of _cachedLanguages) {
141
+ for (const [id, lang] of _cachedLanguages) {
138
142
  if (lang && typeof lang.delete === 'function') {
139
143
  try {
140
144
  lang.delete();
141
- } catch {}
145
+ } catch (e) {
146
+ debug(`Failed to dispose language ${id}: ${e.message}`);
147
+ }
142
148
  }
143
149
  }
144
150
  _cachedLanguages = null;
@@ -189,14 +195,15 @@ export async function ensureWasmTrees(fileSymbols, rootDir) {
189
195
  let code;
190
196
  try {
191
197
  code = fs.readFileSync(absPath, 'utf-8');
192
- } catch {
198
+ } catch (e) {
199
+ debug(`ensureWasmTrees: cannot read ${relPath}: ${e.message}`);
193
200
  continue;
194
201
  }
195
202
  try {
196
203
  symbols._tree = parser.parse(code);
197
204
  symbols._langId = entry.id;
198
- } catch {
199
- // skip files that fail to parse
205
+ } catch (e) {
206
+ debug(`ensureWasmTrees: parse failed for ${relPath}: ${e.message}`);
200
207
  }
201
208
  }
202
209
  }
@@ -483,7 +490,9 @@ export function getActiveEngine(opts = {}) {
483
490
  if (native) {
484
491
  try {
485
492
  version = getNativePackageVersion() ?? version;
486
- } catch {}
493
+ } catch (e) {
494
+ debug(`getNativePackageVersion failed: ${e.message}`);
495
+ }
487
496
  }
488
497
  return { name, version };
489
498
  }
@@ -6,6 +6,23 @@
6
6
  * importers continue to work without changes.
7
7
  */
8
8
 
9
+ // ── Re-export from dedicated module for backward compat ───────────────────
10
+ export { isTestFile, TEST_PATTERN } from '../infrastructure/test-filter.js';
11
+ export { iterListFunctions, iterRoles, iterWhere } from '../shared/generators.js';
12
+ // ── Kind/edge constants (canonical source: kinds.js) ─────────────────────
13
+ export {
14
+ ALL_SYMBOL_KINDS,
15
+ CORE_EDGE_KINDS,
16
+ CORE_SYMBOL_KINDS,
17
+ EVERY_EDGE_KIND,
18
+ EVERY_SYMBOL_KIND,
19
+ EXTENDED_SYMBOL_KINDS,
20
+ STRUCTURAL_EDGE_KINDS,
21
+ VALID_ROLES,
22
+ } from '../shared/kinds.js';
23
+ // ── Shared utilities ─────────────────────────────────────────────────────
24
+ export { kindIcon, normalizeSymbol } from '../shared/normalize.js';
25
+ export { briefData } from './analysis/brief.js';
9
26
  export { contextData, explainData } from './analysis/context.js';
10
27
  export { fileDepsData, fnDepsData, pathData } from './analysis/dependencies.js';
11
28
  export { exportsData } from './analysis/exports.js';
@@ -30,19 +47,3 @@ export {
30
47
  queryNameData,
31
48
  whereData,
32
49
  } from './analysis/symbol-lookup.js';
33
- // ── Re-export from dedicated module for backward compat ───────────────────
34
- export { isTestFile, TEST_PATTERN } from './infrastructure/test-filter.js';
35
- // ── Kind/edge constants (canonical source: kinds.js) ─────────────────────
36
- export {
37
- ALL_SYMBOL_KINDS,
38
- CORE_EDGE_KINDS,
39
- CORE_SYMBOL_KINDS,
40
- EVERY_EDGE_KIND,
41
- EVERY_SYMBOL_KIND,
42
- EXTENDED_SYMBOL_KINDS,
43
- STRUCTURAL_EDGE_KINDS,
44
- VALID_ROLES,
45
- } from './kinds.js';
46
- export { iterListFunctions, iterRoles, iterWhere } from './shared/generators.js';
47
- // ── Shared utilities ─────────────────────────────────────────────────────
48
- export { kindIcon, normalizeSymbol } from './shared/normalize.js';
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { closeDb, findDbPath, openDb } from '../db.js';
4
- import { DbError } from '../errors.js';
5
- import { warn } from '../logger.js';
3
+ import { closeDb, findDbPath, openDb } from '../../db/index.js';
4
+ import { warn } from '../../infrastructure/logger.js';
5
+ import { DbError } from '../../shared/errors.js';
6
6
  import { embed, getModelConfig } from './models.js';
7
7
  import { buildSourceText } from './strategies/source.js';
8
8
  import { buildStructuredText } from './strategies/structured.js';
@@ -1,7 +1,7 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
- import { ConfigError, EngineError } from '../errors.js';
4
- import { info } from '../logger.js';
3
+ import { info } from '../../infrastructure/logger.js';
4
+ import { ConfigError, EngineError } from '../../shared/errors.js';
5
5
 
6
6
  // Lazy-load transformers (heavy, optional module)
7
7
  let pipeline = null;
@@ -1,4 +1,4 @@
1
- import { warn } from '../../logger.js';
1
+ import { warn } from '../../../infrastructure/logger.js';
2
2
  import { hybridSearchData } from './hybrid.js';
3
3
  import { ftsSearchData } from './keyword.js';
4
4
  import { multiSearchData, searchData } from './semantic.js';
@@ -29,15 +29,19 @@ const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
29
29
  * @param {object} opts
30
30
  * @param {string} [opts.filePattern] - Glob pattern (only applied if it contains glob chars)
31
31
  * @param {boolean} [opts.noTests] - Exclude test/spec files
32
- * @param {boolean} [opts.isGlob] - Pre-computed: does filePattern contain glob chars?
33
32
  * @returns {Array}
34
33
  */
35
34
  export function applyFilters(rows, opts = {}) {
36
35
  let filtered = rows;
37
- const isGlob =
38
- opts.isGlob !== undefined ? opts.isGlob : opts.filePattern && /[*?[\]]/.test(opts.filePattern);
39
- if (isGlob) {
40
- filtered = filtered.filter((row) => globMatch(row.file, opts.filePattern));
36
+ const fp = opts.filePattern;
37
+ const fpArr = Array.isArray(fp) ? fp : fp ? [fp] : [];
38
+ if (fpArr.length > 0) {
39
+ filtered = filtered.filter((row) =>
40
+ fpArr.some((p) => {
41
+ const patternIsGlob = /[*?[\]]/.test(p);
42
+ return patternIsGlob ? globMatch(row.file, p) : row.file.includes(p);
43
+ }),
44
+ );
41
45
  }
42
46
  if (opts.noTests) {
43
47
  filtered = filtered.filter((row) => !TEST_PATTERN.test(row.file));
@@ -1,4 +1,4 @@
1
- import { openReadonlyOrFail } from '../../db.js';
1
+ import { openReadonlyOrFail } from '../../../db/index.js';
2
2
  import { hasFtsIndex } from '../stores/fts5.js';
3
3
  import { ftsSearchData } from './keyword.js';
4
4
  import { searchData } from './semantic.js';
@@ -1,4 +1,5 @@
1
- import { openReadonlyOrFail } from '../../db.js';
1
+ import { openReadonlyOrFail } from '../../../db/index.js';
2
+ import { buildFileConditionSQL } from '../../../db/query-builder.js';
2
3
  import { normalizeSymbol } from '../../queries.js';
3
4
  import { hasFtsIndex, sanitizeFtsQuery } from '../stores/fts5.js';
4
5
  import { applyFilters } from './filters.js';
@@ -36,10 +37,16 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
36
37
  params.push(opts.kind);
37
38
  }
38
39
 
39
- const isGlob = opts.filePattern && /[*?[\]]/.test(opts.filePattern);
40
- if (opts.filePattern && !isGlob) {
41
- sql += ' AND n.file LIKE ?';
42
- params.push(`%${opts.filePattern}%`);
40
+ const fp = opts.filePattern;
41
+ const fpArr = Array.isArray(fp) ? fp : fp ? [fp] : [];
42
+ const isGlob = fpArr.length > 0 && fpArr.some((p) => /[*?[\]]/.test(p));
43
+ // For non-glob patterns, push filtering into SQL via buildFileConditionSQL
44
+ // (handles escapeLike + ESCAPE clause). Glob patterns are handled post-query
45
+ // by applyFilters.
46
+ if (fpArr.length > 0 && !isGlob) {
47
+ const fc = buildFileConditionSQL(fpArr, 'n.file');
48
+ sql += fc.sql;
49
+ params.push(...fc.params);
43
50
  }
44
51
 
45
52
  sql += ' ORDER BY rank LIMIT ?';
@@ -53,7 +60,7 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
53
60
  return { results: [] };
54
61
  }
55
62
 
56
- rows = applyFilters(rows, { ...opts, isGlob });
63
+ rows = applyFilters(rows, opts);
57
64
 
58
65
  const hc = new Map();
59
66
  const results = rows.slice(0, limit).map((row) => ({
@@ -1,5 +1,6 @@
1
- import { getEmbeddingCount, getEmbeddingMeta } from '../../db/repository/embeddings.js';
2
- import { openReadonlyOrFail } from '../../db.js';
1
+ import { openReadonlyOrFail } from '../../../db/index.js';
2
+ import { escapeLike } from '../../../db/query-builder.js';
3
+ import { getEmbeddingCount, getEmbeddingMeta } from '../../../db/repository/embeddings.js';
3
4
  import { MODELS } from '../models.js';
4
5
  import { applyFilters } from './filters.js';
5
6
 
@@ -35,7 +36,9 @@ export function prepareSearch(customDbPath, opts = {}) {
35
36
  }
36
37
 
37
38
  // Pre-filter: allow filtering by kind or file pattern to reduce search space
38
- const isGlob = opts.filePattern && /[*?[\]]/.test(opts.filePattern);
39
+ const fp = opts.filePattern;
40
+ const fpArr = Array.isArray(fp) ? fp : fp ? [fp] : [];
41
+ const isGlob = fpArr.length > 0 && fpArr.some((p) => /[*?[\]]/.test(p));
39
42
  let sql = `
40
43
  SELECT e.node_id, e.vector, e.text_preview, n.name, n.kind, n.file, n.line, n.end_line, n.role
41
44
  FROM embeddings e
@@ -47,16 +50,21 @@ export function prepareSearch(customDbPath, opts = {}) {
47
50
  conditions.push('n.kind = ?');
48
51
  params.push(opts.kind);
49
52
  }
50
- if (opts.filePattern && !isGlob) {
51
- conditions.push('n.file LIKE ?');
52
- params.push(`%${opts.filePattern}%`);
53
+ if (fpArr.length > 0 && !isGlob) {
54
+ if (fpArr.length === 1) {
55
+ conditions.push("n.file LIKE ? ESCAPE '\\'");
56
+ params.push(`%${escapeLike(fpArr[0])}%`);
57
+ } else {
58
+ conditions.push(`(${fpArr.map(() => "n.file LIKE ? ESCAPE '\\'").join(' OR ')})`);
59
+ params.push(...fpArr.map((f) => `%${escapeLike(f)}%`));
60
+ }
53
61
  }
54
62
  if (conditions.length > 0) {
55
63
  sql += ` WHERE ${conditions.join(' AND ')}`;
56
64
  }
57
65
 
58
66
  let rows = db.prepare(sql).all(...params);
59
- rows = applyFilters(rows, { ...opts, isGlob });
67
+ rows = applyFilters(rows, opts);
60
68
 
61
69
  return { db, rows, modelKey, storedDim };
62
70
  } catch (err) {
@@ -1,4 +1,4 @@
1
- import { warn } from '../../logger.js';
1
+ import { warn } from '../../../infrastructure/logger.js';
2
2
  import { normalizeSymbol } from '../../queries.js';
3
3
  import { embed } from '../models.js';
4
4
  import { cosineSim } from '../stores/sqlite-blob.js';
@@ -1,4 +1,4 @@
1
- import { findCalleeNames, findCallerNames } from '../../db.js';
1
+ import { findCalleeNames, findCallerNames } from '../../../db/index.js';
2
2
  import { extractLeadingComment, splitIdentifier } from './text-utils.js';
3
3
 
4
4
  /**