@optave/codegraph 3.1.3 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +17 -19
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1485
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1522
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/cycles.js +30 -85
  76. package/src/dataflow.js +1 -2
  77. package/src/db/connection.js +4 -4
  78. package/src/db/migrations.js +41 -0
  79. package/src/db/query-builder.js +6 -5
  80. package/src/db/repository/base.js +201 -0
  81. package/src/db/repository/graph-read.js +5 -2
  82. package/src/db/repository/in-memory-repository.js +584 -0
  83. package/src/db/repository/index.js +5 -1
  84. package/src/db/repository/nodes.js +63 -4
  85. package/src/db/repository/sqlite-repository.js +219 -0
  86. package/src/db.js +5 -0
  87. package/src/embeddings/generator.js +163 -0
  88. package/src/embeddings/index.js +13 -0
  89. package/src/embeddings/models.js +218 -0
  90. package/src/embeddings/search/cli-formatter.js +151 -0
  91. package/src/embeddings/search/filters.js +46 -0
  92. package/src/embeddings/search/hybrid.js +121 -0
  93. package/src/embeddings/search/keyword.js +68 -0
  94. package/src/embeddings/search/prepare.js +66 -0
  95. package/src/embeddings/search/semantic.js +145 -0
  96. package/src/embeddings/stores/fts5.js +27 -0
  97. package/src/embeddings/stores/sqlite-blob.js +24 -0
  98. package/src/embeddings/strategies/source.js +14 -0
  99. package/src/embeddings/strategies/structured.js +43 -0
  100. package/src/embeddings/strategies/text-utils.js +43 -0
  101. package/src/errors.js +78 -0
  102. package/src/export.js +217 -520
  103. package/src/extractors/csharp.js +10 -2
  104. package/src/extractors/go.js +3 -1
  105. package/src/extractors/helpers.js +71 -0
  106. package/src/extractors/java.js +9 -2
  107. package/src/extractors/javascript.js +38 -1
  108. package/src/extractors/php.js +3 -1
  109. package/src/extractors/python.js +14 -3
  110. package/src/extractors/rust.js +3 -1
  111. package/src/graph/algorithms/bfs.js +49 -0
  112. package/src/graph/algorithms/centrality.js +16 -0
  113. package/src/graph/algorithms/index.js +5 -0
  114. package/src/graph/algorithms/louvain.js +26 -0
  115. package/src/graph/algorithms/shortest-path.js +41 -0
  116. package/src/graph/algorithms/tarjan.js +49 -0
  117. package/src/graph/builders/dependency.js +91 -0
  118. package/src/graph/builders/index.js +3 -0
  119. package/src/graph/builders/structure.js +40 -0
  120. package/src/graph/builders/temporal.js +33 -0
  121. package/src/graph/classifiers/index.js +2 -0
  122. package/src/graph/classifiers/risk.js +85 -0
  123. package/src/graph/classifiers/roles.js +64 -0
  124. package/src/graph/index.js +13 -0
  125. package/src/graph/model.js +230 -0
  126. package/src/index.js +33 -210
  127. package/src/infrastructure/result-formatter.js +2 -21
  128. package/src/mcp/index.js +2 -0
  129. package/src/mcp/middleware.js +26 -0
  130. package/src/mcp/server.js +128 -0
  131. package/src/mcp/tool-registry.js +801 -0
  132. package/src/mcp/tools/ast-query.js +14 -0
  133. package/src/mcp/tools/audit.js +21 -0
  134. package/src/mcp/tools/batch-query.js +11 -0
  135. package/src/mcp/tools/branch-compare.js +10 -0
  136. package/src/mcp/tools/cfg.js +21 -0
  137. package/src/mcp/tools/check.js +43 -0
  138. package/src/mcp/tools/co-changes.js +20 -0
  139. package/src/mcp/tools/code-owners.js +12 -0
  140. package/src/mcp/tools/communities.js +15 -0
  141. package/src/mcp/tools/complexity.js +18 -0
  142. package/src/mcp/tools/context.js +17 -0
  143. package/src/mcp/tools/dataflow.js +26 -0
  144. package/src/mcp/tools/diff-impact.js +24 -0
  145. package/src/mcp/tools/execution-flow.js +26 -0
  146. package/src/mcp/tools/export-graph.js +57 -0
  147. package/src/mcp/tools/file-deps.js +12 -0
  148. package/src/mcp/tools/file-exports.js +13 -0
  149. package/src/mcp/tools/find-cycles.js +15 -0
  150. package/src/mcp/tools/fn-impact.js +15 -0
  151. package/src/mcp/tools/impact-analysis.js +12 -0
  152. package/src/mcp/tools/index.js +71 -0
  153. package/src/mcp/tools/list-functions.js +14 -0
  154. package/src/mcp/tools/list-repos.js +11 -0
  155. package/src/mcp/tools/module-map.js +6 -0
  156. package/src/mcp/tools/node-roles.js +14 -0
  157. package/src/mcp/tools/path.js +12 -0
  158. package/src/mcp/tools/query.js +30 -0
  159. package/src/mcp/tools/semantic-search.js +65 -0
  160. package/src/mcp/tools/sequence.js +17 -0
  161. package/src/mcp/tools/structure.js +15 -0
  162. package/src/mcp/tools/symbol-children.js +14 -0
  163. package/src/mcp/tools/triage.js +35 -0
  164. package/src/mcp/tools/where.js +13 -0
  165. package/src/mcp.js +2 -1470
  166. package/src/native.js +3 -1
  167. package/src/presentation/colors.js +44 -0
  168. package/src/presentation/export.js +444 -0
  169. package/src/presentation/result-formatter.js +21 -0
  170. package/src/presentation/sequence-renderer.js +43 -0
  171. package/src/presentation/table.js +47 -0
  172. package/src/presentation/viewer.js +634 -0
  173. package/src/queries.js +35 -2276
  174. package/src/resolve.js +1 -1
  175. package/src/sequence.js +2 -38
  176. package/src/shared/file-utils.js +153 -0
  177. package/src/shared/generators.js +125 -0
  178. package/src/shared/hierarchy.js +27 -0
  179. package/src/shared/normalize.js +59 -0
  180. package/src/snapshot.js +6 -5
  181. package/src/structure.js +15 -40
  182. package/src/triage.js +20 -72
  183. package/src/viewer.js +35 -656
  184. package/src/watcher.js +8 -148
  185. package/src/embedder.js +0 -1097
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Stage: buildEdges
3
+ *
4
+ * Builds import, call, receiver, extends, and implements edges.
5
+ * Uses pre-loaded node lookup maps (N+1 optimization).
6
+ */
7
+ import path from 'node:path';
8
+ import { performance } from 'node:perf_hooks';
9
+ import { getNodeId } from '../../db.js';
10
+ import { loadNative } from '../../native.js';
11
+ import { computeConfidence } from '../../resolve.js';
12
+ import { BUILTIN_RECEIVERS, batchInsertEdges } from '../helpers.js';
13
+ import { getResolved, isBarrelFile, resolveBarrelExport } from './resolve-imports.js';
14
+
15
+ /**
16
+ * @param {import('../context.js').PipelineContext} ctx
17
+ */
18
+ export async function buildEdges(ctx) {
19
+ const { db, fileSymbols, barrelOnlyFiles, rootDir, engineName } = ctx;
20
+
21
+ const getNodeIdStmt = {
22
+ get: (name, kind, file, line) => {
23
+ const id = getNodeId(db, name, kind, file, line);
24
+ return id != null ? { id } : undefined;
25
+ },
26
+ };
27
+
28
+ // Pre-load all nodes into lookup maps
29
+ const allNodes = db
30
+ .prepare(
31
+ `SELECT id, name, kind, file, line FROM nodes WHERE kind IN ('function','method','class','interface','struct','type','module','enum','trait')`,
32
+ )
33
+ .all();
34
+ ctx.nodesByName = new Map();
35
+ for (const node of allNodes) {
36
+ if (!ctx.nodesByName.has(node.name)) ctx.nodesByName.set(node.name, []);
37
+ ctx.nodesByName.get(node.name).push(node);
38
+ }
39
+ ctx.nodesByNameAndFile = new Map();
40
+ for (const node of allNodes) {
41
+ const key = `${node.name}|${node.file}`;
42
+ if (!ctx.nodesByNameAndFile.has(key)) ctx.nodesByNameAndFile.set(key, []);
43
+ ctx.nodesByNameAndFile.get(key).push(node);
44
+ }
45
+
46
+ const t0 = performance.now();
47
+ const buildEdgesTx = db.transaction(() => {
48
+ const allEdgeRows = [];
49
+
50
+ // ── Import edges ────────────────────────────────────────────────
51
+ for (const [relPath, symbols] of fileSymbols) {
52
+ if (barrelOnlyFiles.has(relPath)) continue;
53
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
54
+ if (!fileNodeRow) continue;
55
+ const fileNodeId = fileNodeRow.id;
56
+
57
+ for (const imp of symbols.imports) {
58
+ const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
59
+ const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
60
+ if (targetRow) {
61
+ const edgeKind = imp.reexport
62
+ ? 'reexports'
63
+ : imp.typeOnly
64
+ ? 'imports-type'
65
+ : imp.dynamicImport
66
+ ? 'dynamic-imports'
67
+ : 'imports';
68
+ allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
69
+
70
+ if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
71
+ const resolvedSources = new Set();
72
+ for (const name of imp.names) {
73
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
74
+ const actualSource = resolveBarrelExport(ctx, resolvedPath, cleanName);
75
+ if (
76
+ actualSource &&
77
+ actualSource !== resolvedPath &&
78
+ !resolvedSources.has(actualSource)
79
+ ) {
80
+ resolvedSources.add(actualSource);
81
+ const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
82
+ if (actualRow) {
83
+ allEdgeRows.push([
84
+ fileNodeId,
85
+ actualRow.id,
86
+ edgeKind === 'imports-type'
87
+ ? 'imports-type'
88
+ : edgeKind === 'dynamic-imports'
89
+ ? 'dynamic-imports'
90
+ : 'imports',
91
+ 0.9,
92
+ 0,
93
+ ]);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ // ── Call/receiver/extends/implements edges ───────────────────────
103
+ const native = engineName === 'native' ? loadNative() : null;
104
+ if (native?.buildCallEdges) {
105
+ const nativeFiles = [];
106
+ for (const [relPath, symbols] of fileSymbols) {
107
+ if (barrelOnlyFiles.has(relPath)) continue;
108
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
109
+ if (!fileNodeRow) continue;
110
+
111
+ const importedNames = [];
112
+ for (const imp of symbols.imports) {
113
+ const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
114
+ for (const name of imp.names) {
115
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
116
+ let targetFile = resolvedPath;
117
+ if (isBarrelFile(ctx, resolvedPath)) {
118
+ const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
119
+ if (actual) targetFile = actual;
120
+ }
121
+ importedNames.push({ name: cleanName, file: targetFile });
122
+ }
123
+ }
124
+
125
+ nativeFiles.push({
126
+ file: relPath,
127
+ fileNodeId: fileNodeRow.id,
128
+ definitions: symbols.definitions.map((d) => ({
129
+ name: d.name,
130
+ kind: d.kind,
131
+ line: d.line,
132
+ endLine: d.endLine ?? null,
133
+ })),
134
+ calls: symbols.calls,
135
+ importedNames,
136
+ classes: symbols.classes,
137
+ });
138
+ }
139
+
140
+ const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [...BUILTIN_RECEIVERS]);
141
+ for (const e of nativeEdges) {
142
+ allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
143
+ }
144
+ } else {
145
+ // JS fallback
146
+ for (const [relPath, symbols] of fileSymbols) {
147
+ if (barrelOnlyFiles.has(relPath)) continue;
148
+ const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
149
+ if (!fileNodeRow) continue;
150
+
151
+ const importedNames = new Map();
152
+ for (const imp of symbols.imports) {
153
+ const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
154
+ for (const name of imp.names) {
155
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
156
+ importedNames.set(cleanName, resolvedPath);
157
+ }
158
+ }
159
+
160
+ const seenCallEdges = new Set();
161
+ for (const call of symbols.calls) {
162
+ if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
163
+ let caller = null;
164
+ let callerSpan = Infinity;
165
+ for (const def of symbols.definitions) {
166
+ if (def.line <= call.line) {
167
+ const end = def.endLine || Infinity;
168
+ if (call.line <= end) {
169
+ const span = end - def.line;
170
+ if (span < callerSpan) {
171
+ const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
172
+ if (row) {
173
+ caller = row;
174
+ callerSpan = span;
175
+ }
176
+ }
177
+ } else if (!caller) {
178
+ const row = getNodeIdStmt.get(def.name, def.kind, relPath, def.line);
179
+ if (row) caller = row;
180
+ }
181
+ }
182
+ }
183
+ if (!caller) caller = fileNodeRow;
184
+
185
+ const isDynamic = call.dynamic ? 1 : 0;
186
+ let targets;
187
+ const importedFrom = importedNames.get(call.name);
188
+
189
+ if (importedFrom) {
190
+ targets = ctx.nodesByNameAndFile.get(`${call.name}|${importedFrom}`) || [];
191
+ if (targets.length === 0 && isBarrelFile(ctx, importedFrom)) {
192
+ const actualSource = resolveBarrelExport(ctx, importedFrom, call.name);
193
+ if (actualSource) {
194
+ targets = ctx.nodesByNameAndFile.get(`${call.name}|${actualSource}`) || [];
195
+ }
196
+ }
197
+ }
198
+ if (!targets || targets.length === 0) {
199
+ targets = ctx.nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
200
+ if (targets.length === 0) {
201
+ const methodCandidates = (ctx.nodesByName.get(call.name) || []).filter(
202
+ (n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
203
+ );
204
+ if (methodCandidates.length > 0) {
205
+ targets = methodCandidates;
206
+ } else if (
207
+ !call.receiver ||
208
+ call.receiver === 'this' ||
209
+ call.receiver === 'self' ||
210
+ call.receiver === 'super'
211
+ ) {
212
+ targets = (ctx.nodesByName.get(call.name) || []).filter(
213
+ (n) => computeConfidence(relPath, n.file, null) >= 0.5,
214
+ );
215
+ }
216
+ }
217
+ }
218
+
219
+ if (targets.length > 1) {
220
+ targets.sort((a, b) => {
221
+ const confA = computeConfidence(relPath, a.file, importedFrom);
222
+ const confB = computeConfidence(relPath, b.file, importedFrom);
223
+ return confB - confA;
224
+ });
225
+ }
226
+
227
+ for (const t of targets) {
228
+ const edgeKey = `${caller.id}|${t.id}`;
229
+ if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
230
+ seenCallEdges.add(edgeKey);
231
+ const confidence = computeConfidence(relPath, t.file, importedFrom);
232
+ allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic]);
233
+ }
234
+ }
235
+
236
+ // Receiver edge
237
+ if (
238
+ call.receiver &&
239
+ !BUILTIN_RECEIVERS.has(call.receiver) &&
240
+ call.receiver !== 'this' &&
241
+ call.receiver !== 'self' &&
242
+ call.receiver !== 'super'
243
+ ) {
244
+ const receiverKinds = new Set(['class', 'struct', 'interface', 'type', 'module']);
245
+ const samefile = ctx.nodesByNameAndFile.get(`${call.receiver}|${relPath}`) || [];
246
+ const candidates =
247
+ samefile.length > 0 ? samefile : ctx.nodesByName.get(call.receiver) || [];
248
+ const receiverNodes = candidates.filter((n) => receiverKinds.has(n.kind));
249
+ if (receiverNodes.length > 0 && caller) {
250
+ const recvTarget = receiverNodes[0];
251
+ const recvKey = `recv|${caller.id}|${recvTarget.id}`;
252
+ if (!seenCallEdges.has(recvKey)) {
253
+ seenCallEdges.add(recvKey);
254
+ allEdgeRows.push([caller.id, recvTarget.id, 'receiver', 0.7, 0]);
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ // Class extends edges
261
+ for (const cls of symbols.classes) {
262
+ if (cls.extends) {
263
+ const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
264
+ (n) => n.kind === 'class',
265
+ );
266
+ const targetCandidates = ctx.nodesByName.get(cls.extends) || [];
267
+ const targetRows = targetCandidates.filter((n) => n.kind === 'class');
268
+ if (sourceRow) {
269
+ for (const t of targetRows) {
270
+ allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
271
+ }
272
+ }
273
+ }
274
+
275
+ if (cls.implements) {
276
+ const sourceRow = (ctx.nodesByNameAndFile.get(`${cls.name}|${relPath}`) || []).find(
277
+ (n) => n.kind === 'class',
278
+ );
279
+ const targetCandidates = ctx.nodesByName.get(cls.implements) || [];
280
+ const targetRows = targetCandidates.filter(
281
+ (n) => n.kind === 'interface' || n.kind === 'class',
282
+ );
283
+ if (sourceRow) {
284
+ for (const t of targetRows) {
285
+ allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ batchInsertEdges(db, allEdgeRows);
294
+ });
295
+ buildEdgesTx();
296
+ ctx.timing.edgesMs = performance.now() - t0;
297
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Stage: buildStructure + classifyRoles
3
+ *
4
+ * Builds directory structure, containment edges, metrics, and classifies node roles.
5
+ */
6
+ import path from 'node:path';
7
+ import { performance } from 'node:perf_hooks';
8
+ import { normalizePath } from '../../constants.js';
9
+ import { debug } from '../../logger.js';
10
+ import { readFileSafe } from '../helpers.js';
11
+
12
+ /**
13
+ * @param {import('../context.js').PipelineContext} ctx
14
+ */
15
+ export async function buildStructure(ctx) {
16
+ const { db, fileSymbols, rootDir, discoveredDirs, allSymbols, isFullBuild } = ctx;
17
+
18
+ // Build line count map (prefer cached _lineCount from parser)
19
+ ctx.lineCountMap = new Map();
20
+ for (const [relPath, symbols] of fileSymbols) {
21
+ if (symbols.lineCount ?? symbols._lineCount) {
22
+ ctx.lineCountMap.set(relPath, symbols.lineCount ?? symbols._lineCount);
23
+ } else {
24
+ const absPath = path.join(rootDir, relPath);
25
+ try {
26
+ const content = readFileSafe(absPath);
27
+ ctx.lineCountMap.set(relPath, content.split('\n').length);
28
+ } catch {
29
+ ctx.lineCountMap.set(relPath, 0);
30
+ }
31
+ }
32
+ }
33
+
34
+ // For incremental builds, load unchanged files from DB for complete structure
35
+ if (!isFullBuild) {
36
+ const existingFiles = db.prepare("SELECT DISTINCT file FROM nodes WHERE kind = 'file'").all();
37
+ const defsByFile = db.prepare(
38
+ "SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file' AND kind != 'directory'",
39
+ );
40
+ const importCountByFile = db.prepare(
41
+ `SELECT COUNT(DISTINCT n2.file) AS cnt FROM edges e
42
+ JOIN nodes n1 ON e.source_id = n1.id
43
+ JOIN nodes n2 ON e.target_id = n2.id
44
+ WHERE n1.file = ? AND e.kind = 'imports'`,
45
+ );
46
+ const lineCountByFile = db.prepare(
47
+ `SELECT n.name AS file, m.line_count
48
+ FROM node_metrics m JOIN nodes n ON m.node_id = n.id
49
+ WHERE n.kind = 'file'`,
50
+ );
51
+ const cachedLineCounts = new Map();
52
+ for (const row of lineCountByFile.all()) {
53
+ cachedLineCounts.set(row.file, row.line_count);
54
+ }
55
+ let loadedFromDb = 0;
56
+ for (const { file: relPath } of existingFiles) {
57
+ if (!fileSymbols.has(relPath)) {
58
+ const importCount = importCountByFile.get(relPath)?.cnt || 0;
59
+ fileSymbols.set(relPath, {
60
+ definitions: defsByFile.all(relPath),
61
+ imports: new Array(importCount),
62
+ exports: [],
63
+ });
64
+ loadedFromDb++;
65
+ }
66
+ if (!ctx.lineCountMap.has(relPath)) {
67
+ const cached = cachedLineCounts.get(relPath);
68
+ if (cached != null) {
69
+ ctx.lineCountMap.set(relPath, cached);
70
+ } else {
71
+ const absPath = path.join(rootDir, relPath);
72
+ try {
73
+ const content = readFileSafe(absPath);
74
+ ctx.lineCountMap.set(relPath, content.split('\n').length);
75
+ } catch {
76
+ ctx.lineCountMap.set(relPath, 0);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ debug(`Structure: ${fileSymbols.size} files (${loadedFromDb} loaded from DB)`);
82
+ }
83
+
84
+ // Build directory structure
85
+ const t0 = performance.now();
86
+ const relDirs = new Set();
87
+ for (const absDir of discoveredDirs) {
88
+ relDirs.add(normalizePath(path.relative(rootDir, absDir)));
89
+ }
90
+ try {
91
+ const { buildStructure: buildStructureFn } = await import('../../structure.js');
92
+ const changedFilePaths = isFullBuild ? null : [...allSymbols.keys()];
93
+ buildStructureFn(db, fileSymbols, rootDir, ctx.lineCountMap, relDirs, changedFilePaths);
94
+ } catch (err) {
95
+ debug(`Structure analysis failed: ${err.message}`);
96
+ }
97
+ ctx.timing.structureMs = performance.now() - t0;
98
+
99
+ // Classify node roles
100
+ const t1 = performance.now();
101
+ try {
102
+ const { classifyNodeRoles } = await import('../../structure.js');
103
+ const roleSummary = classifyNodeRoles(db);
104
+ debug(
105
+ `Roles: ${Object.entries(roleSummary)
106
+ .map(([r, c]) => `${r}=${c}`)
107
+ .join(', ')}`,
108
+ );
109
+ } catch (err) {
110
+ debug(`Role classification failed: ${err.message}`);
111
+ }
112
+ ctx.timing.rolesMs = performance.now() - t1;
113
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Stage: collectFiles
3
+ *
4
+ * Collects all source files to process. Handles both normal and scoped rebuilds.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { normalizePath } from '../../constants.js';
9
+ import { info } from '../../logger.js';
10
+ import { collectFiles as collectFilesUtil } from '../helpers.js';
11
+
12
+ /**
13
+ * @param {import('../context.js').PipelineContext} ctx
14
+ */
15
+ export async function collectFiles(ctx) {
16
+ const { rootDir, config, opts } = ctx;
17
+
18
+ if (opts.scope) {
19
+ // Scoped rebuild: rebuild only specified files
20
+ const scopedFiles = opts.scope.map((f) => normalizePath(f));
21
+ const existing = [];
22
+ const missing = [];
23
+ for (const rel of scopedFiles) {
24
+ const abs = path.join(rootDir, rel);
25
+ if (fs.existsSync(abs)) {
26
+ existing.push({ file: abs, relPath: rel });
27
+ } else {
28
+ missing.push(rel);
29
+ }
30
+ }
31
+ ctx.allFiles = existing.map((e) => e.file);
32
+ ctx.discoveredDirs = new Set(existing.map((e) => path.dirname(e.file)));
33
+ ctx.parseChanges = existing;
34
+ ctx.metadataUpdates = [];
35
+ ctx.removed = missing;
36
+ ctx.isFullBuild = false;
37
+ info(`Scoped rebuild: ${existing.length} files to rebuild, ${missing.length} to purge`);
38
+ } else {
39
+ const collected = collectFilesUtil(rootDir, [], config, new Set());
40
+ ctx.allFiles = collected.files;
41
+ ctx.discoveredDirs = collected.directories;
42
+ info(`Found ${ctx.allFiles.length} files to parse`);
43
+ }
44
+ }