@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
@@ -405,6 +405,79 @@ function handleReturn(
405
405
  }
406
406
  }
407
407
 
408
+ /** Collect parameter entries for a function and push a new scope onto the stack. */
409
+ function enterFunctionScope(
410
+ funcNode: TreeSitterNode,
411
+ rules: AnyRules,
412
+ scopeStack: ScopeEntry[],
413
+ parameters: DataflowParam[],
414
+ ): void {
415
+ const name = functionName(funcNode, rules);
416
+ const paramsNode = funcNode.childForFieldName(rules.paramListField);
417
+ const paramList = extractParams(paramsNode, rules);
418
+ const paramMap = new Map<string, number>();
419
+ for (const p of paramList) {
420
+ paramMap.set(p.name, p.index);
421
+ if (name) {
422
+ parameters.push({
423
+ funcName: name,
424
+ paramName: p.name,
425
+ paramIndex: p.index,
426
+ line: (paramsNode?.startPosition?.row ?? funcNode.startPosition.row) + 1,
427
+ });
428
+ }
429
+ }
430
+ scopeStack.push({ funcName: name, funcNode, params: paramMap, locals: new Map() });
431
+ }
432
+
433
+ interface DataflowDispatchCtx {
434
+ rules: AnyRules;
435
+ scopeStack: ScopeEntry[];
436
+ returns: DataflowReturnEntry[];
437
+ assignments: DataflowAssignment[];
438
+ argFlows: DataflowArgFlow[];
439
+ mutations: DataflowMutation[];
440
+ isCallNode: (t: string) => boolean;
441
+ }
442
+
443
+ /**
444
+ * Route a node to the appropriate dataflow handler based on its type.
445
+ * Function-definition nodes are short-circuited with an early return.
446
+ */
447
+ function dispatchDataflowNode(ctx: DataflowDispatchCtx, node: TreeSitterNode): void {
448
+ const { rules } = ctx;
449
+ const t = node.type;
450
+
451
+ if (rules.functionNodes.has(t)) return;
452
+
453
+ if (rules.returnNode && t === rules.returnNode) {
454
+ handleReturn(node, rules, ctx.scopeStack, ctx.returns);
455
+ return;
456
+ }
457
+
458
+ if (
459
+ (rules.varDeclaratorNode && t === rules.varDeclaratorNode) ||
460
+ rules.varDeclaratorNodes?.has(t)
461
+ ) {
462
+ handleVarDeclarator(node, rules, ctx.scopeStack, ctx.assignments, ctx.isCallNode);
463
+ return;
464
+ }
465
+
466
+ if (ctx.isCallNode(t)) {
467
+ handleCallExpr(node, rules, ctx.scopeStack, ctx.argFlows);
468
+ return;
469
+ }
470
+
471
+ if (rules.assignmentNode && t === rules.assignmentNode) {
472
+ handleAssignment(node, rules, ctx.scopeStack, ctx.assignments, ctx.mutations, ctx.isCallNode);
473
+ return;
474
+ }
475
+
476
+ if (rules.expressionStmtNode && t === rules.expressionStmtNode) {
477
+ handleExprStmtMutation(node, rules, ctx.scopeStack, ctx.mutations, ctx.isCallNode);
478
+ }
479
+ }
480
+
408
481
  export function createDataflowVisitor(rules: AnyRules): Visitor {
409
482
  const isCallNode: (t: string) => boolean = rules.callNodes
410
483
  ? (t: string) => rules.callNodes.has(t)
@@ -417,6 +490,16 @@ export function createDataflowVisitor(rules: AnyRules): Visitor {
417
490
  const mutations: DataflowMutation[] = [];
418
491
  const scopeStack: ScopeEntry[] = [];
419
492
 
493
+ const dispatchCtx: DataflowDispatchCtx = {
494
+ rules,
495
+ scopeStack,
496
+ returns,
497
+ assignments,
498
+ argFlows,
499
+ mutations,
500
+ isCallNode,
501
+ };
502
+
420
503
  return {
421
504
  name: 'dataflow',
422
505
  functionNodeTypes: rules.functionNodes,
@@ -426,22 +509,7 @@ export function createDataflowVisitor(rules: AnyRules): Visitor {
426
509
  _funcName: string | null,
427
510
  _context: VisitorContext,
428
511
  ): void {
429
- const name = functionName(funcNode, rules);
430
- const paramsNode = funcNode.childForFieldName(rules.paramListField);
431
- const paramList = extractParams(paramsNode, rules);
432
- const paramMap = new Map<string, number>();
433
- for (const p of paramList) {
434
- paramMap.set(p.name, p.index);
435
- if (name) {
436
- parameters.push({
437
- funcName: name,
438
- paramName: p.name,
439
- paramIndex: p.index,
440
- line: (paramsNode?.startPosition?.row ?? funcNode.startPosition.row) + 1,
441
- });
442
- }
443
- }
444
- scopeStack.push({ funcName: name, funcNode, params: paramMap, locals: new Map() });
512
+ enterFunctionScope(funcNode, rules, scopeStack, parameters);
445
513
  },
446
514
 
447
515
  exitFunction(
@@ -453,37 +521,8 @@ export function createDataflowVisitor(rules: AnyRules): Visitor {
453
521
  },
454
522
 
455
523
  enterNode(node: TreeSitterNode, _context: VisitorContext): EnterNodeResult | undefined {
456
- const t = node.type;
457
-
458
- if (rules.functionNodes.has(t)) return;
459
-
460
- if (rules.returnNode && t === rules.returnNode) {
461
- handleReturn(node, rules, scopeStack, returns);
462
- return;
463
- }
464
-
465
- if (rules.varDeclaratorNode && t === rules.varDeclaratorNode) {
466
- handleVarDeclarator(node, rules, scopeStack, assignments, isCallNode);
467
- return;
468
- }
469
- if (rules.varDeclaratorNodes?.has(t)) {
470
- handleVarDeclarator(node, rules, scopeStack, assignments, isCallNode);
471
- return;
472
- }
473
-
474
- if (isCallNode(t)) {
475
- handleCallExpr(node, rules, scopeStack, argFlows);
476
- return;
477
- }
478
-
479
- if (rules.assignmentNode && t === rules.assignmentNode) {
480
- handleAssignment(node, rules, scopeStack, assignments, mutations, isCallNode);
481
- return;
482
- }
483
-
484
- if (rules.expressionStmtNode && t === rules.expressionStmtNode) {
485
- handleExprStmtMutation(node, rules, scopeStack, mutations, isCallNode);
486
- }
524
+ dispatchDataflowNode(dispatchCtx, node);
525
+ return undefined;
487
526
  },
488
527
 
489
528
  finish(): DataflowResultInternal {
@@ -1,7 +1,37 @@
1
1
  import path from 'node:path';
2
- import { buildEmbeddings, DEFAULT_MODEL, EMBEDDING_STRATEGIES } from '../../domain/search/index.js';
2
+ import { openReadonlyOrFail } from '../../db/index.js';
3
+ import { getEmbeddingMeta } from '../../db/repository/embeddings.js';
4
+ import {
5
+ buildEmbeddings,
6
+ DEFAULT_MODEL,
7
+ EMBEDDING_STRATEGIES,
8
+ MODELS,
9
+ } from '../../domain/search/index.js';
10
+ import { info, warn } from '../../infrastructure/logger.js';
3
11
  import type { CommandDefinition } from '../types.js';
4
12
 
13
+ function resolveStickyModel(dbPath: string | undefined): string | null {
14
+ try {
15
+ const db = openReadonlyOrFail(dbPath);
16
+ try {
17
+ const storedName = getEmbeddingMeta(db, 'model');
18
+ if (!storedName) return null;
19
+ for (const [key, cfg] of Object.entries(MODELS)) {
20
+ if (cfg.name === storedName) return key;
21
+ }
22
+ warn(
23
+ `Stored embedding model "${storedName}" is no longer recognised — falling back to default. ` +
24
+ 'Embeddings will be rebuilt with the new model.',
25
+ );
26
+ return null;
27
+ } finally {
28
+ db.close();
29
+ }
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
5
35
  export const command: CommandDefinition = {
6
36
  name: 'embed [dir]',
7
37
  description:
@@ -9,7 +39,7 @@ export const command: CommandDefinition = {
9
39
  options: [
10
40
  [
11
41
  '-m, --model <name>',
12
- 'Embedding model (default from config or minilm). Run `codegraph models` for details',
42
+ 'Embedding model. Defaults to config, then the model used by existing embeddings, then the built-in default. Run `codegraph models` for options',
13
43
  ],
14
44
  [
15
45
  '-s, --strategy <name>',
@@ -25,8 +55,28 @@ export const command: CommandDefinition = {
25
55
  },
26
56
  async execute([dir], opts, ctx) {
27
57
  const root = path.resolve(dir || '.');
58
+ const dbPath = opts.db as string | undefined;
28
59
  const embeddingsConfig = ctx.config.embeddings;
29
- const model = (opts.model as string) || (embeddingsConfig?.model as string) || DEFAULT_MODEL;
30
- await buildEmbeddings(root, model, opts.db as string | undefined, { strategy: opts.strategy });
60
+ const flagModel = opts.model as string | undefined;
61
+ const configModel = (embeddingsConfig?.model as string | null | undefined) ?? null;
62
+
63
+ let model: string;
64
+ if (flagModel) {
65
+ model = flagModel;
66
+ } else if (configModel) {
67
+ model = configModel;
68
+ } else {
69
+ const sticky = resolveStickyModel(dbPath);
70
+ if (sticky) {
71
+ info(
72
+ `Reusing previously-stored embedding model "${sticky}". Pass --model to switch, or set embeddings.model in your config.`,
73
+ );
74
+ model = sticky;
75
+ } else {
76
+ model = DEFAULT_MODEL;
77
+ }
78
+ }
79
+
80
+ await buildEmbeddings(root, model, dbPath, { strategy: opts.strategy });
31
81
  },
32
82
  };
@@ -58,9 +58,32 @@ export function fileDepsData(
58
58
  *
59
59
  * Uses Repository.findCallers() so it works with both native and WASM engines.
60
60
  */
61
+ type CallerRow = { id: number; name: string; kind: string; file: string; line: number };
62
+
63
+ /** Compute the next BFS frontier from a batched upstream-callers lookup. */
64
+ function buildNextCallerFrontier(
65
+ unvisited: CallerRow[],
66
+ batchCallers: Map<number, CallerRow[]>,
67
+ visited: Set<number>,
68
+ noTests: boolean,
69
+ ): CallerRow[] {
70
+ const nextFrontier: CallerRow[] = [];
71
+ const nextFrontierIds = new Set<number>();
72
+ for (const f of unvisited) {
73
+ const upstream = batchCallers.get(f.id) || [];
74
+ for (const u of upstream) {
75
+ if (noTests && isTestFile(u.file)) continue;
76
+ if (visited.has(u.id) || nextFrontierIds.has(u.id)) continue;
77
+ nextFrontierIds.add(u.id);
78
+ nextFrontier.push(u);
79
+ }
80
+ }
81
+ return nextFrontier;
82
+ }
83
+
61
84
  function buildTransitiveCallers(
62
85
  repo: InstanceType<typeof Repository>,
63
- callers: Array<{ id: number; name: string; kind: string; file: string; line: number }>,
86
+ callers: CallerRow[],
64
87
  nodeId: number,
65
88
  depth: number,
66
89
  noTests: boolean,
@@ -81,18 +104,8 @@ function buildTransitiveCallers(
81
104
  if (unvisited.length === 0) break;
82
105
 
83
106
  const batchCallers = repo.findCallersBatch(unvisited.map((f) => f.id));
84
- const nextFrontier: typeof frontier = [];
85
- const nextFrontierIds = new Set<number>();
86
- for (const f of unvisited) {
87
- const upstream = batchCallers.get(f.id) || [];
88
- for (const u of upstream) {
89
- if (noTests && isTestFile(u.file)) continue;
90
- if (!visited.has(u.id) && !nextFrontierIds.has(u.id)) {
91
- nextFrontierIds.add(u.id);
92
- nextFrontier.push(u);
93
- }
94
- }
95
- }
107
+ const nextFrontier = buildNextCallerFrontier(unvisited, batchCallers, visited, noTests);
108
+
96
109
  if (nextFrontier.length > 0) {
97
110
  transitiveCallers[d] = nextFrontier.map((n) => ({
98
111
  name: n.name,
@@ -258,22 +271,30 @@ function resolveEndpoints(
258
271
  };
259
272
  }
260
273
 
261
- /**
262
- * BFS from sourceId toward targetId.
263
- * Returns { found, parent, alternateCount, foundDepth }.
264
- * `parent` maps nodeId -> { parentId, edgeKind }.
265
- */
266
- function bfsShortestPath(
274
+ type NeighborRow = {
275
+ id: number;
276
+ name: string;
277
+ kind: string;
278
+ file: string;
279
+ line: number;
280
+ edge_kind: string;
281
+ };
282
+
283
+ type BfsShortestState = {
284
+ visited: Set<number>;
285
+ parent: Map<number, { parentId: number; edgeKind: string }>;
286
+ found: boolean;
287
+ foundDepth: number;
288
+ alternateCount: number;
289
+ };
290
+
291
+ /** Build the SQL statement that yields neighbors of a node id in the requested direction. */
292
+ function buildNeighborStmt(
267
293
  db: BetterSqlite3Database,
268
- sourceId: number,
269
- targetId: number,
270
294
  edgeKinds: string[],
271
295
  reverse: boolean,
272
- maxDepth: number,
273
- noTests: boolean,
274
- ) {
296
+ ): ReturnType<BetterSqlite3Database['prepare']> {
275
297
  const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
276
-
277
298
  // Forward: source_id -> target_id (A calls... calls B)
278
299
  // Reverse: target_id -> source_id (B is called by... called by A)
279
300
  const neighborQuery = reverse
@@ -283,50 +304,78 @@ function bfsShortestPath(
283
304
  : `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
284
305
  FROM edges e JOIN nodes n ON e.target_id = n.id
285
306
  WHERE e.source_id = ? AND e.kind IN (${kindPlaceholders})`;
286
- const neighborStmt = db.prepare(neighborQuery);
307
+ return db.prepare(neighborQuery);
308
+ }
309
+
310
+ /** Process a single neighbor row during BFS; returns true once the target has been reached. */
311
+ function visitNeighbor(
312
+ n: NeighborRow,
313
+ currentId: number,
314
+ depth: number,
315
+ targetId: number,
316
+ state: BfsShortestState,
317
+ nextQueue: number[],
318
+ noTests: boolean,
319
+ ): void {
320
+ if (noTests && isTestFile(n.file)) return;
321
+ if (n.id === targetId) {
322
+ if (!state.found) {
323
+ state.found = true;
324
+ state.foundDepth = depth;
325
+ state.parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
326
+ }
327
+ state.alternateCount++;
328
+ return;
329
+ }
330
+ if (state.visited.has(n.id)) return;
331
+ state.visited.add(n.id);
332
+ state.parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
333
+ nextQueue.push(n.id);
334
+ }
287
335
 
288
- const visited = new Set([sourceId]);
289
- const parent = new Map<number, { parentId: number; edgeKind: string }>();
336
+ /**
337
+ * BFS from sourceId toward targetId.
338
+ * Returns { found, parent, alternateCount, foundDepth }.
339
+ * `parent` maps nodeId -> { parentId, edgeKind }.
340
+ */
341
+ function bfsShortestPath(
342
+ db: BetterSqlite3Database,
343
+ sourceId: number,
344
+ targetId: number,
345
+ edgeKinds: string[],
346
+ reverse: boolean,
347
+ maxDepth: number,
348
+ noTests: boolean,
349
+ ) {
350
+ const neighborStmt = buildNeighborStmt(db, edgeKinds, reverse);
351
+ const state: BfsShortestState = {
352
+ visited: new Set([sourceId]),
353
+ parent: new Map(),
354
+ found: false,
355
+ foundDepth: -1,
356
+ alternateCount: 0,
357
+ };
290
358
  let queue = [sourceId];
291
- let found = false;
292
- let alternateCount = 0;
293
- let foundDepth = -1;
294
359
 
295
360
  for (let depth = 1; depth <= maxDepth; depth++) {
296
361
  const nextQueue: number[] = [];
297
362
  for (const currentId of queue) {
298
- const neighbors = neighborStmt.all(currentId, ...edgeKinds) as Array<{
299
- id: number;
300
- name: string;
301
- kind: string;
302
- file: string;
303
- line: number;
304
- edge_kind: string;
305
- }>;
363
+ const neighbors = neighborStmt.all(currentId, ...edgeKinds) as NeighborRow[];
306
364
  for (const n of neighbors) {
307
- if (noTests && isTestFile(n.file)) continue;
308
- if (n.id === targetId) {
309
- if (!found) {
310
- found = true;
311
- foundDepth = depth;
312
- parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
313
- }
314
- alternateCount++;
315
- continue;
316
- }
317
- if (!visited.has(n.id)) {
318
- visited.add(n.id);
319
- parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
320
- nextQueue.push(n.id);
321
- }
365
+ visitNeighbor(n, currentId, depth, targetId, state, nextQueue, noTests);
322
366
  }
323
367
  }
324
- if (found) break;
368
+ if (state.found) break;
325
369
  queue = nextQueue;
326
370
  if (queue.length === 0) break;
327
371
  }
328
372
 
329
- return { found, parent, alternateCount, foundDepth };
373
+ return {
374
+ found: state.found,
375
+ parent: state.parent,
376
+ alternateCount: state.alternateCount,
377
+ foundDepth: state.foundDepth,
378
+ };
330
379
  }
331
380
 
332
381
  /**
@@ -474,6 +523,53 @@ export function pathData(
474
523
 
475
524
  // ── File-level shortest path ────────────────────────────────────────────
476
525
 
526
+ type FileBfsState = {
527
+ visited: Set<string>;
528
+ parentMap: Map<string, string>;
529
+ found: boolean;
530
+ alternateCount: number;
531
+ };
532
+
533
+ /** Process a neighbor file during file-level BFS; updates state in place. */
534
+ function visitFileNeighbor(
535
+ neighborFile: string,
536
+ currentFile: string,
537
+ targetFile: string,
538
+ state: FileBfsState,
539
+ nextQueue: string[],
540
+ noTests: boolean,
541
+ ): void {
542
+ if (noTests && isTestFile(neighborFile)) return;
543
+ if (neighborFile === targetFile) {
544
+ if (!state.found) {
545
+ state.found = true;
546
+ state.parentMap.set(neighborFile, currentFile);
547
+ }
548
+ state.alternateCount++;
549
+ return;
550
+ }
551
+ if (state.visited.has(neighborFile)) return;
552
+ state.visited.add(neighborFile);
553
+ state.parentMap.set(neighborFile, currentFile);
554
+ nextQueue.push(neighborFile);
555
+ }
556
+
557
+ /** Reconstruct file path from target back to source using parent links. */
558
+ function reconstructFilePath(
559
+ parentMap: Map<string, string>,
560
+ sourceFile: string,
561
+ targetFile: string,
562
+ ): string[] {
563
+ const filePath: string[] = [targetFile];
564
+ let cur = targetFile;
565
+ while (cur !== sourceFile) {
566
+ cur = parentMap.get(cur)!;
567
+ filePath.push(cur);
568
+ }
569
+ filePath.reverse();
570
+ return filePath;
571
+ }
572
+
477
573
  /** BFS over file adjacency graph to find shortest path. */
478
574
  function bfsFilePath(
479
575
  neighborStmt: ReturnType<BetterSqlite3Database['prepare']>,
@@ -483,11 +579,13 @@ function bfsFilePath(
483
579
  maxDepth: number,
484
580
  noTests: boolean,
485
581
  ): { found: boolean; path: string[]; alternateCount: number } {
486
- const visited = new Set([sourceFile]);
487
- const parentMap = new Map<string, string>();
582
+ const state: FileBfsState = {
583
+ visited: new Set([sourceFile]),
584
+ parentMap: new Map<string, string>(),
585
+ found: false,
586
+ alternateCount: 0,
587
+ };
488
588
  let queue = [sourceFile];
489
- let found = false;
490
- let alternateCount = 0;
491
589
 
492
590
  for (let depth = 1; depth <= maxDepth; depth++) {
493
591
  const nextQueue: string[] = [];
@@ -496,38 +594,21 @@ function bfsFilePath(
496
594
  neighbor_file: string;
497
595
  }>;
498
596
  for (const n of neighbors) {
499
- if (noTests && isTestFile(n.neighbor_file)) continue;
500
- if (n.neighbor_file === targetFile) {
501
- if (!found) {
502
- found = true;
503
- parentMap.set(n.neighbor_file, currentFile);
504
- }
505
- alternateCount++;
506
- continue;
507
- }
508
- if (!visited.has(n.neighbor_file)) {
509
- visited.add(n.neighbor_file);
510
- parentMap.set(n.neighbor_file, currentFile);
511
- nextQueue.push(n.neighbor_file);
512
- }
597
+ visitFileNeighbor(n.neighbor_file, currentFile, targetFile, state, nextQueue, noTests);
513
598
  }
514
599
  }
515
- if (found) break;
600
+ if (state.found) break;
516
601
  queue = nextQueue;
517
602
  if (queue.length === 0) break;
518
603
  }
519
604
 
520
- if (!found) return { found: false, path: [], alternateCount: 0 };
605
+ if (!state.found) return { found: false, path: [], alternateCount: 0 };
521
606
 
522
- // Reconstruct path
523
- const filePath: string[] = [targetFile];
524
- let cur = targetFile;
525
- while (cur !== sourceFile) {
526
- cur = parentMap.get(cur)!;
527
- filePath.push(cur);
528
- }
529
- filePath.reverse();
530
- return { found: true, path: filePath, alternateCount: Math.max(0, alternateCount - 1) };
607
+ return {
608
+ found: true,
609
+ path: reconstructFilePath(state.parentMap, sourceFile, targetFile),
610
+ alternateCount: Math.max(0, state.alternateCount - 1),
611
+ };
531
612
  }
532
613
 
533
614
  /**