@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,195 @@
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
+ /**
19
+ * @param {import('../context.js').PipelineContext} ctx
20
+ */
21
+ export async function insertNodes(ctx) {
22
+ const { db, allSymbols, filesToParse, metadataUpdates, rootDir, removed } = ctx;
23
+
24
+ // Build lookup from incremental data (pre-computed hashes + stats)
25
+ const precomputedData = new Map();
26
+ for (const item of filesToParse) {
27
+ if (item.relPath) {
28
+ precomputedData.set(item.relPath, item);
29
+ }
30
+ }
31
+
32
+ const bulkGetNodeIds = { all: (file) => bulkNodeIdsByFile(db, file) };
33
+
34
+ // Prepare hash upsert
35
+ let upsertHash;
36
+ try {
37
+ upsertHash = db.prepare(
38
+ 'INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)',
39
+ );
40
+ } catch {
41
+ upsertHash = null;
42
+ }
43
+
44
+ // Populate fileSymbols before the transaction so it is a pure input
45
+ // to (rather than a side-effect of) the DB write — avoids partial
46
+ // population if the transaction rolls back.
47
+ for (const [relPath, symbols] of allSymbols) {
48
+ ctx.fileSymbols.set(relPath, symbols);
49
+ }
50
+
51
+ const insertAll = db.transaction(() => {
52
+ // Phase 1: Batch insert all file nodes + definitions + exports
53
+ // Row format: [name, kind, file, line, end_line, parent_id, qualified_name, scope, visibility]
54
+ const phase1Rows = [];
55
+ for (const [relPath, symbols] of allSymbols) {
56
+ phase1Rows.push([relPath, 'file', relPath, 0, null, null, null, null, null]);
57
+ for (const def of symbols.definitions) {
58
+ // Methods already have 'Class.method' as name — use as qualified_name.
59
+ // For methods, scope is the class portion; for top-level defs, scope is null.
60
+ const dotIdx = def.name.lastIndexOf('.');
61
+ const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;
62
+ phase1Rows.push([
63
+ def.name,
64
+ def.kind,
65
+ relPath,
66
+ def.line,
67
+ def.endLine || null,
68
+ null,
69
+ def.name,
70
+ scope,
71
+ def.visibility || null,
72
+ ]);
73
+ }
74
+ for (const exp of symbols.exports) {
75
+ phase1Rows.push([exp.name, exp.kind, relPath, exp.line, null, null, exp.name, null, null]);
76
+ }
77
+ }
78
+ batchInsertNodes(db, phase1Rows);
79
+
80
+ // Phase 1b: Mark exported symbols
81
+ const markExported = db.prepare(
82
+ 'UPDATE nodes SET exported = 1 WHERE name = ? AND kind = ? AND file = ? AND line = ?',
83
+ );
84
+ for (const [relPath, symbols] of allSymbols) {
85
+ for (const exp of symbols.exports) {
86
+ markExported.run(exp.name, exp.kind, relPath, exp.line);
87
+ }
88
+ }
89
+
90
+ // Phase 3: Batch insert children (needs parent IDs from Phase 2)
91
+ const childRows = [];
92
+ for (const [relPath, symbols] of allSymbols) {
93
+ const nodeIdMap = new Map();
94
+ for (const row of bulkGetNodeIds.all(relPath)) {
95
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
96
+ }
97
+ for (const def of symbols.definitions) {
98
+ if (!def.children?.length) continue;
99
+ const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
100
+ if (!defId) continue;
101
+ for (const child of def.children) {
102
+ const qualifiedName = `${def.name}.${child.name}`;
103
+ childRows.push([
104
+ child.name,
105
+ child.kind,
106
+ relPath,
107
+ child.line,
108
+ child.endLine || null,
109
+ defId,
110
+ qualifiedName,
111
+ def.name,
112
+ child.visibility || null,
113
+ ]);
114
+ }
115
+ }
116
+ }
117
+ batchInsertNodes(db, childRows);
118
+
119
+ // Phase 5: Batch insert contains/parameter_of edges
120
+ const edgeRows = [];
121
+ for (const [relPath, symbols] of allSymbols) {
122
+ const nodeIdMap = new Map();
123
+ for (const row of bulkGetNodeIds.all(relPath)) {
124
+ nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
125
+ }
126
+ const fileId = nodeIdMap.get(`${relPath}|file|0`);
127
+ for (const def of symbols.definitions) {
128
+ const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
129
+ if (fileId && defId) {
130
+ edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
131
+ }
132
+ if (def.children?.length && defId) {
133
+ for (const child of def.children) {
134
+ const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
135
+ if (childId) {
136
+ edgeRows.push([defId, childId, 'contains', 1.0, 0]);
137
+ if (child.kind === 'parameter') {
138
+ edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ // Update file hash — skip reverse-dep files (unchanged)
146
+ if (upsertHash) {
147
+ const precomputed = precomputedData.get(relPath);
148
+ if (precomputed?._reverseDepOnly) {
149
+ // no-op: file unchanged, hash already correct
150
+ } else if (precomputed?.hash) {
151
+ const stat = precomputed.stat || fileStat(path.join(rootDir, relPath));
152
+ const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
153
+ const size = stat ? stat.size : 0;
154
+ upsertHash.run(relPath, precomputed.hash, mtime, size);
155
+ } else {
156
+ const absPath = path.join(rootDir, relPath);
157
+ let code;
158
+ try {
159
+ code = readFileSafe(absPath);
160
+ } catch {
161
+ code = null;
162
+ }
163
+ if (code !== null) {
164
+ const stat = fileStat(absPath);
165
+ const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
166
+ const size = stat ? stat.size : 0;
167
+ upsertHash.run(relPath, fileHash(code), mtime, size);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ batchInsertEdges(db, edgeRows);
173
+
174
+ // Also update metadata-only entries (self-heal mtime/size without re-parse)
175
+ if (upsertHash) {
176
+ for (const item of metadataUpdates) {
177
+ const mtime = item.stat ? Math.floor(item.stat.mtimeMs) : 0;
178
+ const size = item.stat ? item.stat.size : 0;
179
+ upsertHash.run(item.relPath, item.hash, mtime, size);
180
+ }
181
+ }
182
+ });
183
+
184
+ const t0 = performance.now();
185
+ insertAll();
186
+ ctx.timing.insertMs = performance.now() - t0;
187
+
188
+ // Clean up removed file hashes
189
+ if (upsertHash && removed.length > 0) {
190
+ const deleteHash = db.prepare('DELETE FROM file_hashes WHERE file = ?');
191
+ for (const relPath of removed) {
192
+ deleteHash.run(relPath);
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Stage: parseFiles
3
+ *
4
+ * Parses source files via parseFilesAuto (native or WASM engine).
5
+ * Populates ctx.allSymbols, ctx.fileSymbols, ctx.filesToParse.
6
+ */
7
+ import { performance } from 'node:perf_hooks';
8
+ import { info } from '../../../../infrastructure/logger.js';
9
+ import { parseFilesAuto } from '../../../parser.js';
10
+
11
+ /**
12
+ * @param {import('../context.js').PipelineContext} ctx
13
+ */
14
+ export async function parseFiles(ctx) {
15
+ const { allFiles, parseChanges, isFullBuild, engineOpts, rootDir } = ctx;
16
+
17
+ ctx.filesToParse = isFullBuild ? allFiles.map((f) => ({ file: f })) : parseChanges;
18
+ ctx.fileSymbols = new Map();
19
+
20
+ const filePaths = ctx.filesToParse.map((item) => item.file);
21
+ const t0 = performance.now();
22
+ ctx.allSymbols = await parseFilesAuto(filePaths, rootDir, engineOpts);
23
+ ctx.timing.parseMs = performance.now() - t0;
24
+
25
+ const parsed = ctx.allSymbols.size;
26
+ const skipped = ctx.filesToParse.length - parsed;
27
+ info(`Parsed ${parsed} files (${skipped} skipped)`);
28
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Stage: resolveImports
3
+ *
4
+ * Batch import resolution + barrel/re-export map construction.
5
+ * For incremental builds, loads unchanged barrel files for resolution.
6
+ */
7
+ import path from 'node:path';
8
+ import { performance } from 'node:perf_hooks';
9
+ import { parseFilesAuto } from '../../../parser.js';
10
+ import { resolveImportPath, resolveImportsBatch } from '../../resolve.js';
11
+
12
+ /**
13
+ * @param {import('../context.js').PipelineContext} ctx
14
+ */
15
+ export async function resolveImports(ctx) {
16
+ const { db, fileSymbols, rootDir, aliases, allFiles, isFullBuild, engineOpts } = ctx;
17
+
18
+ // Collect all (fromFile, importSource) pairs and resolve in one native call
19
+ const t0 = performance.now();
20
+ const batchInputs = [];
21
+ for (const [relPath, symbols] of fileSymbols) {
22
+ const absFile = path.join(rootDir, relPath);
23
+ for (const imp of symbols.imports) {
24
+ batchInputs.push({ fromFile: absFile, importSource: imp.source });
25
+ }
26
+ }
27
+ ctx.batchResolved = resolveImportsBatch(batchInputs, rootDir, aliases, allFiles);
28
+ ctx.timing.resolveMs = performance.now() - t0;
29
+
30
+ // Build re-export map for barrel resolution
31
+ ctx.reexportMap = new Map();
32
+ for (const [relPath, symbols] of fileSymbols) {
33
+ const reexports = symbols.imports.filter((imp) => imp.reexport);
34
+ if (reexports.length > 0) {
35
+ ctx.reexportMap.set(
36
+ relPath,
37
+ reexports.map((imp) => ({
38
+ source: getResolved(ctx, path.join(rootDir, relPath), imp.source),
39
+ names: imp.names,
40
+ wildcardReexport: imp.wildcardReexport || false,
41
+ })),
42
+ );
43
+ }
44
+ }
45
+
46
+ // For incremental builds, load unchanged barrel files into reexportMap
47
+ ctx.barrelOnlyFiles = new Set();
48
+ if (!isFullBuild) {
49
+ const barrelCandidates = db
50
+ .prepare(
51
+ `SELECT DISTINCT n1.file FROM edges e
52
+ JOIN nodes n1 ON e.source_id = n1.id
53
+ WHERE e.kind = 'reexports' AND n1.kind = 'file'`,
54
+ )
55
+ .all();
56
+ for (const { file: relPath } of barrelCandidates) {
57
+ if (fileSymbols.has(relPath)) continue;
58
+ const absPath = path.join(rootDir, relPath);
59
+ try {
60
+ const symbols = await parseFilesAuto([absPath], rootDir, engineOpts);
61
+ const fileSym = symbols.get(relPath);
62
+ if (fileSym) {
63
+ fileSymbols.set(relPath, fileSym);
64
+ ctx.barrelOnlyFiles.add(relPath);
65
+ const reexports = fileSym.imports.filter((imp) => imp.reexport);
66
+ if (reexports.length > 0) {
67
+ ctx.reexportMap.set(
68
+ relPath,
69
+ reexports.map((imp) => ({
70
+ source: getResolved(ctx, absPath, imp.source),
71
+ names: imp.names,
72
+ wildcardReexport: imp.wildcardReexport || false,
73
+ })),
74
+ );
75
+ }
76
+ }
77
+ } catch {
78
+ /* skip if unreadable */
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Resolve an import source, preferring batch results.
86
+ * Exported so other stages (build-edges) can reuse it.
87
+ */
88
+ export function getResolved(ctx, absFile, importSource) {
89
+ if (ctx.batchResolved) {
90
+ const key = `${absFile}|${importSource}`;
91
+ const hit = ctx.batchResolved.get(key);
92
+ if (hit !== undefined) return hit;
93
+ }
94
+ return resolveImportPath(absFile, importSource, ctx.rootDir, ctx.aliases);
95
+ }
96
+
97
+ /**
98
+ * Check if a file is a barrel (re-export hub).
99
+ */
100
+ export function isBarrelFile(ctx, relPath) {
101
+ const symbols = ctx.fileSymbols.get(relPath);
102
+ if (!symbols) return false;
103
+ const reexports = symbols.imports.filter((imp) => imp.reexport);
104
+ if (reexports.length === 0) return false;
105
+ const ownDefs = symbols.definitions.length;
106
+ return reexports.length >= ownDefs;
107
+ }
108
+
109
+ /**
110
+ * Resolve a symbol through barrel re-export chains.
111
+ */
112
+ export function resolveBarrelExport(ctx, barrelPath, symbolName, visited = new Set()) {
113
+ if (visited.has(barrelPath)) return null;
114
+ visited.add(barrelPath);
115
+ const reexports = ctx.reexportMap.get(barrelPath);
116
+ if (!reexports) return null;
117
+
118
+ for (const re of reexports) {
119
+ if (re.names.length > 0 && !re.wildcardReexport) {
120
+ if (re.names.includes(symbolName)) {
121
+ const targetSymbols = ctx.fileSymbols.get(re.source);
122
+ if (targetSymbols) {
123
+ const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
124
+ if (hasDef) return re.source;
125
+ const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
126
+ if (deeper) return deeper;
127
+ }
128
+ return re.source;
129
+ }
130
+ continue;
131
+ }
132
+ if (re.wildcardReexport || re.names.length === 0) {
133
+ const targetSymbols = ctx.fileSymbols.get(re.source);
134
+ if (targetSymbols) {
135
+ const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
136
+ if (hasDef) return re.source;
137
+ const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
138
+ if (deeper) return deeper;
139
+ }
140
+ }
141
+ }
142
+ return null;
143
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Stage: runAnalyses
3
+ *
4
+ * Dispatches to the unified AST analysis engine (AST nodes, complexity, CFG, dataflow).
5
+ * Filters out reverse-dep files for incremental builds.
6
+ */
7
+ import { debug, warn } from '../../../../infrastructure/logger.js';
8
+
9
+ /**
10
+ * @param {import('../context.js').PipelineContext} ctx
11
+ */
12
+ export async function runAnalyses(ctx) {
13
+ const { db, allSymbols, rootDir, opts, engineOpts, isFullBuild, filesToParse } = ctx;
14
+
15
+ // For incremental builds, exclude reverse-dep-only files
16
+ let astComplexitySymbols = allSymbols;
17
+ if (!isFullBuild) {
18
+ const reverseDepFiles = new Set(
19
+ filesToParse.filter((item) => item._reverseDepOnly).map((item) => item.relPath),
20
+ );
21
+ if (reverseDepFiles.size > 0) {
22
+ astComplexitySymbols = new Map();
23
+ for (const [relPath, symbols] of allSymbols) {
24
+ if (!reverseDepFiles.has(relPath)) {
25
+ astComplexitySymbols.set(relPath, symbols);
26
+ }
27
+ }
28
+ debug(
29
+ `AST/complexity/CFG/dataflow: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`,
30
+ );
31
+ }
32
+ }
33
+
34
+ const { runAnalyses: runAnalysesFn } = await import('../../../../ast-analysis/engine.js');
35
+ try {
36
+ const analysisTiming = await runAnalysesFn(db, astComplexitySymbols, rootDir, opts, engineOpts);
37
+ ctx.timing.astMs = analysisTiming.astMs;
38
+ ctx.timing.complexityMs = analysisTiming.complexityMs;
39
+ ctx.timing.cfgMs = analysisTiming.cfgMs;
40
+ ctx.timing.dataflowMs = analysisTiming.dataflowMs;
41
+ } catch (err) {
42
+ warn(`Analysis engine failed (AST/complexity/CFG/dataflow may be incomplete): ${err.message}`);
43
+ }
44
+ }
@@ -0,0 +1,11 @@
1
+ // Barrel re-export — keeps all existing `import { ... } from './builder.js'` working.
2
+ // See src/builder/ for the pipeline implementation (ROADMAP 3.9).
3
+
4
+ export {
5
+ collectFiles,
6
+ loadPathAliases,
7
+ purgeFilesFromGraph,
8
+ readFileSafe,
9
+ } from './builder/helpers.js';
10
+ export { buildGraph } from './builder/pipeline.js';
11
+ export { resolveImportPath } from './resolve.js';
@@ -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
@@ -0,0 +1,82 @@
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
+
6
+ /**
7
+ * Detect circular dependencies in the codebase using Tarjan's SCC algorithm.
8
+ * Dispatches to native Rust implementation when available, falls back to JS.
9
+ * @param {object} db - Open SQLite database
10
+ * @param {object} opts - { fileLevel: true, noTests: false }
11
+ * @returns {string[][]} Array of cycles, each cycle is an array of file paths
12
+ */
13
+ export function findCycles(db, opts = {}) {
14
+ const fileLevel = opts.fileLevel !== false;
15
+ const noTests = opts.noTests || false;
16
+
17
+ const graph = buildDependencyGraph(db, { fileLevel, noTests });
18
+
19
+ // Build a label map: DB string ID → human-readable key
20
+ // File-level: file path; Function-level: name|file composite (for native Rust compat)
21
+ const idToLabel = new Map();
22
+ for (const [id, attrs] of graph.nodes()) {
23
+ if (fileLevel) {
24
+ idToLabel.set(id, attrs.file);
25
+ } else {
26
+ idToLabel.set(id, `${attrs.label}|${attrs.file}`);
27
+ }
28
+ }
29
+
30
+ // Build edge array with human-readable keys (for native engine)
31
+ const edges = graph.toEdgeArray().map((e) => ({
32
+ source: idToLabel.get(e.source),
33
+ target: idToLabel.get(e.target),
34
+ }));
35
+
36
+ // Try native Rust implementation
37
+ const native = loadNative();
38
+ if (native) {
39
+ return native.detectCycles(edges);
40
+ }
41
+
42
+ // Fallback: JS Tarjan via graph subsystem
43
+ // Re-key graph with human-readable labels for consistent output
44
+ const labelGraph = new CodeGraph();
45
+ for (const { source, target } of edges) {
46
+ labelGraph.addEdge(source, target);
47
+ }
48
+ return tarjan(labelGraph);
49
+ }
50
+
51
+ /**
52
+ * Pure-JS Tarjan's SCC implementation.
53
+ * Kept for backward compatibility — accepts raw {source, target}[] edges.
54
+ */
55
+ export function findCyclesJS(edges) {
56
+ const graph = new CodeGraph();
57
+ for (const { source, target } of edges) {
58
+ graph.addEdge(source, target);
59
+ }
60
+ return tarjan(graph);
61
+ }
62
+
63
+ /**
64
+ * Format cycles for human-readable output.
65
+ */
66
+ export function formatCycles(cycles) {
67
+ if (cycles.length === 0) {
68
+ return 'No circular dependencies detected.';
69
+ }
70
+
71
+ const lines = [`Found ${cycles.length} circular dependency cycle(s):\n`];
72
+ for (let i = 0; i < cycles.length; i++) {
73
+ const cycle = cycles[i];
74
+ lines.push(` Cycle ${i + 1} (${cycle.length} files):`);
75
+ for (const file of cycle) {
76
+ lines.push(` -> ${file}`);
77
+ }
78
+ lines.push(` -> ${cycle[0]} (back to start)`);
79
+ lines.push('');
80
+ }
81
+ return lines.join('\n');
82
+ }
@@ -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
 
@@ -178,4 +178,4 @@ export function resolveImportsBatch(inputs, rootDir, aliases, knownFiles) {
178
178
 
179
179
  // ── Exported for testing ────────────────────────────────────────────
180
180
 
181
- export { resolveImportPathJS, computeConfidenceJS };
181
+ export { computeConfidenceJS, resolveImportPathJS };