@optave/codegraph 3.11.0 → 3.11.2

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 (230) 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/call-resolver.d.ts +71 -0
  28. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  29. package/dist/domain/graph/builder/call-resolver.js +130 -0
  30. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  31. package/dist/domain/graph/builder/helpers.d.ts +4 -4
  32. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/helpers.js +47 -33
  34. package/dist/domain/graph/builder/helpers.js.map +1 -1
  35. package/dist/domain/graph/builder/incremental.d.ts +6 -0
  36. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/incremental.js +214 -127
  38. package/dist/domain/graph/builder/incremental.js.map +1 -1
  39. package/dist/domain/graph/builder/pipeline.d.ts +1 -44
  40. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  41. package/dist/domain/graph/builder/pipeline.js +10 -766
  42. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  43. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  44. package/dist/domain/graph/builder/stages/build-edges.js +151 -192
  45. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  46. package/dist/domain/graph/builder/stages/build-structure.d.ts.map +1 -1
  47. package/dist/domain/graph/builder/stages/build-structure.js +82 -65
  48. package/dist/domain/graph/builder/stages/build-structure.js.map +1 -1
  49. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  50. package/dist/domain/graph/builder/stages/detect-changes.js +84 -56
  51. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  52. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  53. package/dist/domain/graph/builder/stages/finalize.js +60 -51
  54. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  55. package/dist/domain/graph/builder/stages/insert-nodes.d.ts +8 -6
  56. package/dist/domain/graph/builder/stages/insert-nodes.d.ts.map +1 -1
  57. package/dist/domain/graph/builder/stages/insert-nodes.js +107 -122
  58. package/dist/domain/graph/builder/stages/insert-nodes.js.map +1 -1
  59. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts +14 -0
  60. package/dist/domain/graph/builder/stages/native-db-lifecycle.d.ts.map +1 -0
  61. package/dist/domain/graph/builder/stages/native-db-lifecycle.js +77 -0
  62. package/dist/domain/graph/builder/stages/native-db-lifecycle.js.map +1 -0
  63. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts +62 -0
  64. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -0
  65. package/dist/domain/graph/builder/stages/native-orchestrator.js +747 -0
  66. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -0
  67. package/dist/domain/graph/cycles.d.ts +6 -4
  68. package/dist/domain/graph/cycles.d.ts.map +1 -1
  69. package/dist/domain/graph/cycles.js +50 -55
  70. package/dist/domain/graph/cycles.js.map +1 -1
  71. package/dist/domain/graph/journal.d.ts.map +1 -1
  72. package/dist/domain/graph/journal.js +89 -70
  73. package/dist/domain/graph/journal.js.map +1 -1
  74. package/dist/domain/graph/watcher.d.ts.map +1 -1
  75. package/dist/domain/graph/watcher.js +10 -4
  76. package/dist/domain/graph/watcher.js.map +1 -1
  77. package/dist/domain/parser.d.ts +12 -23
  78. package/dist/domain/parser.d.ts.map +1 -1
  79. package/dist/domain/parser.js +126 -79
  80. package/dist/domain/parser.js.map +1 -1
  81. package/dist/domain/search/generator.d.ts +3 -1
  82. package/dist/domain/search/generator.d.ts.map +1 -1
  83. package/dist/domain/search/generator.js +68 -45
  84. package/dist/domain/search/generator.js.map +1 -1
  85. package/dist/domain/search/models.d.ts +2 -0
  86. package/dist/domain/search/models.d.ts.map +1 -1
  87. package/dist/domain/search/models.js +37 -3
  88. package/dist/domain/search/models.js.map +1 -1
  89. package/dist/domain/search/search/hybrid.d.ts.map +1 -1
  90. package/dist/domain/search/search/hybrid.js +49 -40
  91. package/dist/domain/search/search/hybrid.js.map +1 -1
  92. package/dist/domain/search/search/semantic.d.ts.map +1 -1
  93. package/dist/domain/search/search/semantic.js +69 -49
  94. package/dist/domain/search/search/semantic.js.map +1 -1
  95. package/dist/domain/wasm-worker-entry.js +201 -136
  96. package/dist/domain/wasm-worker-entry.js.map +1 -1
  97. package/dist/extractors/elixir.js +95 -71
  98. package/dist/extractors/elixir.js.map +1 -1
  99. package/dist/extractors/gleam.d.ts.map +1 -1
  100. package/dist/extractors/gleam.js +23 -31
  101. package/dist/extractors/gleam.js.map +1 -1
  102. package/dist/extractors/helpers.d.ts +79 -1
  103. package/dist/extractors/helpers.d.ts.map +1 -1
  104. package/dist/extractors/helpers.js +137 -0
  105. package/dist/extractors/helpers.js.map +1 -1
  106. package/dist/extractors/java.d.ts.map +1 -1
  107. package/dist/extractors/java.js +37 -49
  108. package/dist/extractors/java.js.map +1 -1
  109. package/dist/extractors/javascript.d.ts.map +1 -1
  110. package/dist/extractors/javascript.js +44 -44
  111. package/dist/extractors/javascript.js.map +1 -1
  112. package/dist/extractors/julia.js +27 -34
  113. package/dist/extractors/julia.js.map +1 -1
  114. package/dist/extractors/r.d.ts.map +1 -1
  115. package/dist/extractors/r.js +33 -58
  116. package/dist/extractors/r.js.map +1 -1
  117. package/dist/extractors/solidity.d.ts.map +1 -1
  118. package/dist/extractors/solidity.js +38 -61
  119. package/dist/extractors/solidity.js.map +1 -1
  120. package/dist/features/boundaries.d.ts.map +1 -1
  121. package/dist/features/boundaries.js +49 -39
  122. package/dist/features/boundaries.js.map +1 -1
  123. package/dist/features/cfg.d.ts.map +1 -1
  124. package/dist/features/cfg.js +90 -63
  125. package/dist/features/cfg.js.map +1 -1
  126. package/dist/features/check.d.ts.map +1 -1
  127. package/dist/features/check.js +43 -34
  128. package/dist/features/check.js.map +1 -1
  129. package/dist/features/cochange.d.ts.map +1 -1
  130. package/dist/features/cochange.js +68 -56
  131. package/dist/features/cochange.js.map +1 -1
  132. package/dist/features/complexity.d.ts.map +1 -1
  133. package/dist/features/complexity.js +105 -75
  134. package/dist/features/complexity.js.map +1 -1
  135. package/dist/features/dataflow.d.ts.map +1 -1
  136. package/dist/features/dataflow.js +37 -29
  137. package/dist/features/dataflow.js.map +1 -1
  138. package/dist/features/flow.d.ts.map +1 -1
  139. package/dist/features/flow.js +31 -22
  140. package/dist/features/flow.js.map +1 -1
  141. package/dist/features/graph-enrichment.d.ts.map +1 -1
  142. package/dist/features/graph-enrichment.js +77 -70
  143. package/dist/features/graph-enrichment.js.map +1 -1
  144. package/dist/features/owners.d.ts +17 -26
  145. package/dist/features/owners.d.ts.map +1 -1
  146. package/dist/features/owners.js +120 -109
  147. package/dist/features/owners.js.map +1 -1
  148. package/dist/features/sequence.d.ts.map +1 -1
  149. package/dist/features/sequence.js +59 -54
  150. package/dist/features/sequence.js.map +1 -1
  151. package/dist/features/structure-query.d.ts.map +1 -1
  152. package/dist/features/structure-query.js +60 -60
  153. package/dist/features/structure-query.js.map +1 -1
  154. package/dist/features/structure.d.ts.map +1 -1
  155. package/dist/features/structure.js +149 -52
  156. package/dist/features/structure.js.map +1 -1
  157. package/dist/graph/algorithms/leiden/optimiser.d.ts.map +1 -1
  158. package/dist/graph/algorithms/leiden/optimiser.js +100 -69
  159. package/dist/graph/algorithms/leiden/optimiser.js.map +1 -1
  160. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  161. package/dist/graph/classifiers/roles.js +63 -59
  162. package/dist/graph/classifiers/roles.js.map +1 -1
  163. package/dist/infrastructure/config.d.ts +1 -1
  164. package/dist/infrastructure/config.d.ts.map +1 -1
  165. package/dist/infrastructure/config.js +1 -1
  166. package/dist/infrastructure/config.js.map +1 -1
  167. package/dist/presentation/cfg.d.ts.map +1 -1
  168. package/dist/presentation/cfg.js +44 -29
  169. package/dist/presentation/cfg.js.map +1 -1
  170. package/dist/presentation/flow.d.ts.map +1 -1
  171. package/dist/presentation/flow.js +58 -38
  172. package/dist/presentation/flow.js.map +1 -1
  173. package/dist/types.d.ts +1 -1
  174. package/dist/types.d.ts.map +1 -1
  175. package/grammars/tree-sitter-erlang.wasm +0 -0
  176. package/package.json +9 -9
  177. package/src/ast-analysis/engine.ts +145 -61
  178. package/src/ast-analysis/visitor-utils.ts +86 -46
  179. package/src/ast-analysis/visitors/ast-store-visitor.ts +104 -69
  180. package/src/ast-analysis/visitors/dataflow-visitor.ts +86 -47
  181. package/src/cli/commands/embed.ts +54 -4
  182. package/src/domain/analysis/dependencies.ts +166 -85
  183. package/src/domain/analysis/fn-impact.ts +120 -50
  184. package/src/domain/analysis/module-map.ts +175 -140
  185. package/src/domain/graph/builder/call-resolver.ts +181 -0
  186. package/src/domain/graph/builder/helpers.ts +85 -76
  187. package/src/domain/graph/builder/incremental.ts +321 -152
  188. package/src/domain/graph/builder/pipeline.ts +19 -957
  189. package/src/domain/graph/builder/stages/build-edges.ts +229 -275
  190. package/src/domain/graph/builder/stages/build-structure.ts +115 -82
  191. package/src/domain/graph/builder/stages/detect-changes.ts +107 -64
  192. package/src/domain/graph/builder/stages/finalize.ts +72 -70
  193. package/src/domain/graph/builder/stages/insert-nodes.ts +154 -120
  194. package/src/domain/graph/builder/stages/native-db-lifecycle.ts +74 -0
  195. package/src/domain/graph/builder/stages/native-orchestrator.ts +942 -0
  196. package/src/domain/graph/cycles.ts +51 -49
  197. package/src/domain/graph/journal.ts +84 -69
  198. package/src/domain/graph/watcher.ts +12 -4
  199. package/src/domain/parser.ts +143 -66
  200. package/src/domain/search/generator.ts +132 -74
  201. package/src/domain/search/models.ts +39 -3
  202. package/src/domain/search/search/hybrid.ts +53 -42
  203. package/src/domain/search/search/semantic.ts +105 -65
  204. package/src/domain/wasm-worker-entry.ts +235 -152
  205. package/src/extractors/elixir.ts +91 -64
  206. package/src/extractors/gleam.ts +33 -37
  207. package/src/extractors/helpers.ts +205 -1
  208. package/src/extractors/java.ts +42 -45
  209. package/src/extractors/javascript.ts +44 -43
  210. package/src/extractors/julia.ts +28 -35
  211. package/src/extractors/r.ts +38 -56
  212. package/src/extractors/solidity.ts +43 -71
  213. package/src/features/boundaries.ts +64 -46
  214. package/src/features/cfg.ts +145 -74
  215. package/src/features/check.ts +60 -43
  216. package/src/features/cochange.ts +95 -72
  217. package/src/features/complexity.ts +134 -79
  218. package/src/features/dataflow.ts +57 -34
  219. package/src/features/flow.ts +48 -24
  220. package/src/features/graph-enrichment.ts +105 -70
  221. package/src/features/owners.ts +186 -146
  222. package/src/features/sequence.ts +99 -69
  223. package/src/features/structure-query.ts +94 -79
  224. package/src/features/structure.ts +199 -79
  225. package/src/graph/algorithms/leiden/optimiser.ts +142 -87
  226. package/src/graph/classifiers/roles.ts +64 -54
  227. package/src/infrastructure/config.ts +1 -1
  228. package/src/presentation/cfg.ts +48 -32
  229. package/src/presentation/flow.ts +100 -52
  230. package/src/types.ts +1 -1
@@ -92,23 +92,69 @@ function marshalSymbolBatches(allSymbols: Map<string, ExtractorOutput>): InsertN
92
92
  return batches;
93
93
  }
94
94
 
95
+ /** A single file_hashes row. */
96
+ interface FileHashRecord {
97
+ file: string;
98
+ hash: string;
99
+ mtime: number;
100
+ size: number;
101
+ }
102
+
103
+ /** Resolve the (hash, mtime, size) tuple for a relPath, reading from disk if needed. */
104
+ function resolveHashFromPrecomputed(
105
+ relPath: string,
106
+ precomputed: PrecomputedFileData,
107
+ rootDir: string,
108
+ caller: string,
109
+ ): FileHashRecord | null {
110
+ if (precomputed.hash) {
111
+ let mtime: number;
112
+ let size: number;
113
+ if (precomputed.stat) {
114
+ mtime = precomputed.stat.mtime;
115
+ size = precomputed.stat.size;
116
+ } else {
117
+ const rawStat = fileStat(path.join(rootDir, relPath));
118
+ mtime = rawStat ? rawStat.mtime : 0;
119
+ size = rawStat ? rawStat.size : 0;
120
+ }
121
+ return { file: relPath, hash: precomputed.hash, mtime, size };
122
+ }
123
+
124
+ const absPath = path.join(rootDir, relPath);
125
+ let code: string | null;
126
+ try {
127
+ code = readFileSafe(absPath);
128
+ } catch (e) {
129
+ debug(`${caller}: readFileSafe failed for ${relPath}: ${toErrorMessage(e)}`);
130
+ code = null;
131
+ }
132
+ if (code === null) return null;
133
+ const stat = fileStat(absPath);
134
+ return {
135
+ file: relPath,
136
+ hash: fileHash(code),
137
+ mtime: stat ? stat.mtime : 0,
138
+ size: stat ? stat.size : 0,
139
+ };
140
+ }
141
+
95
142
  /**
96
- * Build file hash entries for every collected file, including those that
97
- * produced zero symbols (empty files, parsers that silently no-op'd, or
98
- * optional-language extensions whose grammar wasn't installed). Iterating the
99
- * symbol map instead would skip such files and leave them missing from
100
- * `file_hashes`, which permanently breaks the JS-side fast-skip pre-flight on
101
- * any subsequent no-op rebuild (#1068).
143
+ * Walk every collected file once and yield a `FileHashRecord` for it, plus one
144
+ * record per metadata-only update. Shared by `buildFileHashes` (native path)
145
+ * and `updateFileHashes` (JS fallback) so the iteration and hash-resolution
146
+ * logic stays in one place.
102
147
  *
103
- * Exported for unit testing.
148
+ * Files marked `_reverseDepOnly` are skipped — their hashes are already
149
+ * correct in the DB.
104
150
  */
105
- export function buildFileHashes(
151
+ function* iterFileHashRecords(
106
152
  filesToParse: FileToParse[],
107
153
  precomputedData: Map<string, PrecomputedFileData>,
108
154
  metadataUpdates: MetadataUpdate[],
109
155
  rootDir: string,
110
- ): Array<{ file: string; hash: string; mtime: number; size: number }> {
111
- const fileHashes: Array<{ file: string; hash: string; mtime: number; size: number }> = [];
156
+ caller: string,
157
+ ): Generator<FileHashRecord> {
112
158
  const seen = new Set<string>();
113
159
 
114
160
  for (const item of filesToParse) {
@@ -117,47 +163,53 @@ export function buildFileHashes(
117
163
  seen.add(relPath);
118
164
 
119
165
  const precomputed = precomputedData.get(relPath);
120
- if (precomputed?._reverseDepOnly) {
121
- continue; // file unchanged, hash already correct
122
- }
123
- if (precomputed?.hash) {
124
- let mtime: number;
125
- let size: number;
126
- if (precomputed.stat) {
127
- mtime = precomputed.stat.mtime;
128
- size = precomputed.stat.size;
129
- } else {
130
- const rawStat = fileStat(path.join(rootDir, relPath));
131
- mtime = rawStat ? rawStat.mtime : 0;
132
- size = rawStat ? rawStat.size : 0;
133
- }
134
- fileHashes.push({ file: relPath, hash: precomputed.hash, mtime, size });
135
- } else {
136
- const absPath = path.join(rootDir, relPath);
137
- let code: string | null;
138
- try {
139
- code = readFileSafe(absPath);
140
- } catch (e) {
141
- debug(`buildFileHashes: readFileSafe failed for ${relPath}: ${toErrorMessage(e)}`);
142
- code = null;
143
- }
144
- if (code !== null) {
145
- const stat = fileStat(absPath);
146
- const mtime = stat ? stat.mtime : 0;
147
- const size = stat ? stat.size : 0;
148
- fileHashes.push({ file: relPath, hash: fileHash(code), mtime, size });
149
- }
150
- }
166
+ if (precomputed?._reverseDepOnly) continue;
167
+
168
+ const record = resolveHashFromPrecomputed(
169
+ relPath,
170
+ precomputed ?? ({} as PrecomputedFileData),
171
+ rootDir,
172
+ caller,
173
+ );
174
+ if (record) yield record;
151
175
  }
152
176
 
153
- // Also include metadata-only updates (self-heal mtime/size without re-parse)
177
+ // Metadata-only updates (self-heal mtime/size without re-parse)
154
178
  for (const item of metadataUpdates) {
155
- const mtime = item.stat ? item.stat.mtime : 0;
156
- const size = item.stat ? item.stat.size : 0;
157
- fileHashes.push({ file: item.relPath, hash: item.hash, mtime, size });
179
+ yield {
180
+ file: item.relPath,
181
+ hash: item.hash,
182
+ mtime: item.stat ? item.stat.mtime : 0,
183
+ size: item.stat ? item.stat.size : 0,
184
+ };
158
185
  }
186
+ }
159
187
 
160
- return fileHashes;
188
+ /**
189
+ * Build file hash entries for every collected file, including those that
190
+ * produced zero symbols (empty files, parsers that silently no-op'd, or
191
+ * optional-language extensions whose grammar wasn't installed). Iterating the
192
+ * symbol map instead would skip such files and leave them missing from
193
+ * `file_hashes`, which permanently breaks the JS-side fast-skip pre-flight on
194
+ * any subsequent no-op rebuild (#1068).
195
+ *
196
+ * Exported for unit testing.
197
+ */
198
+ export function buildFileHashes(
199
+ filesToParse: FileToParse[],
200
+ precomputedData: Map<string, PrecomputedFileData>,
201
+ metadataUpdates: MetadataUpdate[],
202
+ rootDir: string,
203
+ ): FileHashRecord[] {
204
+ return [
205
+ ...iterFileHashRecords(
206
+ filesToParse,
207
+ precomputedData,
208
+ metadataUpdates,
209
+ rootDir,
210
+ 'buildFileHashes',
211
+ ),
212
+ ];
161
213
  }
162
214
 
163
215
  // ── Native fast-path ─────────────────────────────────────────────────
@@ -260,36 +312,38 @@ function insertDefinitionsAndExports(
260
312
 
261
313
  // ── JS fallback: Phase 2+3 ──────────────────────────────────────────
262
314
 
263
- function insertChildrenAndEdges(
315
+ /** Build the in-memory `name|kind|line` → node-id map for a single file. */
316
+ function loadFileNodeIdMap(db: BetterSqlite3Database, relPath: string): Map<string, number> {
317
+ const map = new Map<string, number>();
318
+ for (const row of bulkNodeIdsByFile(db, relPath)) {
319
+ map.set(`${row.name}|${row.kind}|${row.line}`, row.id);
320
+ }
321
+ return map;
322
+ }
323
+
324
+ /**
325
+ * First pass: for every file, emit file→def containment edges and collect
326
+ * the child-node insertion rows.
327
+ */
328
+ function collectChildRowsAndFileEdges(
264
329
  db: BetterSqlite3Database,
265
330
  allSymbols: Map<string, ExtractorOutput>,
331
+ childRows: unknown[][],
332
+ edgeRows: unknown[][],
266
333
  ): void {
267
- const childRows: unknown[][] = [];
268
- const edgeRows: unknown[][] = [];
269
-
270
334
  for (const [relPath, symbols] of allSymbols) {
271
- // First pass: collect file→def edges and child rows
272
- const nodeIdMap = new Map<string, number>();
273
- for (const row of bulkNodeIdsByFile(db, relPath)) {
274
- nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
275
- }
276
-
335
+ const nodeIdMap = loadFileNodeIdMap(db, relPath);
277
336
  const fileId = nodeIdMap.get(`${relPath}|file|0`);
278
337
 
279
338
  for (const def of symbols.definitions) {
280
339
  const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
281
340
 
282
- // Containment edge: file -> definition
283
341
  if (fileId && defId) {
284
342
  edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
285
343
  }
286
-
287
- if (!def.children?.length) continue;
288
- if (!defId) continue;
344
+ if (!def.children?.length || !defId) continue;
289
345
 
290
346
  for (const child of def.children) {
291
- // Child node
292
- const qualifiedName = `${def.name}.${child.name}`;
293
347
  childRows.push([
294
348
  child.name,
295
349
  child.kind,
@@ -297,39 +351,55 @@ function insertChildrenAndEdges(
297
351
  child.line,
298
352
  child.endLine || null,
299
353
  defId,
300
- qualifiedName,
354
+ `${def.name}.${child.name}`,
301
355
  def.name,
302
356
  child.visibility || null,
303
357
  ]);
304
358
  }
305
359
  }
306
360
  }
361
+ }
307
362
 
308
- // Insert children first (so they exist for edge lookup)
309
- batchInsertNodes(db, childRows);
310
-
311
- // Now re-fetch IDs to include newly-inserted children, then add child edges
363
+ /**
364
+ * Second pass (after child nodes have been inserted): emit def→child
365
+ * containment edges and child→def `parameter_of` edges.
366
+ */
367
+ function collectChildEdges(
368
+ db: BetterSqlite3Database,
369
+ allSymbols: Map<string, ExtractorOutput>,
370
+ edgeRows: unknown[][],
371
+ ): void {
312
372
  for (const [relPath, symbols] of allSymbols) {
313
- const nodeIdMap = new Map<string, number>();
314
- for (const row of bulkNodeIdsByFile(db, relPath)) {
315
- nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
316
- }
373
+ const nodeIdMap = loadFileNodeIdMap(db, relPath);
317
374
  for (const def of symbols.definitions) {
318
375
  if (!def.children?.length) continue;
319
376
  const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
320
377
  if (!defId) continue;
321
378
  for (const child of def.children) {
322
379
  const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
323
- if (childId) {
324
- edgeRows.push([defId, childId, 'contains', 1.0, 0]);
325
- if (child.kind === 'parameter') {
326
- edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
327
- }
380
+ if (!childId) continue;
381
+ edgeRows.push([defId, childId, 'contains', 1.0, 0]);
382
+ if (child.kind === 'parameter') {
383
+ edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
328
384
  }
329
385
  }
330
386
  }
331
387
  }
388
+ }
389
+
390
+ function insertChildrenAndEdges(
391
+ db: BetterSqlite3Database,
392
+ allSymbols: Map<string, ExtractorOutput>,
393
+ ): void {
394
+ const childRows: unknown[][] = [];
395
+ const edgeRows: unknown[][] = [];
332
396
 
397
+ collectChildRowsAndFileEdges(db, allSymbols, childRows, edgeRows);
398
+
399
+ // Insert children first (so they exist for edge lookup)
400
+ batchInsertNodes(db, childRows);
401
+
402
+ collectChildEdges(db, allSymbols, edgeRows);
333
403
  batchInsertEdges(db, edgeRows);
334
404
  }
335
405
 
@@ -348,50 +418,14 @@ function updateFileHashes(
348
418
  // Iterate every collected file (#1068): files that produced zero symbols
349
419
  // (empty, parser no-op, or grammar-missing optional language) still need a
350
420
  // hash row, otherwise the next no-op rebuild's fast-skip pre-flight rejects.
351
- const seen = new Set<string>();
352
- for (const item of filesToParse) {
353
- const relPath = item.relPath ?? normalizePath(path.relative(rootDir, item.file));
354
- if (seen.has(relPath)) continue;
355
- seen.add(relPath);
356
-
357
- const precomputed = precomputedData.get(relPath);
358
- if (precomputed?._reverseDepOnly) {
359
- // no-op: file unchanged, hash already correct
360
- } else if (precomputed?.hash) {
361
- let mtime: number;
362
- let size: number;
363
- if (precomputed.stat) {
364
- mtime = precomputed.stat.mtime;
365
- size = precomputed.stat.size;
366
- } else {
367
- const rawStat = fileStat(path.join(rootDir, relPath));
368
- mtime = rawStat ? rawStat.mtime : 0;
369
- size = rawStat ? rawStat.size : 0;
370
- }
371
- upsertHash.run(relPath, precomputed.hash, mtime, size);
372
- } else {
373
- const absPath = path.join(rootDir, relPath);
374
- let code: string | null;
375
- try {
376
- code = readFileSafe(absPath);
377
- } catch (e) {
378
- debug(`updateFileHashes: readFileSafe failed for ${relPath}: ${toErrorMessage(e)}`);
379
- code = null;
380
- }
381
- if (code !== null) {
382
- const stat = fileStat(absPath);
383
- const mtime = stat ? stat.mtime : 0;
384
- const size = stat ? stat.size : 0;
385
- upsertHash.run(relPath, fileHash(code), mtime, size);
386
- }
387
- }
388
- }
389
-
390
- // Also update metadata-only entries (self-heal mtime/size without re-parse)
391
- for (const item of metadataUpdates) {
392
- const mtime = item.stat ? item.stat.mtime : 0;
393
- const size = item.stat ? item.stat.size : 0;
394
- upsertHash.run(item.relPath, item.hash, mtime, size);
421
+ for (const record of iterFileHashRecords(
422
+ filesToParse,
423
+ precomputedData,
424
+ metadataUpdates,
425
+ rootDir,
426
+ 'updateFileHashes',
427
+ )) {
428
+ upsertHash.run(record.file, record.hash, record.mtime, record.size);
395
429
  }
396
430
  }
397
431
 
@@ -0,0 +1,74 @@
1
+ /**
2
+ * NativeDatabase connection lifecycle helpers.
3
+ *
4
+ * The Rust orchestrator and the JS pipeline stages both juggle the same
5
+ * `nativeDb` handle (rusqlite) alongside `ctx.db` (better-sqlite3). These
6
+ * helpers centralise the open/close/reopen sequence so both call sites
7
+ * preserve the same WAL-safety invariants:
8
+ *
9
+ * - Always checkpoint WAL before closing rusqlite — otherwise better-sqlite3's
10
+ * internal WAL index can drift and surface as SQLITE_CORRUPT on the next
11
+ * read (#715, #736).
12
+ * - Always reopen better-sqlite3 after rusqlite writes to drop the stale
13
+ * page cache.
14
+ *
15
+ * Lives in its own module so `tryNativeOrchestrator` (in `native-orchestrator.ts`)
16
+ * and the JS pipeline stages driver (in `pipeline.ts`) can share the helpers
17
+ * without either file importing the other.
18
+ */
19
+ import { openDb } from '../../../../db/index.js';
20
+ import { debug } from '../../../../infrastructure/logger.js';
21
+ import { loadNative } from '../../../../infrastructure/native.js';
22
+ import { toErrorMessage } from '../../../../shared/errors.js';
23
+ import type { PipelineContext } from '../context.js';
24
+
25
+ /** Checkpoint WAL through rusqlite and close the native connection. */
26
+ export function closeNativeDb(ctx: PipelineContext, label: string): void {
27
+ if (!ctx.nativeDb) return;
28
+ try {
29
+ ctx.nativeDb.exec('PRAGMA wal_checkpoint(TRUNCATE)');
30
+ } catch (e) {
31
+ debug(`${label} WAL checkpoint failed: ${toErrorMessage(e)}`);
32
+ }
33
+ try {
34
+ ctx.nativeDb.close();
35
+ } catch (e) {
36
+ debug(`${label} nativeDb close failed: ${toErrorMessage(e)}`);
37
+ }
38
+ ctx.nativeDb = undefined;
39
+ }
40
+
41
+ /** Try to reopen the native connection for a given pipeline phase. */
42
+ export function reopenNativeDb(ctx: PipelineContext, label: string): void {
43
+ if ((ctx.opts.engine ?? 'auto') === 'wasm') return;
44
+ const native = loadNative();
45
+ if (!native?.NativeDatabase) return;
46
+ try {
47
+ ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
48
+ } catch (e) {
49
+ debug(`reopen nativeDb for ${label} failed: ${toErrorMessage(e)}`);
50
+ ctx.nativeDb = undefined;
51
+ }
52
+ }
53
+
54
+ /** Close nativeDb and clear stale references in engineOpts. */
55
+ export function suspendNativeDb(ctx: PipelineContext, label: string): void {
56
+ closeNativeDb(ctx, label);
57
+ if (ctx.engineOpts?.nativeDb) {
58
+ ctx.engineOpts.nativeDb = undefined;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * After native writes, reopen the JS db connection to get a fresh page cache.
64
+ * Rusqlite WAL truncation invalidates better-sqlite3's internal WAL index,
65
+ * causing SQLITE_CORRUPT on the next read (#715, #736).
66
+ */
67
+ export function refreshJsDb(ctx: PipelineContext): void {
68
+ try {
69
+ ctx.db.close();
70
+ } catch (e) {
71
+ debug(`refreshJsDb close failed: ${toErrorMessage(e)}`);
72
+ }
73
+ ctx.db = openDb(ctx.dbPath);
74
+ }