@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
@@ -753,6 +753,146 @@ function allNativeDataComplete(
753
753
 
754
754
  // ─── Public API ──────────────────────────────────────────────────────────
755
755
 
756
+ /** Distribute the per-file walk time equally among the visitors that ran. */
757
+ function accumulateWalkTime(
758
+ timing: AnalysisTiming,
759
+ walkMs: number,
760
+ astVisitor: Visitor | null,
761
+ complexityVisitor: Visitor | null,
762
+ cfgVisitor: Visitor | null,
763
+ dataflowVisitor: Visitor | null,
764
+ ): void {
765
+ const activeCount = [astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor].filter(
766
+ Boolean,
767
+ ).length;
768
+ if (activeCount === 0) return;
769
+
770
+ const share = walkMs / activeCount;
771
+ if (astVisitor) timing.astMs += share;
772
+ if (complexityVisitor) timing.complexityMs += share;
773
+ if (cfgVisitor) timing.cfgMs += share;
774
+ if (dataflowVisitor) timing.dataflowMs += share;
775
+ }
776
+
777
+ /** Apply visitor walk results to the per-file symbols/definitions. */
778
+ function applyVisitorResults(
779
+ results: WalkResults,
780
+ symbols: ExtractorOutput,
781
+ langId: string,
782
+ astVisitor: Visitor | null,
783
+ complexityVisitor: Visitor | null,
784
+ cfgVisitor: Visitor | null,
785
+ dataflowVisitor: Visitor | null,
786
+ ): void {
787
+ const defs = symbols.definitions || [];
788
+
789
+ if (astVisitor) {
790
+ const astRows = (results['ast-store'] || []) as ASTNodeRow[];
791
+ if (astRows.length > 0) symbols.astNodes = astRows;
792
+ }
793
+
794
+ if (complexityVisitor) storeComplexityResults(results, defs, langId);
795
+ if (cfgVisitor) storeCfgResults(results, defs);
796
+ if (dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
797
+ }
798
+
799
+ /** Process a single file: set up visitors, walk the tree, and apply results. */
800
+ function processFileWalk(
801
+ db: BetterSqlite3Database,
802
+ relPath: string,
803
+ symbols: ExtractorOutput,
804
+ langId: string,
805
+ opts: AnalysisOpts,
806
+ timing: AnalysisTiming,
807
+ ): void {
808
+ if (!symbols._tree) return;
809
+
810
+ const { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor } =
811
+ setupVisitors(db, relPath, symbols, langId, opts);
812
+
813
+ if (visitors.length === 0) return;
814
+
815
+ const walkStart = performance.now();
816
+ const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
817
+ const walkMs = performance.now() - walkStart;
818
+
819
+ accumulateWalkTime(timing, walkMs, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor);
820
+ applyVisitorResults(
821
+ results,
822
+ symbols,
823
+ langId,
824
+ astVisitor,
825
+ complexityVisitor,
826
+ cfgVisitor,
827
+ dataflowVisitor,
828
+ );
829
+ }
830
+
831
+ /**
832
+ * Unified pre-walk: run all applicable visitors in a single DFS per file.
833
+ * Returns the total wall-clock time for diagnostics.
834
+ */
835
+ function runUnifiedWalkPass(
836
+ db: BetterSqlite3Database,
837
+ fileSymbols: Map<string, ExtractorOutput>,
838
+ extToLang: Map<string, string>,
839
+ opts: AnalysisOpts,
840
+ timing: AnalysisTiming,
841
+ ): number {
842
+ const t0walk = performance.now();
843
+
844
+ for (const [relPath, symbols] of fileSymbols) {
845
+ if (!symbols._tree) continue;
846
+
847
+ const ext = path.extname(relPath).toLowerCase();
848
+ const langId = symbols._langId || extToLang.get(ext);
849
+ if (!langId) continue;
850
+
851
+ processFileWalk(db, relPath, symbols, langId, opts, timing);
852
+ }
853
+
854
+ return performance.now() - t0walk;
855
+ }
856
+
857
+ /** Try native Rust standalone analysis to fill gaps before WASM fallback. */
858
+ function tryNativeStandaloneAnalysis(
859
+ fileSymbols: Map<string, ExtractorOutput>,
860
+ rootDir: string,
861
+ opts: AnalysisOpts,
862
+ extToLang: Map<string, string>,
863
+ ): void {
864
+ const native = loadNative();
865
+ if (!native?.analyzeComplexity && !native?.buildCfgAnalysis && !native?.extractDataflowAnalysis) {
866
+ return;
867
+ }
868
+ const t0native = performance.now();
869
+ runNativeAnalysis(native, fileSymbols, rootDir, opts, extToLang);
870
+ debug(`native standalone analysis: ${(performance.now() - t0native).toFixed(1)}ms`);
871
+ }
872
+
873
+ /**
874
+ * Fast path: when all files were parsed by the native engine with full analysis,
875
+ * skip WASM re-parse and JS visitor walks entirely and go straight to DB persistence.
876
+ * Returns true if the fast path handled the work.
877
+ */
878
+ async function runFastPathIfApplicable(
879
+ db: BetterSqlite3Database,
880
+ fileSymbols: Map<string, ExtractorOutput>,
881
+ rootDir: string,
882
+ opts: AnalysisOpts,
883
+ engineOpts: EngineOpts | undefined,
884
+ timing: AnalysisTiming,
885
+ ): Promise<boolean> {
886
+ if (!allNativeDataComplete(fileSymbols, opts)) return false;
887
+
888
+ debug('native full-analysis fast path: all data present, skipping WASM/visitor passes');
889
+ const doComplexity = opts.complexity !== false;
890
+ const doCfg = opts.cfg !== false;
891
+ if (doComplexity && doCfg) reconcileCfgCyclomatic(fileSymbols);
892
+ await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
893
+ return true;
894
+ }
895
+
756
896
  export async function runAnalyses(
757
897
  db: BetterSqlite3Database,
758
898
  fileSymbols: Map<string, ExtractorOutput>,
@@ -771,80 +911,24 @@ export async function runAnalyses(
771
911
 
772
912
  const extToLang = buildExtToLangMap();
773
913
 
774
- // Fast path: when all files were parsed by the native engine with full analysis
775
- // (parseFilesFull), all data is already present — skip WASM re-parse and JS
776
- // visitor walks entirely, go straight to DB persistence.
777
- if (allNativeDataComplete(fileSymbols, opts)) {
778
- debug('native full-analysis fast path: all data present, skipping WASM/visitor passes');
779
- if (doComplexity && doCfg) reconcileCfgCyclomatic(fileSymbols);
780
- await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
914
+ if (await runFastPathIfApplicable(db, fileSymbols, rootDir, opts, engineOpts, timing)) {
781
915
  return timing;
782
916
  }
783
917
 
784
918
  // Native analysis pass: try Rust standalone functions before WASM fallback.
785
919
  // This fills in complexity/CFG/dataflow for files that the native parse pipeline
786
920
  // missed, avoiding the need to parse with WASM + run JS visitors.
787
- const native = loadNative();
788
- if (native?.analyzeComplexity || native?.buildCfgAnalysis || native?.extractDataflowAnalysis) {
789
- const t0native = performance.now();
790
- runNativeAnalysis(native, fileSymbols, rootDir, opts, extToLang);
791
- debug(`native standalone analysis: ${(performance.now() - t0native).toFixed(1)}ms`);
792
- }
921
+ tryNativeStandaloneAnalysis(fileSymbols, rootDir, opts, extToLang);
793
922
 
794
923
  // WASM pre-parse for files that still need it (AST store, or native gaps)
795
924
  await ensureWasmTreesIfNeeded(fileSymbols, opts, rootDir);
796
925
 
797
- // Unified pre-walk: run all applicable visitors in a single DFS per file.
798
926
  // Time each file's walk and distribute equally among active visitors
799
927
  // so that phase timers (astMs, complexityMs, etc.) reflect real work — not
800
928
  // just the DB-write tail in delegateToBuildFunctions.
801
- const t0walk = performance.now();
802
-
803
- for (const [relPath, symbols] of fileSymbols) {
804
- if (!symbols._tree) continue;
805
-
806
- const ext = path.extname(relPath).toLowerCase();
807
- const langId = symbols._langId || extToLang.get(ext);
808
- if (!langId) continue;
809
-
810
- const { visitors, walkerOpts, astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor } =
811
- setupVisitors(db, relPath, symbols, langId, opts);
812
-
813
- if (visitors.length === 0) continue;
814
-
815
- const walkStart = performance.now();
816
- const results = walkWithVisitors(symbols._tree.rootNode, visitors, langId, walkerOpts);
817
- const walkMs = performance.now() - walkStart;
818
-
819
- // Distribute walk time equally among active visitors
820
- const activeCount = [astVisitor, complexityVisitor, cfgVisitor, dataflowVisitor].filter(
821
- Boolean,
822
- ).length;
823
- if (activeCount > 0) {
824
- const share = walkMs / activeCount;
825
- if (astVisitor) timing.astMs += share;
826
- if (complexityVisitor) timing.complexityMs += share;
827
- if (cfgVisitor) timing.cfgMs += share;
828
- if (dataflowVisitor) timing.dataflowMs += share;
829
- }
830
-
831
- const defs = symbols.definitions || [];
832
-
833
- if (astVisitor) {
834
- const astRows = (results['ast-store'] || []) as ASTNodeRow[];
835
- if (astRows.length > 0) symbols.astNodes = astRows;
836
- }
837
-
838
- if (complexityVisitor) storeComplexityResults(results, defs, langId);
839
- if (cfgVisitor) storeCfgResults(results, defs);
840
- if (dataflowVisitor) symbols.dataflow = results.dataflow as DataflowResult;
841
- }
842
-
843
- // Total wall-clock time for the unified walk loop, including per-file
844
- // setupVisitors overhead. Walk time is already distributed into per-phase
845
- // timers above, so this field overlaps with (astMs + complexityMs + ...).
846
- // It is kept as a diagnostic cross-check, not an additive bucket.
847
- timing._unifiedWalkMs = performance.now() - t0walk;
929
+ // _unifiedWalkMs is kept as a diagnostic cross-check (overlaps with the
930
+ // per-phase timers above, not additive).
931
+ timing._unifiedWalkMs = runUnifiedWalkPass(db, fileSymbols, extToLang, opts, timing);
848
932
 
849
933
  // Reconcile: apply CFG-derived cyclomatic override for any definitions that have
850
934
  // both precomputed complexity and CFG data but whose cyclomatic was never overridden.
@@ -88,78 +88,118 @@ export function extractParams(
88
88
  return result;
89
89
  }
90
90
 
91
- /** Extract names from a rest parameter (e.g. `...args`). */
92
- function extractRestParamNames(node: TreeSitterNode, rules: LanguageRules): string[] {
93
- const nameNode = node.childForFieldName('name');
94
- if (nameNode) return [nameNode.text];
95
- for (const child of node.namedChildren) {
96
- if (child.type === rules.paramIdentifier) return [child.text];
97
- }
98
- return [];
99
- }
100
-
101
- /** Extract names from an object destructuring pattern (e.g. `{ a, b: c }`). */
102
- function extractObjectDestructNames(node: TreeSitterNode, rules: LanguageRules): string[] {
103
- const names: string[] = [];
104
- for (const child of node.namedChildren) {
105
- if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
106
- names.push(child.text);
107
- } else if (rules.pairPatternType && child.type === rules.pairPatternType) {
108
- const value = child.childForFieldName('value');
109
- if (value) names.push(...extractParamNames(value, rules));
110
- } else if (rules.restParamType && child.type === rules.restParamType) {
111
- names.push(...extractParamNames(child, rules));
112
- }
113
- }
114
- return names;
115
- }
116
-
117
- /** Extract names from an array destructuring pattern (e.g. `[a, b]`). */
118
- function extractArrayDestructNames(node: TreeSitterNode, rules: LanguageRules): string[] {
119
- const names: string[] = [];
120
- for (const child of node.namedChildren) {
121
- names.push(...extractParamNames(child, rules));
122
- }
123
- return names;
124
- }
125
-
126
91
  /**
127
- * Extract parameter names from a single parameter node.
92
+ * Resolve a single parameter node to either a direct list of names (base case)
93
+ * or a list of child nodes that still need processing. Returns `null` if the
94
+ * node yields nothing.
95
+ *
96
+ * This base case keeps destructuring helpers from recursing back into
97
+ * `extractParamNames`, breaking the 3-node mutual recursion cycle between
98
+ * `extractParamNames`, `extractObjectDestructNames`, and `extractArrayDestructNames`.
128
99
  */
129
- export function extractParamNames(node: TreeSitterNode | null, rules: LanguageRules): string[] {
130
- if (!node) return [];
100
+ function resolveParamNode(
101
+ node: TreeSitterNode,
102
+ rules: LanguageRules,
103
+ ): { names?: string[]; next?: TreeSitterNode[] } | null {
131
104
  const t = node.type;
132
105
 
133
106
  if (rules.extractParamName) {
134
107
  const result = rules.extractParamName(node);
135
- if (result) return result;
108
+ if (result) return { names: result };
136
109
  }
137
110
 
138
- if (t === rules.paramIdentifier) return [node.text];
111
+ if (t === rules.paramIdentifier) return { names: [node.text] };
139
112
 
140
113
  if (rules.paramWrapperTypes.has(t)) {
141
114
  const pattern = node.childForFieldName('pattern') || node.childForFieldName('name');
142
- return pattern ? extractParamNames(pattern, rules) : [];
115
+ return pattern ? { next: [pattern] } : null;
143
116
  }
144
117
 
145
118
  if (rules.defaultParamType && t === rules.defaultParamType) {
146
119
  const left = node.childForFieldName('left') || node.childForFieldName('name');
147
- return left ? extractParamNames(left, rules) : [];
120
+ return left ? { next: [left] } : null;
148
121
  }
149
122
 
150
123
  if (rules.restParamType && t === rules.restParamType) {
151
- return extractRestParamNames(node, rules);
124
+ const nameNode = node.childForFieldName('name');
125
+ if (nameNode) return { names: [nameNode.text] };
126
+ for (const child of node.namedChildren) {
127
+ if (child.type === rules.paramIdentifier) return { names: [child.text] };
128
+ }
129
+ return null;
152
130
  }
153
131
 
154
132
  if (rules.objectDestructType && t === rules.objectDestructType) {
155
- return extractObjectDestructNames(node, rules);
133
+ return { next: collectObjectDestructChildren(node, rules) };
156
134
  }
157
135
 
158
136
  if (rules.arrayDestructType && t === rules.arrayDestructType) {
159
- return extractArrayDestructNames(node, rules);
137
+ return { next: [...node.namedChildren] };
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ /**
144
+ * Collect child nodes from an object destructuring pattern that should be
145
+ * processed for further name extraction. Returns nodes (not names) so the
146
+ * caller drives traversal via a worklist instead of recursion.
147
+ */
148
+ function collectObjectDestructChildren(
149
+ node: TreeSitterNode,
150
+ rules: LanguageRules,
151
+ ): TreeSitterNode[] {
152
+ const next: TreeSitterNode[] = [];
153
+ for (const child of node.namedChildren) {
154
+ if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
155
+ // Shorthand prop is a direct identifier — handled by the shorthand
156
+ // guard in the `extractParamNames` worklist loop (before `resolveParamNode`).
157
+ next.push(child);
158
+ } else if (rules.pairPatternType && child.type === rules.pairPatternType) {
159
+ const value = child.childForFieldName('value');
160
+ if (value) next.push(value);
161
+ } else if (rules.restParamType && child.type === rules.restParamType) {
162
+ next.push(child);
163
+ }
160
164
  }
165
+ return next;
166
+ }
161
167
 
162
- return [];
168
+ /**
169
+ * Extract parameter names from a single parameter node.
170
+ *
171
+ * Uses an iterative worklist to handle nested destructuring (objects, arrays,
172
+ * defaults, rest, wrappers) without mutual recursion through helper functions.
173
+ */
174
+ export function extractParamNames(node: TreeSitterNode | null, rules: LanguageRules): string[] {
175
+ if (!node) return [];
176
+
177
+ const names: string[] = [];
178
+ const stack: TreeSitterNode[] = [node];
179
+
180
+ while (stack.length > 0) {
181
+ const current = stack.pop();
182
+ if (!current) continue;
183
+
184
+ // Shorthand identifier inside an object destructuring is just the node's text.
185
+ if (rules.shorthandPropPattern && current.type === rules.shorthandPropPattern) {
186
+ names.push(current.text);
187
+ continue;
188
+ }
189
+
190
+ const resolved = resolveParamNode(current, rules);
191
+ if (!resolved) continue;
192
+ if (resolved.names) names.push(...resolved.names);
193
+ if (resolved.next) {
194
+ // Push in reverse so traversal order matches the previous recursive order.
195
+ for (let i = resolved.next.length - 1; i >= 0; i--) {
196
+ const child = resolved.next[i];
197
+ if (child) stack.push(child);
198
+ }
199
+ }
200
+ }
201
+
202
+ return names;
163
203
  }
164
204
 
165
205
  /**
@@ -181,46 +181,17 @@ function newTypesFor(astTypeMap: Record<string, string>): Set<string> {
181
181
  return s;
182
182
  }
183
183
 
184
- export function createAstStoreVisitor(
185
- astTypeMap: Record<string, string>,
186
- defs: Definition[],
187
- relPath: string,
188
- nodeIdMap: Map<string, number>,
189
- stringConfig: AstStringConfig = DEFAULT_STRING_CONFIG,
190
- stopRecurseKinds: ReadonlySet<string> = new Set(),
191
- ): Visitor {
192
- const rows: AstStoreRow[] = [];
193
- const matched = new Set<number>();
194
- const newTypes = newTypesFor(astTypeMap);
195
- // When nodeIdMap is empty, parentNodeId resolution is wasted work — the
196
- // worker passes an empty map and the main thread re-resolves against its
197
- // own DB-populated map in features/ast.ts::collectFileAstRows. Skip the
198
- // findParentDef linear scan in that case.
199
- const skipParentLookup = nodeIdMap.size === 0;
200
-
201
- function findParentDef(line: number): Definition | null {
202
- let best: Definition | null = null;
203
- for (const def of defs) {
204
- if (def.line <= line && (def.endLine == null || def.endLine >= line)) {
205
- if (!best || (def.endLine ?? 0) - def.line < (best.endLine ?? 0) - best.line) {
206
- best = def;
207
- }
208
- }
209
- }
210
- return best;
211
- }
212
-
213
- function resolveParentNodeId(line: number): number | null {
214
- if (skipParentLookup) return null;
215
- const parentDef = findParentDef(line);
216
- if (!parentDef) return null;
217
- return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
218
- }
184
+ type NameTextResult = { name: string | null | undefined; text: string | null; skip?: boolean };
185
+ type KindHandler = (node: TreeSitterNode) => NameTextResult;
219
186
 
220
- type NameTextResult = { name: string | null | undefined; text: string | null; skip?: boolean };
221
- type KindHandler = (node: TreeSitterNode) => NameTextResult;
187
+ const DEFAULT_NAME_TEXT_RESULT: NameTextResult = { name: undefined, text: null };
222
188
 
223
- const kindHandlers: Record<string, KindHandler> = {
189
+ /** Build the per-kind resolver map for name/text extraction. */
190
+ function buildKindHandlers(
191
+ newTypes: Set<string>,
192
+ stringConfig: AstStringConfig,
193
+ ): Record<string, KindHandler> {
194
+ return {
224
195
  new: (node) => ({ name: extractConstructorName(node), text: truncate(node.text) }),
225
196
  throw: (node) => ({
226
197
  name: extractThrowName(node, newTypes),
@@ -234,31 +205,102 @@ export function createAstStoreVisitor(
234
205
  },
235
206
  regex: (node) => ({ name: node.text || '?', text: truncate(node.text) }),
236
207
  };
237
- const defaultResult: NameTextResult = { name: undefined, text: null };
208
+ }
238
209
 
239
- function resolveNameAndText(node: TreeSitterNode, kind: string): NameTextResult {
240
- const handler = kindHandlers[kind];
241
- return handler ? handler(node) : defaultResult;
210
+ /** Find the innermost definition whose line range contains `line`. */
211
+ function findParentDef(line: number, defs: Definition[]): Definition | null {
212
+ let best: Definition | null = null;
213
+ for (const def of defs) {
214
+ if (def.line <= line && (def.endLine == null || def.endLine >= line)) {
215
+ if (!best || (def.endLine ?? 0) - def.line < (best.endLine ?? 0) - best.line) {
216
+ best = def;
217
+ }
218
+ }
242
219
  }
220
+ return best;
221
+ }
222
+
223
+ /** Resolve the parent definition's node id for a given source line. */
224
+ function resolveParentNodeId(
225
+ line: number,
226
+ defs: Definition[],
227
+ nodeIdMap: Map<string, number>,
228
+ skipParentLookup: boolean,
229
+ ): number | null {
230
+ if (skipParentLookup) return null;
231
+ const parentDef = findParentDef(line, defs);
232
+ if (!parentDef) return null;
233
+ return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
234
+ }
243
235
 
244
- function collectNode(node: TreeSitterNode, kind: string): void {
245
- if (matched.has(node.id)) return;
236
+ interface CollectCtx {
237
+ rows: AstStoreRow[];
238
+ matched: Set<number>;
239
+ relPath: string;
240
+ defs: Definition[];
241
+ nodeIdMap: Map<string, number>;
242
+ skipParentLookup: boolean;
243
+ kindHandlers: Record<string, KindHandler>;
244
+ }
246
245
 
247
- const resolved = resolveNameAndText(node, kind);
248
- if (resolved.skip) return;
246
+ function collectNode(ctx: CollectCtx, node: TreeSitterNode, kind: string): void {
247
+ if (ctx.matched.has(node.id)) return;
248
+
249
+ const handler = ctx.kindHandlers[kind];
250
+ const resolved = handler ? handler(node) : DEFAULT_NAME_TEXT_RESULT;
251
+ if (resolved.skip) return;
252
+
253
+ const line = node.startPosition.row + 1;
254
+ ctx.rows.push({
255
+ file: ctx.relPath,
256
+ line,
257
+ kind,
258
+ name: resolved.name,
259
+ text: resolved.text,
260
+ receiver: null,
261
+ parentNodeId: resolveParentNodeId(line, ctx.defs, ctx.nodeIdMap, ctx.skipParentLookup),
262
+ });
263
+
264
+ ctx.matched.add(node.id);
265
+ }
249
266
 
250
- rows.push({
251
- file: relPath,
252
- line: node.startPosition.row + 1,
253
- kind,
254
- name: resolved.name,
255
- text: resolved.text,
256
- receiver: null,
257
- parentNodeId: resolveParentNodeId(node.startPosition.row + 1),
258
- });
267
+ /**
268
+ * Resolve the kind for a tree-sitter node, or `null` if the node should be ignored.
269
+ *
270
+ * Gate with `hasOwn` because plain-object lookup walks Object.prototype:
271
+ * tree-sitter node types like `constructor` (Haskell sum-types: Left,
272
+ * Right) would otherwise resolve to `Object.prototype.constructor` (the
273
+ * Object() function), which then crashes the worker boundary with
274
+ * "function Object() { [native code] } could not be cloned" when the
275
+ * resulting astNodes row is structured-cloned back to the main thread.
276
+ */
277
+ function resolveAstKind(node: TreeSitterNode, astTypeMap: Record<string, string>): string | null {
278
+ if (!Object.hasOwn(astTypeMap, node.type)) return null;
279
+ return astTypeMap[node.type] || null;
280
+ }
259
281
 
260
- matched.add(node.id);
261
- }
282
+ export function createAstStoreVisitor(
283
+ astTypeMap: Record<string, string>,
284
+ defs: Definition[],
285
+ relPath: string,
286
+ nodeIdMap: Map<string, number>,
287
+ stringConfig: AstStringConfig = DEFAULT_STRING_CONFIG,
288
+ stopRecurseKinds: ReadonlySet<string> = new Set(),
289
+ ): Visitor {
290
+ const newTypes = newTypesFor(astTypeMap);
291
+ // When nodeIdMap is empty, parentNodeId resolution is wasted work — the
292
+ // worker passes an empty map and the main thread re-resolves against its
293
+ // own DB-populated map in features/ast.ts::collectFileAstRows. Skip the
294
+ // findParentDef linear scan in that case.
295
+ const ctx: CollectCtx = {
296
+ rows: [],
297
+ matched: new Set<number>(),
298
+ relPath,
299
+ defs,
300
+ nodeIdMap,
301
+ skipParentLookup: nodeIdMap.size === 0,
302
+ kindHandlers: buildKindHandlers(newTypes, stringConfig),
303
+ };
262
304
 
263
305
  return {
264
306
  name: 'ast-store',
@@ -267,19 +309,12 @@ export function createAstStoreVisitor(
267
309
  // Guard: skip re-collection but do NOT skipChildren — node.id (memory address)
268
310
  // can be reused by tree-sitter, so a collision would incorrectly suppress an
269
311
  // unrelated subtree. The parent call's skipChildren handles the intended case.
270
- if (matched.has(node.id)) return;
271
-
272
- // Gate with `hasOwn` because plain-object lookup walks Object.prototype:
273
- // tree-sitter node types like `constructor` (Haskell sum-types: Left,
274
- // Right) would otherwise resolve to `Object.prototype.constructor` (the
275
- // Object() function), which then crashes the worker boundary with
276
- // "function Object() { [native code] } could not be cloned" when the
277
- // resulting astNodes row is structured-cloned back to the main thread.
278
- if (!Object.hasOwn(astTypeMap, node.type)) return;
279
- const kind = astTypeMap[node.type];
312
+ if (ctx.matched.has(node.id)) return;
313
+
314
+ const kind = resolveAstKind(node, astTypeMap);
280
315
  if (!kind) return;
281
316
 
282
- collectNode(node, kind);
317
+ collectNode(ctx, node, kind);
283
318
 
284
319
  // Mirror the native walker's recursion policy. In JS/TS, the native
285
320
  // javascript.rs walker returns after collecting `new` or `throw` to
@@ -293,7 +328,7 @@ export function createAstStoreVisitor(
293
328
  },
294
329
 
295
330
  finish(): AstStoreRow[] {
296
- return rows;
331
+ return ctx.rows;
297
332
  },
298
333
  };
299
334
  }