@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
@@ -6,19 +6,20 @@
6
6
  */
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
- import { normalizePath } from '../../constants.js';
10
- import { closeDb } from '../../db.js';
9
+ import { closeDb } from '../../../../db/index.js';
10
+ import { debug, info } from '../../../../infrastructure/logger.js';
11
+ import { normalizePath } from '../../../../shared/constants.js';
12
+ import { parseFilesAuto } from '../../../parser.js';
11
13
  import { readJournal, writeJournalHeader } from '../../journal.js';
12
- import { debug, info } from '../../logger.js';
13
- import { parseFilesAuto } from '../../parser.js';
14
14
  import { fileHash, fileStat, purgeFilesFromGraph, readFileSafe } from '../helpers.js';
15
15
 
16
+ // ── Three-tier change detection ─────────────────────────────────────────
17
+
16
18
  /**
17
19
  * Determine which files have changed since last build.
18
- * Three-tier cascade:
19
- * Tier 0Journal: O(changed) when watcher was running
20
- * Tier 1mtime+size: O(n) stats, O(changed) reads
21
- * Tier 2 — Hash comparison: O(changed) reads (fallback from Tier 1)
20
+ * Tier 0 — Journal: O(changed) when watcher was running
21
+ * Tier 1mtime+size: O(n) stats, O(changed) reads
22
+ * Tier 2Hash comparison: O(changed) reads (fallback from Tier 1)
22
23
  */
23
24
  function getChangedFiles(db, allFiles, rootDir) {
24
25
  let hasTable = false;
@@ -44,6 +45,17 @@ function getChangedFiles(db, allFiles, rootDir) {
44
45
  .map((r) => [r.file, r]),
45
46
  );
46
47
 
48
+ const removed = detectRemovedFiles(existing, allFiles, rootDir);
49
+
50
+ // Tier 0: Journal
51
+ const journalResult = tryJournalTier(db, existing, rootDir, removed);
52
+ if (journalResult) return journalResult;
53
+
54
+ // Tier 1 + 2: mtime/size fast-path → hash comparison
55
+ return mtimeAndHashTiers(existing, allFiles, rootDir, removed);
56
+ }
57
+
58
+ function detectRemovedFiles(existing, allFiles, rootDir) {
47
59
  const currentFiles = new Set();
48
60
  for (const file of allFiles) {
49
61
  currentFiles.add(normalizePath(path.relative(rootDir, file)));
@@ -55,51 +67,57 @@ function getChangedFiles(db, allFiles, rootDir) {
55
67
  removed.push(existingFile);
56
68
  }
57
69
  }
70
+ return removed;
71
+ }
58
72
 
59
- // ── Tier 0: Journal ──────────────────────────────────────────────
73
+ function tryJournalTier(db, existing, rootDir, removed) {
60
74
  const journal = readJournal(rootDir);
61
- if (journal.valid) {
62
- const dbMtimes = db.prepare('SELECT MAX(mtime) as latest FROM file_hashes').get();
63
- const latestDbMtime = dbMtimes?.latest || 0;
64
- const hasJournalEntries = journal.changed.length > 0 || journal.removed.length > 0;
65
-
66
- if (hasJournalEntries && journal.timestamp >= latestDbMtime) {
67
- debug(
68
- `Tier 0: journal valid, ${journal.changed.length} changed, ${journal.removed.length} removed`,
69
- );
70
- const changed = [];
71
-
72
- for (const relPath of journal.changed) {
73
- const absPath = path.join(rootDir, relPath);
74
- const stat = fileStat(absPath);
75
- if (!stat) continue;
76
-
77
- let content;
78
- try {
79
- content = readFileSafe(absPath);
80
- } catch {
81
- continue;
82
- }
83
- const hash = fileHash(content);
84
- const record = existing.get(relPath);
85
- if (!record || record.hash !== hash) {
86
- changed.push({ file: absPath, content, hash, relPath, stat });
87
- }
88
- }
75
+ if (!journal.valid) return null;
89
76
 
90
- const removedSet = new Set(removed);
91
- for (const relPath of journal.removed) {
92
- if (existing.has(relPath)) removedSet.add(relPath);
93
- }
77
+ const dbMtimes = db.prepare('SELECT MAX(mtime) as latest FROM file_hashes').get();
78
+ const latestDbMtime = dbMtimes?.latest || 0;
79
+ const hasJournalEntries = journal.changed.length > 0 || journal.removed.length > 0;
94
80
 
95
- return { changed, removed: [...removedSet], isFullBuild: false };
96
- }
81
+ if (!hasJournalEntries || journal.timestamp < latestDbMtime) {
97
82
  debug(
98
83
  `Tier 0: skipped (${hasJournalEntries ? 'timestamp stale' : 'no entries'}), falling to Tier 1`,
99
84
  );
85
+ return null;
86
+ }
87
+
88
+ debug(
89
+ `Tier 0: journal valid, ${journal.changed.length} changed, ${journal.removed.length} removed`,
90
+ );
91
+ const changed = [];
92
+
93
+ for (const relPath of journal.changed) {
94
+ const absPath = path.join(rootDir, relPath);
95
+ const stat = fileStat(absPath);
96
+ if (!stat) continue;
97
+
98
+ let content;
99
+ try {
100
+ content = readFileSafe(absPath);
101
+ } catch {
102
+ continue;
103
+ }
104
+ const hash = fileHash(content);
105
+ const record = existing.get(relPath);
106
+ if (!record || record.hash !== hash) {
107
+ changed.push({ file: absPath, content, hash, relPath, stat });
108
+ }
109
+ }
110
+
111
+ const removedSet = new Set(removed);
112
+ for (const relPath of journal.removed) {
113
+ if (existing.has(relPath)) removedSet.add(relPath);
100
114
  }
101
115
 
102
- // ── Tier 1: mtime+size fast-path ─────────────────────────────────
116
+ return { changed, removed: [...removedSet], isFullBuild: false };
117
+ }
118
+
119
+ function mtimeAndHashTiers(existing, allFiles, rootDir, removed) {
120
+ // Tier 1: mtime+size fast-path
103
121
  const needsHash = [];
104
122
  const skipped = [];
105
123
 
@@ -130,7 +148,7 @@ function getChangedFiles(db, allFiles, rootDir) {
130
148
  debug(`Tier 1: ${skipped.length} skipped by mtime+size, ${needsHash.length} need hash check`);
131
149
  }
132
150
 
133
- // ── Tier 2: Hash comparison ──────────────────────────────────────
151
+ // Tier 2: Hash comparison
134
152
  const changed = [];
135
153
 
136
154
  for (const item of needsHash) {
@@ -168,9 +186,10 @@ function getChangedFiles(db, allFiles, rootDir) {
168
186
  return { changed, removed, isFullBuild: false };
169
187
  }
170
188
 
189
+ // ── Pending analysis ────────────────────────────────────────────────────
190
+
171
191
  /**
172
192
  * Run pending analysis pass when no file changes but analysis tables are empty.
173
- * @returns {boolean} true if analysis was run and we should early-exit
174
193
  */
175
194
  async function runPendingAnalysis(ctx) {
176
195
  const { db, opts, engineOpts, allFiles, rootDir } = ctx;
@@ -203,19 +222,18 @@ async function runPendingAnalysis(ctx) {
203
222
  };
204
223
  const analysisSymbols = await parseFilesAuto(allFiles, rootDir, analysisOpts);
205
224
  if (needsCfg) {
206
- const { buildCFGData } = await import('../../cfg.js');
225
+ const { buildCFGData } = await import('../../../../features/cfg.js');
207
226
  await buildCFGData(db, analysisSymbols, rootDir, engineOpts);
208
227
  }
209
228
  if (needsDataflow) {
210
- const { buildDataflowEdges } = await import('../../dataflow.js');
229
+ const { buildDataflowEdges } = await import('../../../../features/dataflow.js');
211
230
  await buildDataflowEdges(db, analysisSymbols, rootDir, engineOpts);
212
231
  }
213
232
  return true;
214
233
  }
215
234
 
216
- /**
217
- * Self-heal metadata-only updates (mtime/size) without re-parsing.
218
- */
235
+ // ── Metadata self-heal ──────────────────────────────────────────────────
236
+
219
237
  function healMetadata(ctx) {
220
238
  const { db, metadataUpdates } = ctx;
221
239
  if (!metadataUpdates || metadataUpdates.length === 0) return;
@@ -237,126 +255,111 @@ function healMetadata(ctx) {
237
255
  }
238
256
  }
239
257
 
240
- /**
241
- * @param {import('../context.js').PipelineContext} ctx
242
- */
243
- export async function detectChanges(ctx) {
244
- const { db, allFiles, rootDir, incremental, forceFullRebuild, opts } = ctx;
245
-
246
- // Scoped builds already set parseChanges in collectFiles.
247
- // Still need to purge removed files and set hasEmbeddings.
248
- if (opts.scope) {
249
- let hasEmbeddings = false;
250
- try {
251
- db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
252
- hasEmbeddings = true;
253
- } catch {
254
- /* table doesn't exist */
255
- }
256
- ctx.hasEmbeddings = hasEmbeddings;
258
+ // ── Reverse-dependency cascade ──────────────────────────────────────────
257
259
 
258
- // Reverse-dependency cascade BEFORE purging (needs existing edges to find importers)
259
- const changePaths = ctx.parseChanges.map(
260
- (item) => item.relPath || normalizePath(path.relative(rootDir, item.file)),
261
- );
262
- const reverseDeps = new Set();
263
- if (!opts.noReverseDeps) {
264
- const changedRelPaths = new Set([...changePaths, ...ctx.removed]);
265
- if (changedRelPaths.size > 0) {
266
- const findReverseDeps = db.prepare(`
267
- SELECT DISTINCT n_src.file FROM edges e
268
- JOIN nodes n_src ON e.source_id = n_src.id
269
- JOIN nodes n_tgt ON e.target_id = n_tgt.id
270
- WHERE n_tgt.file = ? AND n_src.file != n_tgt.file AND n_src.kind != 'directory'
271
- `);
272
- for (const relPath of changedRelPaths) {
273
- for (const row of findReverseDeps.all(relPath)) {
274
- if (!changedRelPaths.has(row.file) && !reverseDeps.has(row.file)) {
275
- const absPath = path.join(rootDir, row.file);
276
- if (fs.existsSync(absPath)) {
277
- reverseDeps.add(row.file);
278
- }
279
- }
280
- }
260
+ function findReverseDependencies(db, changedRelPaths, rootDir) {
261
+ const reverseDeps = new Set();
262
+ if (changedRelPaths.size === 0) return reverseDeps;
263
+
264
+ const findReverseDepsStmt = db.prepare(`
265
+ SELECT DISTINCT n_src.file FROM edges e
266
+ JOIN nodes n_src ON e.source_id = n_src.id
267
+ JOIN nodes n_tgt ON e.target_id = n_tgt.id
268
+ WHERE n_tgt.file = ? AND n_src.file != n_tgt.file AND n_src.kind != 'directory'
269
+ `);
270
+ for (const relPath of changedRelPaths) {
271
+ for (const row of findReverseDepsStmt.all(relPath)) {
272
+ if (!changedRelPaths.has(row.file) && !reverseDeps.has(row.file)) {
273
+ const absPath = path.join(rootDir, row.file);
274
+ if (fs.existsSync(absPath)) {
275
+ reverseDeps.add(row.file);
281
276
  }
282
277
  }
283
278
  }
284
-
285
- // Now purge changed + removed files
286
- if (changePaths.length > 0 || ctx.removed.length > 0) {
287
- purgeFilesFromGraph(db, [...ctx.removed, ...changePaths], { purgeHashes: false });
288
- }
289
-
290
- // Delete outgoing edges for reverse-dep files and add to parse list
291
- if (reverseDeps.size > 0) {
292
- const deleteOutgoingEdgesForFile = db.prepare(
293
- 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
294
- );
295
- for (const relPath of reverseDeps) {
296
- deleteOutgoingEdgesForFile.run(relPath);
297
- }
298
- for (const relPath of reverseDeps) {
299
- const absPath = path.join(rootDir, relPath);
300
- ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
301
- }
302
- info(
303
- `Scoped rebuild: ${changePaths.length} changed, ${ctx.removed.length} removed, ${reverseDeps.size} reverse-deps`,
304
- );
305
- }
306
- return;
307
279
  }
280
+ return reverseDeps;
281
+ }
308
282
 
309
- const increResult =
310
- incremental && !forceFullRebuild
311
- ? getChangedFiles(db, allFiles, rootDir)
312
- : { changed: allFiles.map((f) => ({ file: f })), removed: [], isFullBuild: true };
283
+ function purgeAndAddReverseDeps(ctx, changePaths, reverseDeps) {
284
+ const { db, rootDir } = ctx;
313
285
 
314
- ctx.removed = increResult.removed;
315
- ctx.isFullBuild = increResult.isFullBuild;
316
- ctx.parseChanges = increResult.changed.filter((c) => !c.metadataOnly);
317
- ctx.metadataUpdates = increResult.changed.filter((c) => c.metadataOnly);
286
+ if (changePaths.length > 0 || ctx.removed.length > 0) {
287
+ purgeFilesFromGraph(db, [...ctx.removed, ...changePaths], { purgeHashes: false });
288
+ }
318
289
 
319
- // Early exit: no changes detected
320
- if (!ctx.isFullBuild && ctx.parseChanges.length === 0 && ctx.removed.length === 0) {
321
- const ranAnalysis = await runPendingAnalysis(ctx);
322
- if (ranAnalysis) {
323
- closeDb(db);
324
- writeJournalHeader(rootDir, Date.now());
325
- ctx.earlyExit = true;
326
- return;
290
+ if (reverseDeps.size > 0) {
291
+ const deleteOutgoingEdgesForFile = db.prepare(
292
+ 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
293
+ );
294
+ for (const relPath of reverseDeps) {
295
+ deleteOutgoingEdgesForFile.run(relPath);
296
+ }
297
+ for (const relPath of reverseDeps) {
298
+ const absPath = path.join(rootDir, relPath);
299
+ ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
327
300
  }
328
-
329
- healMetadata(ctx);
330
- info('No changes detected. Graph is up to date.');
331
- closeDb(db);
332
- writeJournalHeader(rootDir, Date.now());
333
- ctx.earlyExit = true;
334
- return;
335
301
  }
302
+ }
336
303
 
337
- // ── Full build: truncate all tables ──────────────────────────────
338
- let hasEmbeddings = false;
304
+ // ── Shared helpers ───────────────────────────────────────────────────────
305
+
306
+ function detectHasEmbeddings(db) {
339
307
  try {
340
308
  db.prepare('SELECT 1 FROM embeddings LIMIT 1').get();
341
- hasEmbeddings = true;
309
+ return true;
342
310
  } catch {
343
- /* table doesn't exist */
311
+ return false;
344
312
  }
345
- ctx.hasEmbeddings = hasEmbeddings;
313
+ }
346
314
 
347
- if (ctx.isFullBuild) {
348
- const deletions =
349
- 'PRAGMA foreign_keys = OFF; DELETE FROM cfg_edges; DELETE FROM cfg_blocks; DELETE FROM node_metrics; DELETE FROM edges; DELETE FROM function_complexity; DELETE FROM dataflow; DELETE FROM ast_nodes; DELETE FROM nodes; PRAGMA foreign_keys = ON;';
350
- db.exec(
351
- hasEmbeddings
352
- ? `${deletions.replace('PRAGMA foreign_keys = ON;', '')} DELETE FROM embeddings; PRAGMA foreign_keys = ON;`
353
- : deletions,
354
- );
355
- return;
315
+ // ── Scoped build path ───────────────────────────────────────────────────
316
+
317
+ function handleScopedBuild(ctx) {
318
+ const { db, rootDir, opts } = ctx;
319
+
320
+ ctx.hasEmbeddings = detectHasEmbeddings(db);
321
+
322
+ const changePaths = ctx.parseChanges.map(
323
+ (item) => item.relPath || normalizePath(path.relative(rootDir, item.file)),
324
+ );
325
+
326
+ let reverseDeps = new Set();
327
+ if (!opts.noReverseDeps) {
328
+ const changedRelPaths = new Set([...changePaths, ...ctx.removed]);
329
+ reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir);
356
330
  }
357
331
 
358
- // ── Reverse-dependency cascade (incremental) ─────────────────────
359
- const reverseDeps = new Set();
332
+ // Purge changed + removed files, then add reverse-deps
333
+ purgeAndAddReverseDeps(ctx, changePaths, reverseDeps);
334
+
335
+ info(
336
+ `Scoped rebuild: ${changePaths.length} changed, ${ctx.removed.length} removed, ${reverseDeps.size} reverse-deps`,
337
+ );
338
+ }
339
+
340
+ // ── Full/incremental build path ─────────────────────────────────────────
341
+
342
+ function handleFullBuild(ctx) {
343
+ const { db } = ctx;
344
+
345
+ const hasEmbeddings = detectHasEmbeddings(db);
346
+ ctx.hasEmbeddings = hasEmbeddings;
347
+
348
+ const deletions =
349
+ 'PRAGMA foreign_keys = OFF; DELETE FROM cfg_edges; DELETE FROM cfg_blocks; DELETE FROM node_metrics; DELETE FROM edges; DELETE FROM function_complexity; DELETE FROM dataflow; DELETE FROM ast_nodes; DELETE FROM nodes; PRAGMA foreign_keys = ON;';
350
+ db.exec(
351
+ hasEmbeddings
352
+ ? `${deletions.replace('PRAGMA foreign_keys = ON;', '')} DELETE FROM embeddings; PRAGMA foreign_keys = ON;`
353
+ : deletions,
354
+ );
355
+ }
356
+
357
+ function handleIncrementalBuild(ctx) {
358
+ const { db, rootDir, opts } = ctx;
359
+
360
+ ctx.hasEmbeddings = detectHasEmbeddings(db);
361
+
362
+ let reverseDeps = new Set();
360
363
  if (!opts.noReverseDeps) {
361
364
  const changedRelPaths = new Set();
362
365
  for (const item of ctx.parseChanges) {
@@ -365,25 +368,7 @@ export async function detectChanges(ctx) {
365
368
  for (const relPath of ctx.removed) {
366
369
  changedRelPaths.add(relPath);
367
370
  }
368
-
369
- if (changedRelPaths.size > 0) {
370
- const findReverseDeps = db.prepare(`
371
- SELECT DISTINCT n_src.file FROM edges e
372
- JOIN nodes n_src ON e.source_id = n_src.id
373
- JOIN nodes n_tgt ON e.target_id = n_tgt.id
374
- WHERE n_tgt.file = ? AND n_src.file != n_tgt.file AND n_src.kind != 'directory'
375
- `);
376
- for (const relPath of changedRelPaths) {
377
- for (const row of findReverseDeps.all(relPath)) {
378
- if (!changedRelPaths.has(row.file) && !reverseDeps.has(row.file)) {
379
- const absPath = path.join(rootDir, row.file);
380
- if (fs.existsSync(absPath)) {
381
- reverseDeps.add(row.file);
382
- }
383
- }
384
- }
385
- }
386
- }
371
+ reverseDeps = findReverseDependencies(db, changedRelPaths, rootDir);
387
372
  }
388
373
 
389
374
  info(
@@ -393,21 +378,57 @@ export async function detectChanges(ctx) {
393
378
  debug(`Changed files: ${ctx.parseChanges.map((c) => c.relPath).join(', ')}`);
394
379
  if (ctx.removed.length > 0) debug(`Removed files: ${ctx.removed.join(', ')}`);
395
380
 
396
- // Purge changed and removed files
397
381
  const changePaths = ctx.parseChanges.map(
398
382
  (item) => item.relPath || normalizePath(path.relative(rootDir, item.file)),
399
383
  );
400
- purgeFilesFromGraph(db, [...ctx.removed, ...changePaths], { purgeHashes: false });
384
+ purgeAndAddReverseDeps(ctx, changePaths, reverseDeps);
385
+ }
401
386
 
402
- // Delete outgoing edges for reverse-dep files, then add them to parse list
403
- const deleteOutgoingEdgesForFile = db.prepare(
404
- 'DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)',
405
- );
406
- for (const relPath of reverseDeps) {
407
- deleteOutgoingEdgesForFile.run(relPath);
387
+ // ── Main entry point ────────────────────────────────────────────────────
388
+
389
+ /**
390
+ * @param {import('../context.js').PipelineContext} ctx
391
+ */
392
+ export async function detectChanges(ctx) {
393
+ const { db, allFiles, rootDir, incremental, forceFullRebuild, opts } = ctx;
394
+
395
+ // Scoped builds already set parseChanges in collectFiles
396
+ if (opts.scope) {
397
+ handleScopedBuild(ctx);
398
+ return;
408
399
  }
409
- for (const relPath of reverseDeps) {
410
- const absPath = path.join(rootDir, relPath);
411
- ctx.parseChanges.push({ file: absPath, relPath, _reverseDepOnly: true });
400
+
401
+ const increResult =
402
+ incremental && !forceFullRebuild
403
+ ? getChangedFiles(db, allFiles, rootDir)
404
+ : { changed: allFiles.map((f) => ({ file: f })), removed: [], isFullBuild: true };
405
+
406
+ ctx.removed = increResult.removed;
407
+ ctx.isFullBuild = increResult.isFullBuild;
408
+ ctx.parseChanges = increResult.changed.filter((c) => !c.metadataOnly);
409
+ ctx.metadataUpdates = increResult.changed.filter((c) => c.metadataOnly);
410
+
411
+ // Early exit: no changes detected
412
+ if (!ctx.isFullBuild && ctx.parseChanges.length === 0 && ctx.removed.length === 0) {
413
+ const ranAnalysis = await runPendingAnalysis(ctx);
414
+ if (ranAnalysis) {
415
+ closeDb(db);
416
+ writeJournalHeader(rootDir, Date.now());
417
+ ctx.earlyExit = true;
418
+ return;
419
+ }
420
+
421
+ healMetadata(ctx);
422
+ info('No changes detected. Graph is up to date.');
423
+ closeDb(db);
424
+ writeJournalHeader(rootDir, Date.now());
425
+ ctx.earlyExit = true;
426
+ return;
427
+ }
428
+
429
+ if (ctx.isFullBuild) {
430
+ handleFullBuild(ctx);
431
+ } else {
432
+ handleIncrementalBuild(ctx);
412
433
  }
413
434
  }
@@ -6,13 +6,13 @@
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { performance } from 'node:perf_hooks';
9
- import { closeDb, getBuildMeta, setBuildMeta } from '../../db.js';
9
+ import { closeDb, getBuildMeta, setBuildMeta } from '../../../../db/index.js';
10
+ import { debug, info, warn } from '../../../../infrastructure/logger.js';
10
11
  import { writeJournalHeader } from '../../journal.js';
11
- import { debug, info, warn } from '../../logger.js';
12
12
 
13
13
  const __builderDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1'));
14
14
  const CODEGRAPH_VERSION = JSON.parse(
15
- fs.readFileSync(path.join(__builderDir, '..', '..', '..', 'package.json'), 'utf-8'),
15
+ fs.readFileSync(path.join(__builderDir, '..', '..', '..', '..', '..', 'package.json'), 'utf-8'),
16
16
  ).version;
17
17
 
18
18
  /**
@@ -127,7 +127,7 @@ export async function finalize(ctx) {
127
127
  debug(`Skipping auto-registration for temp directory: ${resolvedRoot}`);
128
128
  } else {
129
129
  try {
130
- const { registerRepo } = await import('../../registry.js');
130
+ const { registerRepo } = await import('../../../../infrastructure/registry.js');
131
131
  registerRepo(rootDir);
132
132
  } catch (err) {
133
133
  debug(`Auto-registration failed: ${err.message}`);