@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
@@ -139,18 +139,25 @@ interface OwnersDataOpts {
139
139
  boundary?: boolean;
140
140
  }
141
141
 
142
- export function ownersData(
143
- customDbPath?: string,
144
- opts: OwnersDataOpts = {},
145
- ): {
142
+ interface OwnedSymbol {
143
+ name: string;
144
+ kind: string;
145
+ file: string;
146
+ line: number;
147
+ owners: string[];
148
+ }
149
+
150
+ interface OwnerBoundary {
151
+ from: OwnedSymbol;
152
+ to: OwnedSymbol;
153
+ edgeKind: string;
154
+ }
155
+
156
+ interface OwnersDataResult {
146
157
  codeownersFile: string | null;
147
158
  files: { file: string; owners: string[] }[];
148
- symbols: { name: string; kind: string; file: string; line: number; owners: string[] }[];
149
- boundaries: {
150
- from: { name: string; kind: string; file: string; line: number; owners: string[] };
151
- to: { name: string; kind: string; file: string; line: number; owners: string[] };
152
- edgeKind: string;
153
- }[];
159
+ symbols: OwnedSymbol[];
160
+ boundaries: OwnerBoundary[];
154
161
  summary: {
155
162
  totalFiles: number;
156
163
  ownedFiles: number;
@@ -159,160 +166,193 @@ export function ownersData(
159
166
  ownerCount: number;
160
167
  byOwner: { owner: string; fileCount: number }[];
161
168
  };
169
+ }
170
+
171
+ interface BetterSqlite3DatabaseLike {
172
+ prepare(sql: string): { all(...params: unknown[]): unknown[] };
173
+ close(): void;
174
+ }
175
+
176
+ function emptyOwnersResult(codeownersFile: string | null): OwnersDataResult {
177
+ return {
178
+ codeownersFile,
179
+ files: [],
180
+ symbols: [],
181
+ boundaries: [],
182
+ summary: {
183
+ totalFiles: 0,
184
+ ownedFiles: 0,
185
+ unownedFiles: 0,
186
+ coveragePercent: 0,
187
+ ownerCount: 0,
188
+ byOwner: [],
189
+ },
190
+ };
191
+ }
192
+
193
+ /** Load all distinct files from the DB and apply test/file filters. */
194
+ function loadFilteredFiles(db: BetterSqlite3DatabaseLike, opts: OwnersDataOpts): string[] {
195
+ let allFiles = (db.prepare('SELECT DISTINCT file FROM nodes').all() as { file: string }[]).map(
196
+ (r) => r.file,
197
+ );
198
+ if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
199
+ const fileFilters = normalizeFileFilter(opts.file);
200
+ if (fileFilters.length > 0) {
201
+ allFiles = allFiles.filter((f) => fileFilters.some((filter) => f.includes(filter)));
202
+ }
203
+ return allFiles;
204
+ }
205
+
206
+ /** Build owner index (owner -> list of files) and count owned files. */
207
+ function buildOwnerIndex(fileOwners: { file: string; owners: string[] }[]): {
208
+ ownerIndex: Map<string, string[]>;
209
+ ownedCount: number;
162
210
  } {
211
+ const ownerIndex = new Map<string, string[]>();
212
+ let ownedCount = 0;
213
+ for (const fo of fileOwners) {
214
+ if (fo.owners.length > 0) ownedCount++;
215
+ for (const o of fo.owners) {
216
+ if (!ownerIndex.has(o)) ownerIndex.set(o, []);
217
+ ownerIndex.get(o)!.push(fo.file);
218
+ }
219
+ }
220
+ return { ownerIndex, ownedCount };
221
+ }
222
+
223
+ /** Load symbols restricted to the given file set, applying noTests and kind filters. */
224
+ function loadSymbolsForFiles(
225
+ db: BetterSqlite3DatabaseLike,
226
+ fileSet: Set<string>,
227
+ opts: OwnersDataOpts,
228
+ rules: CodeownersRule[],
229
+ ): OwnedSymbol[] {
230
+ let symbols = (
231
+ db.prepare('SELECT name, kind, file, line FROM nodes').all() as {
232
+ name: string;
233
+ kind: string;
234
+ file: string;
235
+ line: number;
236
+ }[]
237
+ ).filter((n) => fileSet.has(n.file));
238
+
239
+ if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
240
+ if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
241
+
242
+ return symbols.map((s) => ({ ...s, owners: matchOwners(s.file, rules) }));
243
+ }
244
+
245
+ interface CallEdgeRow {
246
+ id: number;
247
+ edgeKind: string;
248
+ srcName: string;
249
+ srcKind: string;
250
+ srcFile: string;
251
+ srcLine: number;
252
+ tgtName: string;
253
+ tgtKind: string;
254
+ tgtFile: string;
255
+ tgtLine: number;
256
+ }
257
+
258
+ /** Compute cross-owner call boundaries. Returns empty array when boundary mode is off. */
259
+ function computeOwnerBoundaries(
260
+ db: BetterSqlite3DatabaseLike,
261
+ rules: CodeownersRule[],
262
+ noTests: boolean,
263
+ ): OwnerBoundary[] {
264
+ const edges = db
265
+ .prepare(
266
+ `SELECT e.id, e.kind AS edgeKind,
267
+ s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
268
+ t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
269
+ FROM edges e
270
+ JOIN nodes s ON e.source_id = s.id
271
+ JOIN nodes t ON e.target_id = t.id
272
+ WHERE e.kind = 'calls'`,
273
+ )
274
+ .all() as CallEdgeRow[];
275
+
276
+ const boundaries: OwnerBoundary[] = [];
277
+ for (const e of edges) {
278
+ if (noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
279
+ const srcOwners = matchOwners(e.srcFile, rules);
280
+ const tgtOwners = matchOwners(e.tgtFile, rules);
281
+ // Cross-boundary: different owner sets (sort for deterministic comparison + output)
282
+ const sortedSrc = [...srcOwners].sort();
283
+ const sortedTgt = [...tgtOwners].sort();
284
+ const srcKey = sortedSrc.join(',');
285
+ const tgtKey = sortedTgt.join(',');
286
+ if (srcKey === tgtKey) continue;
287
+ boundaries.push({
288
+ from: {
289
+ name: e.srcName,
290
+ kind: e.srcKind,
291
+ file: e.srcFile,
292
+ line: e.srcLine,
293
+ owners: sortedSrc,
294
+ },
295
+ to: { name: e.tgtName, kind: e.tgtKind, file: e.tgtFile, line: e.tgtLine, owners: sortedTgt },
296
+ edgeKind: e.edgeKind,
297
+ });
298
+ }
299
+ return boundaries;
300
+ }
301
+
302
+ /** Build summary stats (totals, coverage, by-owner counts). */
303
+ function buildOwnersSummary(
304
+ totalFiles: number,
305
+ ownedCount: number,
306
+ ownerIndex: Map<string, string[]>,
307
+ ): OwnersDataResult['summary'] {
308
+ const byOwner = [...ownerIndex.entries()]
309
+ .map(([owner, files]) => ({ owner, fileCount: files.length }))
310
+ .sort((a, b) => b.fileCount - a.fileCount);
311
+
312
+ return {
313
+ totalFiles,
314
+ ownedFiles: ownedCount,
315
+ unownedFiles: totalFiles - ownedCount,
316
+ coveragePercent: totalFiles > 0 ? Math.round((ownedCount / totalFiles) * 100) : 0,
317
+ ownerCount: ownerIndex.size,
318
+ byOwner,
319
+ };
320
+ }
321
+
322
+ export function ownersData(customDbPath?: string, opts: OwnersDataOpts = {}): OwnersDataResult {
163
323
  const db = openReadonlyOrFail(customDbPath);
164
324
  try {
165
325
  const dbPath = findDbPath(customDbPath);
166
326
  const repoRoot = path.resolve(path.dirname(dbPath), '..');
167
327
 
168
328
  const parsed = parseCodeowners(repoRoot);
169
- if (!parsed) {
170
- return {
171
- codeownersFile: null,
172
- files: [],
173
- symbols: [],
174
- boundaries: [],
175
- summary: {
176
- totalFiles: 0,
177
- ownedFiles: 0,
178
- unownedFiles: 0,
179
- coveragePercent: 0,
180
- ownerCount: 0,
181
- byOwner: [],
182
- },
183
- };
184
- }
185
-
186
- // Get all distinct files from nodes
187
- let allFiles = (db.prepare('SELECT DISTINCT file FROM nodes').all() as { file: string }[]).map(
188
- (r) => r.file,
189
- );
329
+ if (!parsed) return emptyOwnersResult(null);
190
330
 
191
- if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
192
- const fileFilters = normalizeFileFilter(opts.file);
193
- if (fileFilters.length > 0) {
194
- allFiles = allFiles.filter((f) => fileFilters.some((filter) => f.includes(filter)));
195
- }
196
-
197
- // Map files to owners
198
- const fileOwners = allFiles.map((file) => ({
199
- file,
200
- owners: matchOwners(file, parsed.rules),
201
- }));
202
-
203
- // Build owner-to-files index
204
- const ownerIndex = new Map<string, string[]>();
205
- let ownedCount = 0;
206
- for (const fo of fileOwners) {
207
- if (fo.owners.length > 0) ownedCount++;
208
- for (const o of fo.owners) {
209
- if (!ownerIndex.has(o)) ownerIndex.set(o, []);
210
- ownerIndex.get(o)!.push(fo.file);
211
- }
212
- }
331
+ // Stage 1: load files and bucket them by owner
332
+ const allFiles = loadFilteredFiles(db, opts);
333
+ const fileOwners = allFiles.map((file) => ({ file, owners: matchOwners(file, parsed.rules) }));
334
+ const { ownerIndex, ownedCount } = buildOwnerIndex(fileOwners);
213
335
 
214
- // Filter files if --owner specified
215
- let filteredFiles = fileOwners;
216
- if (opts.owner) {
217
- filteredFiles = fileOwners.filter((fo) => fo.owners.includes(opts.owner!));
218
- }
336
+ // Stage 2: apply optional --owner filter
337
+ const filteredFiles = opts.owner
338
+ ? fileOwners.filter((fo) => fo.owners.includes(opts.owner!))
339
+ : fileOwners;
219
340
 
220
- // Get symbols for filtered files
341
+ // Stage 3: load symbols for filtered files
221
342
  const fileSet = new Set(filteredFiles.map((fo) => fo.file));
222
- let symbols = (
223
- db.prepare('SELECT name, kind, file, line FROM nodes').all() as {
224
- name: string;
225
- kind: string;
226
- file: string;
227
- line: number;
228
- }[]
229
- ).filter((n) => fileSet.has(n.file));
230
-
231
- if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
232
- if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
233
-
234
- const symbolsWithOwners = symbols.map((s) => ({
235
- ...s,
236
- owners: matchOwners(s.file, parsed.rules),
237
- }));
238
-
239
- // Boundary analysis — cross-owner call edges
240
- const boundaries: {
241
- from: { name: string; kind: string; file: string; line: number; owners: string[] };
242
- to: { name: string; kind: string; file: string; line: number; owners: string[] };
243
- edgeKind: string;
244
- }[] = [];
245
- if (opts.boundary) {
246
- const edges = db
247
- .prepare(
248
- `SELECT e.id, e.kind AS edgeKind,
249
- s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
250
- t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
251
- FROM edges e
252
- JOIN nodes s ON e.source_id = s.id
253
- JOIN nodes t ON e.target_id = t.id
254
- WHERE e.kind = 'calls'`,
255
- )
256
- .all() as {
257
- id: number;
258
- edgeKind: string;
259
- srcName: string;
260
- srcKind: string;
261
- srcFile: string;
262
- srcLine: number;
263
- tgtName: string;
264
- tgtKind: string;
265
- tgtFile: string;
266
- tgtLine: number;
267
- }[];
268
-
269
- for (const e of edges) {
270
- if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
271
- const srcOwners = matchOwners(e.srcFile, parsed.rules);
272
- const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
273
- // Cross-boundary: different owner sets
274
- const srcKey = srcOwners.sort().join(',');
275
- const tgtKey = tgtOwners.sort().join(',');
276
- if (srcKey !== tgtKey) {
277
- boundaries.push({
278
- from: {
279
- name: e.srcName,
280
- kind: e.srcKind,
281
- file: e.srcFile,
282
- line: e.srcLine,
283
- owners: srcOwners,
284
- },
285
- to: {
286
- name: e.tgtName,
287
- kind: e.tgtKind,
288
- file: e.tgtFile,
289
- line: e.tgtLine,
290
- owners: tgtOwners,
291
- },
292
- edgeKind: e.edgeKind,
293
- });
294
- }
295
- }
296
- }
343
+ const symbolsWithOwners = loadSymbolsForFiles(db, fileSet, opts, parsed.rules);
297
344
 
298
- // Summary
299
- const byOwner = [...ownerIndex.entries()]
300
- .map(([owner, files]) => ({ owner, fileCount: files.length }))
301
- .sort((a, b) => b.fileCount - a.fileCount);
345
+ // Stage 4: optional boundary analysis (cross-owner call edges)
346
+ const boundaries = opts.boundary
347
+ ? computeOwnerBoundaries(db, parsed.rules, opts.noTests ?? false)
348
+ : [];
302
349
 
303
350
  return {
304
351
  codeownersFile: parsed.path,
305
352
  files: filteredFiles,
306
353
  symbols: symbolsWithOwners,
307
354
  boundaries,
308
- summary: {
309
- totalFiles: allFiles.length,
310
- ownedFiles: ownedCount,
311
- unownedFiles: allFiles.length - ownedCount,
312
- coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
313
- ownerCount: ownerIndex.size,
314
- byOwner,
315
- },
355
+ summary: buildOwnersSummary(allFiles.length, ownedCount, ownerIndex),
316
356
  };
317
357
  } finally {
318
358
  db.close();
@@ -91,6 +91,40 @@ interface BfsResult {
91
91
  truncated: boolean;
92
92
  }
93
93
 
94
+ type CalleeNode = { id: number; name: string; file: string; kind: string; line: number };
95
+
96
+ interface BfsFrame {
97
+ visited: Set<number>;
98
+ messages: SequenceMessage[];
99
+ fileSet: Set<string>;
100
+ idToNode: Map<number, CalleeNode>;
101
+ nextFrontier: number[];
102
+ }
103
+
104
+ function processCallee(
105
+ c: CalleeNode,
106
+ caller: CalleeNode,
107
+ depth: number,
108
+ noTests: boolean,
109
+ frame: BfsFrame,
110
+ ): void {
111
+ if (noTests && isTestFile(c.file)) return;
112
+
113
+ frame.fileSet.add(c.file);
114
+ frame.messages.push({
115
+ from: caller.file,
116
+ to: c.file,
117
+ label: c.name,
118
+ type: 'call',
119
+ depth,
120
+ });
121
+
122
+ if (frame.visited.has(c.id)) return;
123
+ frame.visited.add(c.id);
124
+ frame.nextFrontier.push(c.id);
125
+ frame.idToNode.set(c.id, c);
126
+ }
127
+
94
128
  function bfsCallees(
95
129
  repo: Repository,
96
130
  matchNode: MatchNode,
@@ -101,46 +135,25 @@ function bfsCallees(
101
135
  let frontier = [matchNode.id];
102
136
  const messages: SequenceMessage[] = [];
103
137
  const fileSet = new Set<string>([matchNode.file]);
104
- const idToNode = new Map<
105
- number,
106
- { id: number; name: string; file: string; kind: string; line: number }
107
- >();
138
+ const idToNode = new Map<number, CalleeNode>();
108
139
  idToNode.set(matchNode.id, matchNode);
109
140
  let truncated = false;
110
141
 
111
142
  for (let d = 1; d <= maxDepth; d++) {
112
- const nextFrontier: number[] = [];
143
+ const frame: BfsFrame = { visited, messages, fileSet, idToNode, nextFrontier: [] };
113
144
 
114
145
  for (const fid of frontier) {
115
- const callees = repo.findCallees(fid);
116
146
  const caller = idToNode.get(fid)!;
117
-
118
- for (const c of callees) {
119
- if (noTests && isTestFile(c.file)) continue;
120
-
121
- fileSet.add(c.file);
122
- messages.push({
123
- from: caller.file,
124
- to: c.file,
125
- label: c.name,
126
- type: 'call',
127
- depth: d,
128
- });
129
-
130
- if (visited.has(c.id)) continue;
131
-
132
- visited.add(c.id);
133
- nextFrontier.push(c.id);
134
- idToNode.set(c.id, c);
147
+ for (const c of repo.findCallees(fid)) {
148
+ processCallee(c, caller, d, noTests, frame);
135
149
  }
136
150
  }
137
151
 
138
- frontier = nextFrontier;
152
+ frontier = frame.nextFrontier;
139
153
  if (frontier.length === 0) break;
140
154
 
141
- if (d === maxDepth && frontier.length > 0) {
142
- const hasMoreCalls = frontier.some((fid) => repo.findCallees(fid).length > 0);
143
- if (hasMoreCalls) truncated = true;
155
+ if (d === maxDepth && frontier.some((fid) => repo.findCallees(fid).length > 0)) {
156
+ truncated = true;
144
157
  }
145
158
  }
146
159
 
@@ -174,26 +187,16 @@ function annotateDataflow(
174
187
  }
175
188
  }
176
189
 
177
- function _annotateDataflowImpl(
178
- db: BetterSqlite3Database,
190
+ type DataflowStmts = {
191
+ getReturns: ReturnType<BetterSqlite3Database['prepare']>;
192
+ getFlowsTo: ReturnType<BetterSqlite3Database['prepare']>;
193
+ };
194
+
195
+ function appendReturnMessages(
179
196
  messages: SequenceMessage[],
180
- idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
197
+ nodeByNameFile: Map<string, { id: number; name: string; file: string }>,
198
+ stmts: DataflowStmts,
181
199
  ): void {
182
- const nodeByNameFile = new Map<string, { id: number; name: string; file: string }>();
183
- for (const n of idToNode.values()) {
184
- nodeByNameFile.set(`${n.name}|${n.file}`, n);
185
- }
186
-
187
- const getReturns = db.prepare(
188
- `SELECT d.expression FROM dataflow d
189
- WHERE d.source_id = ? AND d.kind = 'returns'`,
190
- );
191
- const getFlowsTo = db.prepare(
192
- `SELECT d.expression FROM dataflow d
193
- WHERE d.target_id = ? AND d.kind = 'flows_to'
194
- ORDER BY d.param_index`,
195
- );
196
-
197
200
  const seenReturns = new Set<string>();
198
201
  for (const msg of [...messages]) {
199
202
  if (msg.type !== 'call') continue;
@@ -203,40 +206,67 @@ function _annotateDataflowImpl(
203
206
  const returnKey = `${msg.to}->${msg.from}:${msg.label}`;
204
207
  if (seenReturns.has(returnKey)) continue;
205
208
 
206
- const returns = getReturns.all(targetNode.id) as { expression: string }[];
207
-
208
- if (returns.length > 0) {
209
- seenReturns.add(returnKey);
210
- const expr = returns[0]!.expression || 'result';
211
- messages.push({
212
- from: msg.to,
213
- to: msg.from,
214
- label: expr,
215
- type: 'return',
216
- depth: msg.depth,
217
- });
218
- }
209
+ const returns = stmts.getReturns.all(targetNode.id) as { expression: string }[];
210
+ if (returns.length === 0) continue;
211
+
212
+ seenReturns.add(returnKey);
213
+ messages.push({
214
+ from: msg.to,
215
+ to: msg.from,
216
+ label: returns[0]!.expression || 'result',
217
+ type: 'return',
218
+ depth: msg.depth,
219
+ });
219
220
  }
221
+ }
220
222
 
223
+ function annotateCallParams(
224
+ messages: SequenceMessage[],
225
+ nodeByNameFile: Map<string, { id: number; name: string; file: string }>,
226
+ stmts: DataflowStmts,
227
+ ): void {
221
228
  for (const msg of messages) {
222
229
  if (msg.type !== 'call') continue;
223
230
  const targetNode = nodeByNameFile.get(`${msg.label}|${msg.to}`);
224
231
  if (!targetNode) continue;
225
232
 
226
- const params = getFlowsTo.all(targetNode.id) as { expression: string }[];
227
-
228
- if (params.length > 0) {
229
- const paramNames = params
230
- .map((p) => p.expression)
231
- .filter(Boolean)
232
- .slice(0, 3);
233
- if (paramNames.length > 0) {
234
- msg.label = `${msg.label}(${paramNames.join(', ')})`;
235
- }
233
+ const params = stmts.getFlowsTo.all(targetNode.id) as { expression: string }[];
234
+ const paramNames = params
235
+ .map((p) => p.expression)
236
+ .filter(Boolean)
237
+ .slice(0, 3);
238
+ if (paramNames.length > 0) {
239
+ msg.label = `${msg.label}(${paramNames.join(', ')})`;
236
240
  }
237
241
  }
238
242
  }
239
243
 
244
+ function _annotateDataflowImpl(
245
+ db: BetterSqlite3Database,
246
+ messages: SequenceMessage[],
247
+ idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
248
+ ): void {
249
+ const nodeByNameFile = new Map<string, { id: number; name: string; file: string }>();
250
+ for (const n of idToNode.values()) {
251
+ nodeByNameFile.set(`${n.name}|${n.file}`, n);
252
+ }
253
+
254
+ const stmts: DataflowStmts = {
255
+ getReturns: db.prepare(
256
+ `SELECT d.expression FROM dataflow d
257
+ WHERE d.source_id = ? AND d.kind = 'returns'`,
258
+ ),
259
+ getFlowsTo: db.prepare(
260
+ `SELECT d.expression FROM dataflow d
261
+ WHERE d.target_id = ? AND d.kind = 'flows_to'
262
+ ORDER BY d.param_index`,
263
+ ),
264
+ };
265
+
266
+ appendReturnMessages(messages, nodeByNameFile, stmts);
267
+ annotateCallParams(messages, nodeByNameFile, stmts);
268
+ }
269
+
240
270
  interface Participant {
241
271
  id: string;
242
272
  label: string;