@optave/codegraph 3.11.0 → 3.11.1

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 (223) hide show
  1. package/README.md +38 -31
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +91 -60
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitor-utils.d.ts +3 -0
  6. package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
  7. package/dist/ast-analysis/visitor-utils.js +83 -49
  8. package/dist/ast-analysis/visitor-utils.js.map +1 -1
  9. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  10. package/dist/ast-analysis/visitors/ast-store-visitor.js +78 -62
  11. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  12. package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
  13. package/dist/ast-analysis/visitors/dataflow-visitor.js +61 -42
  14. package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
  15. package/dist/cli/commands/embed.d.ts.map +1 -1
  16. package/dist/cli/commands/embed.js +49 -4
  17. package/dist/cli/commands/embed.js.map +1 -1
  18. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  19. package/dist/domain/analysis/dependencies.js +106 -80
  20. package/dist/domain/analysis/dependencies.js.map +1 -1
  21. package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
  22. package/dist/domain/analysis/fn-impact.js +77 -52
  23. package/dist/domain/analysis/fn-impact.js.map +1 -1
  24. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  25. package/dist/domain/analysis/module-map.js +132 -121
  26. package/dist/domain/analysis/module-map.js.map +1 -1
  27. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  28. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  29. package/dist/domain/graph/builder/helpers.js +47 -33
  30. package/dist/domain/graph/builder/helpers.js.map +1 -1
  31. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  32. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/incremental.js +142 -76
  34. package/dist/domain/graph/builder/incremental.js.map +1 -1
  35. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  36. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/pipeline.js +10 -766
  38. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  39. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  40. package/dist/domain/graph/builder/stages/build-edges.js +133 -96
  41. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  42. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  44. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  45. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  46. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  47. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  48. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  49. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  50. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  51. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  52. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  54. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  56. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  57. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  58. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  59. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  60. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  62. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  63. package/dist/domain/graph/cycles.d.ts +6 -4
  64. package/dist/domain/graph/cycles.d.ts.map +1 -1
  65. package/dist/domain/graph/cycles.js +50 -55
  66. package/dist/domain/graph/cycles.js.map +1 -1
  67. package/dist/domain/graph/journal.d.ts.map +1 -1
  68. package/dist/domain/graph/journal.js +89 -70
  69. package/dist/domain/graph/journal.js.map +1 -1
  70. package/dist/domain/graph/watcher.d.ts.map +1 -1
  71. package/dist/domain/graph/watcher.js +5 -2
  72. package/dist/domain/graph/watcher.js.map +1 -1
  73. package/dist/domain/parser.d.ts +12 -23
  74. package/dist/domain/parser.d.ts.map +1 -1
  75. package/dist/domain/parser.js +126 -79
  76. package/dist/domain/parser.js.map +1 -1
  77. package/dist/domain/search/generator.d.ts +3 -1
  78. package/dist/domain/search/generator.d.ts.map +1 -1
  79. package/dist/domain/search/generator.js +68 -45
  80. package/dist/domain/search/generator.js.map +1 -1
  81. package/dist/domain/search/models.d.ts +2 -0
  82. package/dist/domain/search/models.d.ts.map +1 -1
  83. package/dist/domain/search/models.js +37 -3
  84. package/dist/domain/search/models.js.map +1 -1
  85. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  86. package/dist/domain/search/search/hybrid.js +49 -40
  87. package/dist/domain/search/search/hybrid.js.map +1 -1
  88. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  89. package/dist/domain/search/search/semantic.js +69 -49
  90. package/dist/domain/search/search/semantic.js.map +1 -1
  91. package/dist/domain/wasm-worker-entry.js +201 -136
  92. package/dist/domain/wasm-worker-entry.js.map +1 -1
  93. package/dist/extractors/elixir.js +95 -71
  94. package/dist/extractors/elixir.js.map +1 -1
  95. package/dist/extractors/gleam.d.ts.map +1 -1
  96. package/dist/extractors/gleam.js +23 -31
  97. package/dist/extractors/gleam.js.map +1 -1
  98. package/dist/extractors/helpers.d.ts +79 -1
  99. package/dist/extractors/helpers.d.ts.map +1 -1
  100. package/dist/extractors/helpers.js +137 -0
  101. package/dist/extractors/helpers.js.map +1 -1
  102. package/dist/extractors/java.d.ts.map +1 -1
  103. package/dist/extractors/java.js +37 -49
  104. package/dist/extractors/java.js.map +1 -1
  105. package/dist/extractors/javascript.d.ts.map +1 -1
  106. package/dist/extractors/javascript.js +44 -44
  107. package/dist/extractors/javascript.js.map +1 -1
  108. package/dist/extractors/julia.js +27 -34
  109. package/dist/extractors/julia.js.map +1 -1
  110. package/dist/extractors/r.d.ts.map +1 -1
  111. package/dist/extractors/r.js +33 -58
  112. package/dist/extractors/r.js.map +1 -1
  113. package/dist/extractors/solidity.d.ts.map +1 -1
  114. package/dist/extractors/solidity.js +38 -61
  115. package/dist/extractors/solidity.js.map +1 -1
  116. package/dist/features/boundaries.d.ts.map +1 -1
  117. package/dist/features/boundaries.js +49 -39
  118. package/dist/features/boundaries.js.map +1 -1
  119. package/dist/features/cfg.d.ts.map +1 -1
  120. package/dist/features/cfg.js +90 -63
  121. package/dist/features/cfg.js.map +1 -1
  122. package/dist/features/check.d.ts.map +1 -1
  123. package/dist/features/check.js +43 -34
  124. package/dist/features/check.js.map +1 -1
  125. package/dist/features/cochange.d.ts.map +1 -1
  126. package/dist/features/cochange.js +68 -56
  127. package/dist/features/cochange.js.map +1 -1
  128. package/dist/features/complexity.d.ts.map +1 -1
  129. package/dist/features/complexity.js +105 -75
  130. package/dist/features/complexity.js.map +1 -1
  131. package/dist/features/dataflow.d.ts.map +1 -1
  132. package/dist/features/dataflow.js +37 -29
  133. package/dist/features/dataflow.js.map +1 -1
  134. package/dist/features/flow.d.ts.map +1 -1
  135. package/dist/features/flow.js +31 -22
  136. package/dist/features/flow.js.map +1 -1
  137. package/dist/features/graph-enrichment.d.ts.map +1 -1
  138. package/dist/features/graph-enrichment.js +77 -70
  139. package/dist/features/graph-enrichment.js.map +1 -1
  140. package/dist/features/owners.d.ts +17 -26
  141. package/dist/features/owners.d.ts.map +1 -1
  142. package/dist/features/owners.js +120 -109
  143. package/dist/features/owners.js.map +1 -1
  144. package/dist/features/sequence.d.ts.map +1 -1
  145. package/dist/features/sequence.js +59 -54
  146. package/dist/features/sequence.js.map +1 -1
  147. package/dist/features/structure-query.d.ts.map +1 -1
  148. package/dist/features/structure-query.js +60 -60
  149. package/dist/features/structure-query.js.map +1 -1
  150. package/dist/features/structure.js +28 -36
  151. package/dist/features/structure.js.map +1 -1
  152. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  153. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  154. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  155. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  156. package/dist/graph/classifiers/roles.js +63 -59
  157. package/dist/graph/classifiers/roles.js.map +1 -1
  158. package/dist/infrastructure/config.d.ts +1 -1
  159. package/dist/infrastructure/config.d.ts.map +1 -1
  160. package/dist/infrastructure/config.js +1 -1
  161. package/dist/infrastructure/config.js.map +1 -1
  162. package/dist/presentation/cfg.d.ts.map +1 -1
  163. package/dist/presentation/cfg.js +44 -29
  164. package/dist/presentation/cfg.js.map +1 -1
  165. package/dist/presentation/flow.d.ts.map +1 -1
  166. package/dist/presentation/flow.js +58 -38
  167. package/dist/presentation/flow.js.map +1 -1
  168. package/dist/types.d.ts +1 -1
  169. package/dist/types.d.ts.map +1 -1
  170. package/package.json +7 -7
  171. package/src/ast-analysis/engine.ts +145 -61
  172. package/src/ast-analysis/visitor-utils.ts +86 -46
  173. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  174. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  175. package/src/cli/commands/embed.ts +54 -4
  176. package/src/domain/analysis/dependencies.ts +166 -85
  177. package/src/domain/analysis/fn-impact.ts +120 -50
  178. package/src/domain/analysis/module-map.ts +175 -140
  179. package/src/domain/graph/builder/helpers.ts +85 -76
  180. package/src/domain/graph/builder/incremental.ts +217 -90
  181. package/src/domain/graph/builder/pipeline.ts +19 -957
  182. package/src/domain/graph/builder/stages/build-edges.ts +198 -140
  183. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  184. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  185. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  186. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  187. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  188. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  189. package/src/domain/graph/cycles.ts +51 -49
  190. package/src/domain/graph/journal.ts +84 -69
  191. package/src/domain/graph/watcher.ts +8 -2
  192. package/src/domain/parser.ts +143 -66
  193. package/src/domain/search/generator.ts +132 -74
  194. package/src/domain/search/models.ts +39 -3
  195. package/src/domain/search/search/hybrid.ts +53 -42
  196. package/src/domain/search/search/semantic.ts +105 -65
  197. package/src/domain/wasm-worker-entry.ts +235 -152
  198. package/src/extractors/elixir.ts +91 -64
  199. package/src/extractors/gleam.ts +33 -37
  200. package/src/extractors/helpers.ts +205 -1
  201. package/src/extractors/java.ts +42 -45
  202. package/src/extractors/javascript.ts +44 -43
  203. package/src/extractors/julia.ts +28 -35
  204. package/src/extractors/r.ts +38 -56
  205. package/src/extractors/solidity.ts +43 -71
  206. package/src/features/boundaries.ts +64 -46
  207. package/src/features/cfg.ts +145 -74
  208. package/src/features/check.ts +60 -43
  209. package/src/features/cochange.ts +95 -72
  210. package/src/features/complexity.ts +134 -79
  211. package/src/features/dataflow.ts +57 -34
  212. package/src/features/flow.ts +48 -24
  213. package/src/features/graph-enrichment.ts +105 -70
  214. package/src/features/owners.ts +186 -146
  215. package/src/features/sequence.ts +99 -69
  216. package/src/features/structure-query.ts +94 -79
  217. package/src/features/structure.ts +56 -56
  218. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  219. package/src/graph/classifiers/roles.ts +64 -54
  220. package/src/infrastructure/config.ts +1 -1
  221. package/src/presentation/cfg.ts +48 -32
  222. package/src/presentation/flow.ts +100 -52
  223. package/src/types.ts +1 -1
@@ -89,12 +89,74 @@ function setupNodeLookups(ctx: PipelineContext, allNodes: QueryNodeRow[]): void
89
89
 
90
90
  // ── Import edges ────────────────────────────────────────────────────────
91
91
 
92
+ /** Pick the edge kind for an import statement based on its modifiers. */
93
+ function importEdgeKind(imp: Import): string {
94
+ if (imp.reexport) return 'reexports';
95
+ if (imp.typeOnly) return 'imports-type';
96
+ if (imp.dynamicImport) return 'dynamic-imports';
97
+ return 'imports';
98
+ }
99
+
100
+ /**
101
+ * For a `import type` statement, emit symbol-level `imports-type` edges so
102
+ * the target symbols get fan-in credit and aren't classified as dead code.
103
+ */
104
+ function emitTypeOnlySymbolEdges(
105
+ ctx: PipelineContext,
106
+ imp: Import,
107
+ resolvedPath: string,
108
+ fileNodeId: number,
109
+ allEdgeRows: EdgeRowTuple[],
110
+ ): void {
111
+ if (!ctx.nodesByNameAndFile) return;
112
+ for (const name of imp.names) {
113
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
114
+ let targetFile = resolvedPath;
115
+ if (isBarrelFile(ctx, resolvedPath)) {
116
+ const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
117
+ if (actual) targetFile = actual;
118
+ }
119
+ const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
120
+ if (candidates && candidates.length > 0) {
121
+ allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Process a single import statement and emit all resulting edges (file→file,
128
+ * type-only symbol-level, and barrel re-export targets).
129
+ */
130
+ function emitEdgesForImport(
131
+ ctx: PipelineContext,
132
+ imp: Import,
133
+ fileNodeId: number,
134
+ relPath: string,
135
+ getNodeIdStmt: NodeIdStmt,
136
+ allEdgeRows: EdgeRowTuple[],
137
+ ): void {
138
+ const resolvedPath = getResolved(ctx, path.join(ctx.rootDir, relPath), imp.source);
139
+ const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
140
+ if (!targetRow) return;
141
+
142
+ const edgeKind = importEdgeKind(imp);
143
+ allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
144
+
145
+ if (imp.typeOnly) {
146
+ emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows);
147
+ }
148
+
149
+ if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
150
+ buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
151
+ }
152
+ }
153
+
92
154
  function buildImportEdges(
93
155
  ctx: PipelineContext,
94
156
  getNodeIdStmt: NodeIdStmt,
95
157
  allEdgeRows: EdgeRowTuple[],
96
158
  ): void {
97
- const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
159
+ const { fileSymbols, barrelOnlyFiles } = ctx;
98
160
 
99
161
  for (const [relPath, symbols] of fileSymbols) {
100
162
  const isBarrelOnly = barrelOnlyFiles.has(relPath);
@@ -105,40 +167,7 @@ function buildImportEdges(
105
167
  for (const imp of symbols.imports) {
106
168
  // Barrel-only files: only emit reexport edges, skip regular imports
107
169
  if (isBarrelOnly && !imp.reexport) continue;
108
-
109
- const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
110
- const targetRow = getNodeIdStmt.get(resolvedPath, 'file', resolvedPath, 0);
111
- if (!targetRow) continue;
112
-
113
- const edgeKind = imp.reexport
114
- ? 'reexports'
115
- : imp.typeOnly
116
- ? 'imports-type'
117
- : imp.dynamicImport
118
- ? 'dynamic-imports'
119
- : 'imports';
120
- allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
121
-
122
- // Type-only imports: create symbol-level edges so the target symbols
123
- // get fan-in credit and aren't falsely classified as dead code.
124
- if (imp.typeOnly && ctx.nodesByNameAndFile) {
125
- for (const name of imp.names) {
126
- const cleanName = name.replace(/^\*\s+as\s+/, '');
127
- let targetFile = resolvedPath;
128
- if (isBarrelFile(ctx, resolvedPath)) {
129
- const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
130
- if (actual) targetFile = actual;
131
- }
132
- const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
133
- if (candidates && candidates.length > 0) {
134
- allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
135
- }
136
- }
137
- }
138
-
139
- if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
140
- buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
141
- }
170
+ emitEdgesForImport(ctx, imp, fileNodeId, relPath, getNodeIdStmt, allEdgeRows);
142
171
  }
143
172
  }
144
173
  }
@@ -174,83 +203,98 @@ function buildBarrelEdges(
174
203
 
175
204
  // ── Import edges (native engine) ────────────────────────────────────────
176
205
 
177
- function buildImportEdgesNative(
178
- ctx: PipelineContext,
179
- getNodeIdStmt: NodeIdStmt,
180
- allEdgeRows: EdgeRowTuple[],
181
- native: NativeAddon,
182
- ): void {
183
- const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
206
+ /** Native FFI input shape for a single import statement. */
207
+ interface NativeImportInfo {
208
+ source: string;
209
+ names: string[];
210
+ reexport: boolean;
211
+ typeOnly: boolean;
212
+ dynamicImport: boolean;
213
+ wildcardReexport: boolean;
214
+ }
184
215
 
185
- // 1. Build per-file input data
186
- const files: Array<{
187
- file: string;
188
- fileNodeId: number;
189
- isBarrelOnly: boolean;
190
- imports: Array<{
191
- source: string;
192
- names: string[];
193
- reexport: boolean;
194
- typeOnly: boolean;
195
- dynamicImport: boolean;
196
- wildcardReexport: boolean;
197
- }>;
198
- definitionNames: string[];
199
- }> = [];
200
-
201
- // Collect all file node IDs we'll need (sources + targets)
202
- const fileNodeIds: Array<{ file: string; nodeId: number }> = [];
203
- const seenNodeFiles = new Set<string>();
204
-
205
- const addFileNodeId = (relPath: string): { id: number } | undefined => {
206
- if (seenNodeFiles.has(relPath)) return fileNodeRowCache.get(relPath);
207
- const row = getNodeIdStmt.get(relPath, 'file', relPath, 0);
208
- if (row) {
209
- seenNodeFiles.add(relPath);
210
- fileNodeIds.push({ file: relPath, nodeId: row.id });
211
- fileNodeRowCache.set(relPath, row);
212
- }
213
- return row;
216
+ /** Native FFI input shape for a single file. */
217
+ interface NativeFileInput {
218
+ file: string;
219
+ fileNodeId: number;
220
+ isBarrelOnly: boolean;
221
+ imports: NativeImportInfo[];
222
+ definitionNames: string[];
223
+ }
224
+
225
+ /** Native FFI input shape for re-exports of a single file. */
226
+ interface NativeReexportInput {
227
+ file: string;
228
+ reexports: Array<{ source: string; names: string[]; wildcardReexport: boolean }>;
229
+ }
230
+
231
+ /** Lazily-resolving cache of file-node rows for the native input arrays. */
232
+ interface FileNodeIdRegistry {
233
+ ids: Array<{ file: string; nodeId: number }>;
234
+ add(relPath: string): { id: number } | undefined;
235
+ }
236
+
237
+ function createFileNodeIdRegistry(getNodeIdStmt: NodeIdStmt): FileNodeIdRegistry {
238
+ const ids: Array<{ file: string; nodeId: number }> = [];
239
+ const seen = new Set<string>();
240
+ const cache = new Map<string, { id: number }>();
241
+ return {
242
+ ids,
243
+ add(relPath: string) {
244
+ if (seen.has(relPath)) return cache.get(relPath);
245
+ const row = getNodeIdStmt.get(relPath, 'file', relPath, 0);
246
+ if (row) {
247
+ seen.add(relPath);
248
+ ids.push({ file: relPath, nodeId: row.id });
249
+ cache.set(relPath, row);
250
+ }
251
+ return row;
252
+ },
214
253
  };
215
- const fileNodeRowCache = new Map<string, { id: number }>();
254
+ }
216
255
 
217
- // 2. Pre-resolve all imports and build resolved imports array.
218
- // Keys use forward-slash-normalized rootDir + "/" + relPath to match the Rust
219
- // lookup format (format!("{}/{}", root_dir.replace('\\', "/"), file)).
220
- // On Windows, rootDir has backslashes but Rust normalizes them — the JS side
221
- // must do the same or every resolve key lookup misses (#750).
222
- const resolvedImports: Array<{ key: string; resolvedPath: string }> = [];
256
+ function toNativeImportInfo(imp: Import): NativeImportInfo {
257
+ return {
258
+ source: imp.source,
259
+ names: imp.names,
260
+ reexport: !!imp.reexport,
261
+ typeOnly: !!imp.typeOnly,
262
+ dynamicImport: !!imp.dynamicImport,
263
+ wildcardReexport: !!imp.wildcardReexport,
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Pre-resolve every import for the given files, registering each resolved
269
+ * target with the registry so the native side has full node-id coverage.
270
+ *
271
+ * Resolved-import keys use forward-slash-normalized rootDir + "/" + relPath to
272
+ * match the Rust lookup format. On Windows, rootDir has backslashes but Rust
273
+ * normalizes them — the JS side must do the same or every key lookup misses
274
+ * (#750).
275
+ */
276
+ function buildNativeFileInputs(
277
+ ctx: PipelineContext,
278
+ registry: FileNodeIdRegistry,
279
+ ): {
280
+ files: NativeFileInput[];
281
+ resolvedImports: Array<{ key: string; resolvedPath: string }>;
282
+ } {
283
+ const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
223
284
  const fwdRootDir = rootDir.replace(/\\/g, '/');
285
+ const files: NativeFileInput[] = [];
286
+ const resolvedImports: Array<{ key: string; resolvedPath: string }> = [];
224
287
 
225
288
  for (const [relPath, symbols] of fileSymbols) {
226
- const fileNodeRow = addFileNodeId(relPath);
289
+ const fileNodeRow = registry.add(relPath);
227
290
  if (!fileNodeRow) continue;
228
291
 
229
- const importInfos: Array<{
230
- source: string;
231
- names: string[];
232
- reexport: boolean;
233
- typeOnly: boolean;
234
- dynamicImport: boolean;
235
- wildcardReexport: boolean;
236
- }> = [];
237
-
292
+ const importInfos: NativeImportInfo[] = [];
238
293
  for (const imp of symbols.imports) {
239
- // Pre-resolve and register target file node
240
294
  const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
241
- addFileNodeId(resolvedPath);
242
-
243
- // Key matches Rust's format!("{}/{}", root_dir.replace('\\', "/"), file_input.file)
295
+ registry.add(resolvedPath);
244
296
  resolvedImports.push({ key: `${fwdRootDir}/${relPath}|${imp.source}`, resolvedPath });
245
-
246
- importInfos.push({
247
- source: imp.source,
248
- names: imp.names,
249
- reexport: !!imp.reexport,
250
- typeOnly: !!imp.typeOnly,
251
- dynamicImport: !!imp.dynamicImport,
252
- wildcardReexport: !!imp.wildcardReexport,
253
- });
297
+ importInfos.push(toNativeImportInfo(imp));
254
298
  }
255
299
 
256
300
  files.push({
@@ -261,61 +305,75 @@ function buildImportEdgesNative(
261
305
  definitionNames: symbols.definitions.map((d) => d.name),
262
306
  });
263
307
  }
308
+ return { files, resolvedImports };
309
+ }
264
310
 
265
- // 4. Flatten reexportMap
266
- const fileReexports: Array<{
267
- file: string;
268
- reexports: Array<{
269
- source: string;
270
- names: string[];
271
- wildcardReexport: boolean;
272
- }>;
273
- }> = [];
274
- if (ctx.reexportMap) {
275
- for (const [file, entries] of ctx.reexportMap) {
276
- const reexports = (
277
- entries as Array<{ source: string; names: string[]; wildcardReexport: boolean }>
278
- ).map((re) => ({
279
- source: re.source,
280
- names: re.names,
281
- wildcardReexport: !!re.wildcardReexport,
282
- }));
283
- fileReexports.push({ file, reexports });
311
+ /** Flatten `ctx.reexportMap` into the array shape the native side expects. */
312
+ function buildNativeReexports(
313
+ ctx: PipelineContext,
314
+ registry: FileNodeIdRegistry,
315
+ ): NativeReexportInput[] {
316
+ const fileReexports: NativeReexportInput[] = [];
317
+ if (!ctx.reexportMap) return fileReexports;
318
+
319
+ for (const [file, entries] of ctx.reexportMap) {
320
+ const reexports = (
321
+ entries as Array<{ source: string; names: string[]; wildcardReexport: boolean }>
322
+ ).map((re) => ({
323
+ source: re.source,
324
+ names: re.names,
325
+ wildcardReexport: !!re.wildcardReexport,
326
+ }));
327
+ fileReexports.push({ file, reexports });
284
328
 
285
- // Register reexport target files for node ID lookup
286
- for (const re of reexports) {
287
- addFileNodeId(re.source);
288
- }
329
+ for (const re of reexports) {
330
+ registry.add(re.source);
289
331
  }
290
332
  }
333
+ return fileReexports;
334
+ }
291
335
 
292
- // 5. Compute barrel file list
336
+ function collectBarrelFiles(ctx: PipelineContext): string[] {
293
337
  const barrelFiles: string[] = [];
294
- for (const [relPath] of fileSymbols) {
295
- if (isBarrelFile(ctx, relPath)) {
296
- barrelFiles.push(relPath);
297
- }
338
+ for (const [relPath] of ctx.fileSymbols) {
339
+ if (isBarrelFile(ctx, relPath)) barrelFiles.push(relPath);
298
340
  }
341
+ return barrelFiles;
342
+ }
299
343
 
300
- // 6. Build symbol node entries for type-only import resolution
344
+ function collectSymbolNodes(
345
+ ctx: PipelineContext,
346
+ ): Array<{ name: string; file: string; nodeId: number }> {
301
347
  const symbolNodes: Array<{ name: string; file: string; nodeId: number }> = [];
302
- if (ctx.nodesByNameAndFile) {
303
- for (const [key, nodes] of ctx.nodesByNameAndFile) {
304
- if (nodes.length > 0) {
305
- const [name, file] = key.split('|');
306
- symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
307
- }
308
- }
348
+ if (!ctx.nodesByNameAndFile) return symbolNodes;
349
+ for (const [key, nodes] of ctx.nodesByNameAndFile) {
350
+ if (nodes.length === 0) continue;
351
+ const [name, file] = key.split('|');
352
+ symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
309
353
  }
354
+ return symbolNodes;
355
+ }
356
+
357
+ function buildImportEdgesNative(
358
+ ctx: PipelineContext,
359
+ getNodeIdStmt: NodeIdStmt,
360
+ allEdgeRows: EdgeRowTuple[],
361
+ native: NativeAddon,
362
+ ): void {
363
+ const registry = createFileNodeIdRegistry(getNodeIdStmt);
364
+
365
+ const { files, resolvedImports } = buildNativeFileInputs(ctx, registry);
366
+ const fileReexports = buildNativeReexports(ctx, registry);
367
+ const barrelFiles = collectBarrelFiles(ctx);
368
+ const symbolNodes = collectSymbolNodes(ctx);
310
369
 
311
- // 7. Call native
312
370
  const nativeEdges = native.buildImportEdges!(
313
371
  files,
314
372
  resolvedImports,
315
373
  fileReexports,
316
- fileNodeIds,
374
+ registry.ids,
317
375
  barrelFiles,
318
- rootDir,
376
+ ctx.rootDir,
319
377
  symbolNodes,
320
378
  ) as NativeEdge[];
321
379
 
@@ -11,87 +11,104 @@ import type { ExtractorOutput } from '../../../../types.js';
11
11
  import type { PipelineContext } from '../context.js';
12
12
  import { readFileSafe } from '../helpers.js';
13
13
 
14
- export async function buildStructure(ctx: PipelineContext): Promise<void> {
15
- const { db, fileSymbols, rootDir, discoveredDirs, allSymbols, isFullBuild } = ctx;
16
-
17
- // Build line count map (prefer cached _lineCount from parser)
14
+ /** Populate `ctx.lineCountMap` from cached parser results, falling back to disk. */
15
+ function populateLineCountMap(ctx: PipelineContext): void {
16
+ const { fileSymbols, rootDir } = ctx;
18
17
  ctx.lineCountMap = new Map();
19
18
  for (const [relPath, symbols] of fileSymbols) {
20
19
  const lineCount =
21
20
  (symbols as ExtractorOutput & { lineCount?: number }).lineCount ?? symbols._lineCount;
22
21
  if (lineCount) {
23
22
  ctx.lineCountMap.set(relPath, lineCount);
24
- } else {
25
- const absPath = path.join(rootDir, relPath);
26
- try {
27
- const content = readFileSafe(absPath);
28
- ctx.lineCountMap.set(relPath, content.split('\n').length);
29
- } catch {
30
- ctx.lineCountMap.set(relPath, 0);
31
- }
23
+ continue;
24
+ }
25
+ const absPath = path.join(rootDir, relPath);
26
+ try {
27
+ const content = readFileSafe(absPath);
28
+ ctx.lineCountMap.set(relPath, content.split('\n').length);
29
+ } catch {
30
+ ctx.lineCountMap.set(relPath, 0);
32
31
  }
33
32
  }
33
+ }
34
34
 
35
- const changedFileList = isFullBuild ? null : [...allSymbols.keys()];
36
-
37
- // For small incremental builds on large codebases, use a fast path that
38
- // updates only the changed files' metrics via targeted SQL instead of
39
- // loading ALL definitions from DB (~8ms) and recomputing ALL metrics (~15ms).
40
- // Gate: ≤smallFilesThreshold changed files AND significantly more existing files (>20) to
41
- // avoid triggering on small test fixtures where directory metrics matter.
35
+ /** Count file-kind nodes already in the DB, preferring the native connection. */
36
+ function countExistingFiles(ctx: PipelineContext): number {
42
37
  const useNativeReads = ctx.engineName === 'native' && !!ctx.nativeDb;
43
- const existingFileCount = !isFullBuild
44
- ? (
45
- (useNativeReads
46
- ? ctx.nativeDb!.queryGet("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'", [])
47
- : db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get()) as {
48
- c: number;
49
- }
50
- ).c
51
- : 0;
52
- const useSmallIncrementalFastPath =
53
- !isFullBuild &&
54
- changedFileList != null &&
55
- changedFileList.length <= ctx.config.build.smallFilesThreshold &&
56
- existingFileCount > 20;
57
-
58
- if (!isFullBuild && !useSmallIncrementalFastPath) {
59
- // Medium/large incremental: load unchanged files from DB for complete structure
60
- loadUnchangedFilesFromDb(ctx);
61
- }
38
+ const row = (
39
+ useNativeReads
40
+ ? ctx.nativeDb!.queryGet("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'", [])
41
+ : ctx.db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get()
42
+ ) as { c: number };
43
+ return row.c;
44
+ }
62
45
 
63
- // Build directory structure
64
- const t0 = performance.now();
46
+ /**
47
+ * Build directory structure + metrics. Chooses between the fast incremental
48
+ * path (a handful of files changed on a large codebase) and the full path
49
+ * (delegated to `features/structure`).
50
+ */
51
+ async function buildDirectoryStructure(
52
+ ctx: PipelineContext,
53
+ changedFileList: string[] | null,
54
+ useSmallIncrementalFastPath: boolean,
55
+ ): Promise<void> {
65
56
  if (useSmallIncrementalFastPath) {
66
57
  updateChangedFileMetrics(ctx, changedFileList!);
67
- } else {
68
- const relDirs = new Set<string>();
69
- for (const absDir of discoveredDirs) {
70
- relDirs.add(normalizePath(path.relative(rootDir, absDir)));
71
- }
72
- try {
73
- const { buildStructure: buildStructureFn } = (await import(
74
- '../../../../features/structure.js'
75
- )) as {
76
- buildStructure: (
77
- db: PipelineContext['db'],
78
- fileSymbols: Map<string, ExtractorOutput>,
79
- rootDir: string,
80
- lineCountMap: Map<string, number>,
81
- directories: Set<string>,
82
- changedFiles: string[] | null,
83
- ) => void;
84
- };
85
- const changedFilePaths = isFullBuild ? null : [...allSymbols.keys()];
86
- buildStructureFn(db, fileSymbols, rootDir, ctx.lineCountMap, relDirs, changedFilePaths);
87
- } catch (err) {
88
- debug(`Structure analysis failed: ${(err as Error).message}`);
89
- }
58
+ return;
90
59
  }
91
- ctx.timing.structureMs = performance.now() - t0;
92
60
 
93
- // Classify node roles (incremental: only reclassify changed files' nodes)
94
- const t1 = performance.now();
61
+ const { db, fileSymbols, rootDir, discoveredDirs, allSymbols, isFullBuild } = ctx;
62
+ const relDirs = new Set<string>();
63
+ for (const absDir of discoveredDirs) {
64
+ relDirs.add(normalizePath(path.relative(rootDir, absDir)));
65
+ }
66
+ try {
67
+ const { buildStructure: buildStructureFn } = (await import(
68
+ '../../../../features/structure.js'
69
+ )) as {
70
+ buildStructure: (
71
+ db: PipelineContext['db'],
72
+ fileSymbols: Map<string, ExtractorOutput>,
73
+ rootDir: string,
74
+ lineCountMap: Map<string, number>,
75
+ directories: Set<string>,
76
+ changedFiles: string[] | null,
77
+ ) => void;
78
+ };
79
+ const changedFilePaths = isFullBuild ? null : [...allSymbols.keys()];
80
+ buildStructureFn(db, fileSymbols, rootDir, ctx.lineCountMap, relDirs, changedFilePaths);
81
+ } catch (err) {
82
+ debug(`Structure analysis failed: ${(err as Error).message}`);
83
+ }
84
+ }
85
+
86
+ /** Convert a `NativeDatabase.classifyRoles*` result into the JS summary shape. */
87
+ function nativeRoleSummaryToRecord(
88
+ nativeResult: NonNullable<
89
+ ReturnType<NonNullable<PipelineContext['nativeDb']>['classifyRolesFull']>
90
+ >,
91
+ ): Record<string, number> {
92
+ return {
93
+ entry: nativeResult.entry,
94
+ core: nativeResult.core,
95
+ utility: nativeResult.utility,
96
+ adapter: nativeResult.adapter,
97
+ dead: nativeResult.dead,
98
+ 'dead-leaf': nativeResult.deadLeaf,
99
+ 'dead-entry': nativeResult.deadEntry,
100
+ 'dead-ffi': nativeResult.deadFfi,
101
+ 'dead-unresolved': nativeResult.deadUnresolved,
102
+ 'test-only': nativeResult.testOnly,
103
+ leaf: nativeResult.leaf,
104
+ };
105
+ }
106
+
107
+ async function classifyRoles(
108
+ ctx: PipelineContext,
109
+ changedFileList: string[] | null,
110
+ ): Promise<void> {
111
+ const useNativeReads = ctx.engineName === 'native' && !!ctx.nativeDb;
95
112
  try {
96
113
  let roleSummary: Record<string, number> | null = null;
97
114
 
@@ -103,24 +120,9 @@ export async function buildStructure(ctx: PipelineContext): Promise<void> {
103
120
  changedFileList && changedFileList.length > 0
104
121
  ? ctx.nativeDb.classifyRolesIncremental(changedFileList)
105
122
  : ctx.nativeDb.classifyRolesFull();
106
- if (nativeResult) {
107
- roleSummary = {
108
- entry: nativeResult.entry,
109
- core: nativeResult.core,
110
- utility: nativeResult.utility,
111
- adapter: nativeResult.adapter,
112
- dead: nativeResult.dead,
113
- 'dead-leaf': nativeResult.deadLeaf,
114
- 'dead-entry': nativeResult.deadEntry,
115
- 'dead-ffi': nativeResult.deadFfi,
116
- 'dead-unresolved': nativeResult.deadUnresolved,
117
- 'test-only': nativeResult.testOnly,
118
- leaf: nativeResult.leaf,
119
- };
120
- }
123
+ if (nativeResult) roleSummary = nativeRoleSummaryToRecord(nativeResult);
121
124
  }
122
125
 
123
- // Fall back to JS path
124
126
  if (!roleSummary) {
125
127
  const { classifyNodeRoles } = (await import('../../../../features/structure.js')) as {
126
128
  classifyNodeRoles: (
@@ -141,6 +143,37 @@ export async function buildStructure(ctx: PipelineContext): Promise<void> {
141
143
  } catch (err) {
142
144
  debug(`Role classification failed: ${(err as Error).message}`);
143
145
  }
146
+ }
147
+
148
+ export async function buildStructure(ctx: PipelineContext): Promise<void> {
149
+ const { allSymbols, isFullBuild } = ctx;
150
+
151
+ populateLineCountMap(ctx);
152
+
153
+ const changedFileList = isFullBuild ? null : [...allSymbols.keys()];
154
+
155
+ // For small incremental builds on large codebases, use a fast path that
156
+ // updates only the changed files' metrics via targeted SQL instead of
157
+ // loading ALL definitions from DB (~8ms) and recomputing ALL metrics (~15ms).
158
+ // Gate: ≤smallFilesThreshold changed files AND significantly more existing files (>20) to
159
+ // avoid triggering on small test fixtures where directory metrics matter.
160
+ const existingFileCount = !isFullBuild ? countExistingFiles(ctx) : 0;
161
+ const useSmallIncrementalFastPath =
162
+ !isFullBuild &&
163
+ changedFileList != null &&
164
+ changedFileList.length <= ctx.config.build.smallFilesThreshold &&
165
+ existingFileCount > 20;
166
+
167
+ if (!isFullBuild && !useSmallIncrementalFastPath) {
168
+ loadUnchangedFilesFromDb(ctx);
169
+ }
170
+
171
+ const t0 = performance.now();
172
+ await buildDirectoryStructure(ctx, changedFileList, useSmallIncrementalFastPath);
173
+ ctx.timing.structureMs = performance.now() - t0;
174
+
175
+ const t1 = performance.now();
176
+ await classifyRoles(ctx, changedFileList);
144
177
  ctx.timing.rolesMs = performance.now() - t1;
145
178
  }
146
179