@optave/codegraph 3.13.0 → 3.15.0
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.
- package/README.md +35 -34
- package/dist/ast-analysis/engine.d.ts.map +1 -1
- package/dist/ast-analysis/engine.js +38 -40
- package/dist/ast-analysis/engine.js.map +1 -1
- package/dist/ast-analysis/rules/b2.d.ts +7 -0
- package/dist/ast-analysis/rules/b2.d.ts.map +1 -0
- package/dist/ast-analysis/rules/b2.js +240 -0
- package/dist/ast-analysis/rules/b2.js.map +1 -0
- package/dist/ast-analysis/rules/b3.d.ts +6 -0
- package/dist/ast-analysis/rules/b3.d.ts.map +1 -0
- package/dist/ast-analysis/rules/b3.js +105 -0
- package/dist/ast-analysis/rules/b3.js.map +1 -0
- package/dist/ast-analysis/rules/b4.d.ts +9 -0
- package/dist/ast-analysis/rules/b4.d.ts.map +1 -0
- package/dist/ast-analysis/rules/b4.js +361 -0
- package/dist/ast-analysis/rules/b4.js.map +1 -0
- package/dist/ast-analysis/rules/b5.d.ts +4 -0
- package/dist/ast-analysis/rules/b5.d.ts.map +1 -0
- package/dist/ast-analysis/rules/b5.js +52 -0
- package/dist/ast-analysis/rules/b5.js.map +1 -0
- package/dist/ast-analysis/rules/c.d.ts +4 -0
- package/dist/ast-analysis/rules/c.d.ts.map +1 -0
- package/dist/ast-analysis/rules/c.js +143 -0
- package/dist/ast-analysis/rules/c.js.map +1 -0
- package/dist/ast-analysis/rules/index.d.ts.map +1 -1
- package/dist/ast-analysis/rules/index.js +34 -0
- package/dist/ast-analysis/rules/index.js.map +1 -1
- package/dist/ast-analysis/rules/javascript.d.ts.map +1 -1
- package/dist/ast-analysis/rules/javascript.js +3 -0
- package/dist/ast-analysis/rules/javascript.js.map +1 -1
- package/dist/ast-analysis/shared.d.ts.map +1 -1
- package/dist/ast-analysis/shared.js +2 -0
- package/dist/ast-analysis/shared.js.map +1 -1
- package/dist/ast-analysis/visitor-utils.d.ts +1 -0
- package/dist/ast-analysis/visitor-utils.d.ts.map +1 -1
- package/dist/ast-analysis/visitor-utils.js +5 -0
- package/dist/ast-analysis/visitor-utils.js.map +1 -1
- package/dist/ast-analysis/visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitor.js +60 -47
- package/dist/ast-analysis/visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/cfg-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/cfg-visitor.js +126 -76
- package/dist/ast-analysis/visitors/cfg-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/complexity-visitor.js +27 -15
- package/dist/ast-analysis/visitors/complexity-visitor.js.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.d.ts.map +1 -1
- package/dist/ast-analysis/visitors/dataflow-visitor.js +54 -21
- package/dist/ast-analysis/visitors/dataflow-visitor.js.map +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +137 -134
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/roles.d.ts.map +1 -1
- package/dist/cli/commands/roles.js +6 -1
- package/dist/cli/commands/roles.js.map +1 -1
- package/dist/db/better-sqlite3.d.ts +2 -1
- package/dist/db/better-sqlite3.d.ts.map +1 -1
- package/dist/db/better-sqlite3.js.map +1 -1
- package/dist/db/connection.d.ts +7 -1
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +20 -5
- package/dist/db/connection.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +68 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/repository/build-stmts.d.ts.map +1 -1
- package/dist/db/repository/build-stmts.js +18 -0
- package/dist/db/repository/build-stmts.js.map +1 -1
- package/dist/db/repository/dataflow.d.ts +5 -0
- package/dist/db/repository/dataflow.d.ts.map +1 -1
- package/dist/db/repository/dataflow.js +14 -0
- package/dist/db/repository/dataflow.js.map +1 -1
- package/dist/db/repository/index.d.ts +1 -1
- package/dist/db/repository/index.d.ts.map +1 -1
- package/dist/db/repository/index.js +1 -1
- package/dist/db/repository/index.js.map +1 -1
- package/dist/db/repository/native-repository.d.ts.map +1 -1
- package/dist/db/repository/native-repository.js +47 -34
- package/dist/db/repository/native-repository.js.map +1 -1
- package/dist/domain/analysis/context.d.ts +2 -2
- package/dist/domain/analysis/dependencies.d.ts +2 -2
- package/dist/domain/analysis/diff-impact.d.ts +2 -2
- package/dist/domain/analysis/fn-impact.d.ts +3 -1
- package/dist/domain/analysis/fn-impact.d.ts.map +1 -1
- package/dist/domain/analysis/fn-impact.js +4 -0
- package/dist/domain/analysis/fn-impact.js.map +1 -1
- package/dist/domain/analysis/implementations.d.ts +2 -2
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +32 -5
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/analysis/roles.d.ts +7 -1
- package/dist/domain/analysis/roles.d.ts.map +1 -1
- package/dist/domain/analysis/roles.js +16 -0
- package/dist/domain/analysis/roles.js.map +1 -1
- package/dist/domain/analysis/symbol-lookup.d.ts +4 -4
- package/dist/domain/graph/builder/call-resolver.d.ts +17 -5
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
- package/dist/domain/graph/builder/call-resolver.js +85 -220
- package/dist/domain/graph/builder/call-resolver.js.map +1 -1
- package/dist/domain/graph/builder/context.d.ts +1 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +16 -1
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +162 -72
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +166 -97
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
- package/dist/domain/graph/builder/pipeline.js +10 -4
- package/dist/domain/graph/builder/pipeline.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +496 -250
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/collect-files.js +10 -7
- package/dist/domain/graph/builder/stages/collect-files.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.js +895 -545
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
- package/dist/domain/graph/resolver/points-to.d.ts.map +1 -1
- package/dist/domain/graph/resolver/points-to.js +105 -57
- package/dist/domain/graph/resolver/points-to.js.map +1 -1
- package/dist/domain/graph/resolver/strategy.d.ts +61 -0
- package/dist/domain/graph/resolver/strategy.d.ts.map +1 -0
- package/dist/domain/graph/resolver/strategy.js +222 -0
- package/dist/domain/graph/resolver/strategy.js.map +1 -0
- package/dist/domain/graph/watcher.d.ts.map +1 -1
- package/dist/domain/graph/watcher.js +16 -9
- package/dist/domain/graph/watcher.js.map +1 -1
- package/dist/domain/parser.d.ts +12 -0
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +12 -2
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/queries.d.ts +1 -1
- package/dist/domain/queries.d.ts.map +1 -1
- package/dist/domain/queries.js +1 -1
- package/dist/domain/queries.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +3 -0
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
- package/dist/domain/wasm-worker-pool.js +24 -5
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +7 -0
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
- package/dist/extractors/dart.js +48 -3
- package/dist/extractors/dart.js.map +1 -1
- package/dist/extractors/groovy.js +62 -3
- package/dist/extractors/groovy.js.map +1 -1
- package/dist/extractors/helpers.d.ts +4 -2
- package/dist/extractors/helpers.d.ts.map +1 -1
- package/dist/extractors/helpers.js +5 -1
- package/dist/extractors/helpers.js.map +1 -1
- package/dist/extractors/java.js +77 -1
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +549 -163
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/kotlin.js +58 -3
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/objc.js +25 -2
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/scala.js +62 -2
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/swift.js +52 -3
- package/dist/extractors/swift.js.map +1 -1
- package/dist/features/audit.js +26 -23
- package/dist/features/audit.js.map +1 -1
- package/dist/features/boundaries.d.ts.map +1 -1
- package/dist/features/boundaries.js +12 -9
- package/dist/features/boundaries.js.map +1 -1
- package/dist/features/cfg.d.ts.map +1 -1
- package/dist/features/cfg.js +25 -18
- package/dist/features/cfg.js.map +1 -1
- package/dist/features/check.d.ts.map +1 -1
- package/dist/features/check.js +18 -5
- package/dist/features/check.js.map +1 -1
- package/dist/features/communities.d.ts +4 -2
- package/dist/features/communities.d.ts.map +1 -1
- package/dist/features/communities.js +6 -4
- package/dist/features/communities.js.map +1 -1
- package/dist/features/dataflow.d.ts +60 -0
- package/dist/features/dataflow.d.ts.map +1 -1
- package/dist/features/dataflow.js +530 -6
- package/dist/features/dataflow.js.map +1 -1
- package/dist/features/manifesto.d.ts.map +1 -1
- package/dist/features/manifesto.js +59 -72
- package/dist/features/manifesto.js.map +1 -1
- package/dist/features/sequence.d.ts.map +1 -1
- package/dist/features/sequence.js +27 -22
- package/dist/features/sequence.js.map +1 -1
- package/dist/features/snapshot.d.ts.map +1 -1
- package/dist/features/snapshot.js +36 -28
- package/dist/features/snapshot.js.map +1 -1
- package/dist/features/structure.d.ts.map +1 -1
- package/dist/features/structure.js +150 -62
- package/dist/features/structure.js.map +1 -1
- package/dist/features/triage.d.ts.map +1 -1
- package/dist/features/triage.js +18 -11
- package/dist/features/triage.js.map +1 -1
- package/dist/graph/algorithms/bfs.d.ts +1 -1
- package/dist/graph/algorithms/bfs.d.ts.map +1 -1
- package/dist/graph/algorithms/bfs.js +14 -13
- package/dist/graph/algorithms/bfs.js.map +1 -1
- package/dist/graph/algorithms/tarjan.d.ts.map +1 -1
- package/dist/graph/algorithms/tarjan.js +5 -0
- package/dist/graph/algorithms/tarjan.js.map +1 -1
- package/dist/graph/builders/dependency.js +28 -22
- package/dist/graph/builders/dependency.js.map +1 -1
- package/dist/graph/classifiers/roles.d.ts +10 -1
- package/dist/graph/classifiers/roles.d.ts.map +1 -1
- package/dist/graph/classifiers/roles.js +60 -6
- package/dist/graph/classifiers/roles.js.map +1 -1
- package/dist/infrastructure/config.d.ts +10 -0
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +31 -3
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/registry.d.ts +0 -7
- package/dist/infrastructure/registry.d.ts.map +1 -1
- package/dist/infrastructure/registry.js +29 -13
- package/dist/infrastructure/registry.js.map +1 -1
- package/dist/infrastructure/update-check.d.ts.map +1 -1
- package/dist/infrastructure/update-check.js +49 -31
- package/dist/infrastructure/update-check.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -10
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/ast-query.d.ts +1 -1
- package/dist/mcp/tools/ast-query.d.ts.map +1 -1
- package/dist/mcp/tools/audit.d.ts +1 -1
- package/dist/mcp/tools/audit.d.ts.map +1 -1
- package/dist/mcp/tools/batch-query.d.ts +1 -1
- package/dist/mcp/tools/batch-query.d.ts.map +1 -1
- package/dist/mcp/tools/branch-compare.d.ts +1 -1
- package/dist/mcp/tools/branch-compare.d.ts.map +1 -1
- package/dist/mcp/tools/brief.d.ts +1 -1
- package/dist/mcp/tools/brief.d.ts.map +1 -1
- package/dist/mcp/tools/cfg.d.ts +1 -1
- package/dist/mcp/tools/cfg.d.ts.map +1 -1
- package/dist/mcp/tools/check.d.ts +1 -1
- package/dist/mcp/tools/check.d.ts.map +1 -1
- package/dist/mcp/tools/co-changes.d.ts +1 -1
- package/dist/mcp/tools/co-changes.d.ts.map +1 -1
- package/dist/mcp/tools/code-owners.d.ts +1 -1
- package/dist/mcp/tools/code-owners.d.ts.map +1 -1
- package/dist/mcp/tools/communities.d.ts +1 -1
- package/dist/mcp/tools/communities.d.ts.map +1 -1
- package/dist/mcp/tools/complexity.d.ts +1 -1
- package/dist/mcp/tools/complexity.d.ts.map +1 -1
- package/dist/mcp/tools/context.d.ts +1 -1
- package/dist/mcp/tools/context.d.ts.map +1 -1
- package/dist/mcp/tools/dataflow.d.ts +1 -1
- package/dist/mcp/tools/dataflow.d.ts.map +1 -1
- package/dist/mcp/tools/diff-impact.d.ts +1 -1
- package/dist/mcp/tools/diff-impact.d.ts.map +1 -1
- package/dist/mcp/tools/execution-flow.d.ts +1 -1
- package/dist/mcp/tools/execution-flow.d.ts.map +1 -1
- package/dist/mcp/tools/export-graph.d.ts +1 -1
- package/dist/mcp/tools/export-graph.d.ts.map +1 -1
- package/dist/mcp/tools/file-deps.d.ts +1 -1
- package/dist/mcp/tools/file-deps.d.ts.map +1 -1
- package/dist/mcp/tools/file-exports.d.ts +1 -1
- package/dist/mcp/tools/file-exports.d.ts.map +1 -1
- package/dist/mcp/tools/find-cycles.d.ts +1 -1
- package/dist/mcp/tools/find-cycles.d.ts.map +1 -1
- package/dist/mcp/tools/fn-impact.d.ts +1 -1
- package/dist/mcp/tools/fn-impact.d.ts.map +1 -1
- package/dist/mcp/tools/impact-analysis.d.ts +1 -1
- package/dist/mcp/tools/impact-analysis.d.ts.map +1 -1
- package/dist/mcp/tools/implementations.d.ts +1 -1
- package/dist/mcp/tools/implementations.d.ts.map +1 -1
- package/dist/mcp/tools/index.d.ts +2 -5
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/interfaces.d.ts +1 -1
- package/dist/mcp/tools/interfaces.d.ts.map +1 -1
- package/dist/mcp/tools/list-functions.d.ts +1 -1
- package/dist/mcp/tools/list-functions.d.ts.map +1 -1
- package/dist/mcp/tools/list-repos.d.ts +1 -1
- package/dist/mcp/tools/list-repos.d.ts.map +1 -1
- package/dist/mcp/tools/module-map.d.ts +1 -1
- package/dist/mcp/tools/module-map.d.ts.map +1 -1
- package/dist/mcp/tools/node-roles.d.ts +1 -1
- package/dist/mcp/tools/node-roles.d.ts.map +1 -1
- package/dist/mcp/tools/path.d.ts +1 -1
- package/dist/mcp/tools/path.d.ts.map +1 -1
- package/dist/mcp/tools/query.d.ts +1 -1
- package/dist/mcp/tools/query.d.ts.map +1 -1
- package/dist/mcp/tools/semantic-search.d.ts +1 -1
- package/dist/mcp/tools/semantic-search.d.ts.map +1 -1
- package/dist/mcp/tools/sequence.d.ts +1 -1
- package/dist/mcp/tools/sequence.d.ts.map +1 -1
- package/dist/mcp/tools/structure.d.ts +1 -1
- package/dist/mcp/tools/structure.d.ts.map +1 -1
- package/dist/mcp/tools/symbol-children.d.ts +1 -1
- package/dist/mcp/tools/symbol-children.d.ts.map +1 -1
- package/dist/mcp/tools/triage.d.ts +1 -1
- package/dist/mcp/tools/triage.d.ts.map +1 -1
- package/dist/mcp/tools/where.d.ts +1 -1
- package/dist/mcp/tools/where.d.ts.map +1 -1
- package/dist/mcp/types.d.ts +19 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +6 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/presentation/queries-cli/index.d.ts +1 -1
- package/dist/presentation/queries-cli/index.d.ts.map +1 -1
- package/dist/presentation/queries-cli/index.js +1 -1
- package/dist/presentation/queries-cli/index.js.map +1 -1
- package/dist/presentation/queries-cli/overview.d.ts +1 -0
- package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
- package/dist/presentation/queries-cli/overview.js +20 -1
- package/dist/presentation/queries-cli/overview.js.map +1 -1
- package/dist/presentation/queries-cli.d.ts +1 -1
- package/dist/presentation/queries-cli.d.ts.map +1 -1
- package/dist/presentation/queries-cli.js +1 -1
- package/dist/presentation/queries-cli.js.map +1 -1
- package/dist/presentation/viewer.d.ts.map +1 -1
- package/dist/presentation/viewer.js +45 -32
- package/dist/presentation/viewer.js.map +1 -1
- package/dist/shared/constants.d.ts +21 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +25 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/normalize.d.ts.map +1 -1
- package/dist/shared/normalize.js +12 -22
- package/dist/shared/normalize.js.map +1 -1
- package/dist/shared/paginate.d.ts +4 -17
- package/dist/shared/paginate.d.ts.map +1 -1
- package/dist/shared/paginate.js.map +1 -1
- package/dist/types.d.ts +76 -1
- package/dist/types.d.ts.map +1 -1
- package/grammars/tree-sitter-erlang.wasm +0 -0
- package/package.json +7 -7
- package/src/ast-analysis/engine.ts +43 -63
- package/src/ast-analysis/rules/b2.ts +263 -0
- package/src/ast-analysis/rules/b3.ts +127 -0
- package/src/ast-analysis/rules/b4.ts +378 -0
- package/src/ast-analysis/rules/b5.ts +65 -0
- package/src/ast-analysis/rules/c.ts +157 -0
- package/src/ast-analysis/rules/index.ts +34 -0
- package/src/ast-analysis/rules/javascript.ts +3 -0
- package/src/ast-analysis/shared.ts +2 -0
- package/src/ast-analysis/visitor-utils.ts +5 -0
- package/src/ast-analysis/visitor.ts +82 -52
- package/src/ast-analysis/visitors/cfg-visitor.ts +198 -84
- package/src/ast-analysis/visitors/complexity-visitor.ts +44 -16
- package/src/ast-analysis/visitors/dataflow-visitor.ts +68 -29
- package/src/cli/commands/config.ts +184 -184
- package/src/cli/commands/roles.ts +6 -1
- package/src/db/better-sqlite3.ts +5 -4
- package/src/db/connection.ts +23 -5
- package/src/db/index.ts +1 -0
- package/src/db/migrations.ts +68 -0
- package/src/db/repository/build-stmts.ts +30 -0
- package/src/db/repository/dataflow.ts +16 -0
- package/src/db/repository/index.ts +1 -1
- package/src/db/repository/native-repository.ts +56 -40
- package/src/domain/analysis/fn-impact.ts +4 -0
- package/src/domain/analysis/module-map.ts +38 -6
- package/src/domain/analysis/roles.ts +23 -0
- package/src/domain/graph/builder/call-resolver.ts +112 -232
- package/src/domain/graph/builder/context.ts +1 -0
- package/src/domain/graph/builder/helpers.ts +190 -72
- package/src/domain/graph/builder/incremental.ts +249 -120
- package/src/domain/graph/builder/pipeline.ts +11 -5
- package/src/domain/graph/builder/stages/build-edges.ts +696 -296
- package/src/domain/graph/builder/stages/collect-files.ts +12 -6
- package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
- package/src/domain/graph/builder/stages/native-orchestrator.ts +1102 -590
- package/src/domain/graph/resolver/points-to.ts +182 -59
- package/src/domain/graph/resolver/strategy.ts +265 -0
- package/src/domain/graph/watcher.ts +19 -9
- package/src/domain/parser.ts +12 -2
- package/src/domain/queries.ts +1 -1
- package/src/domain/wasm-worker-entry.ts +3 -0
- package/src/domain/wasm-worker-pool.ts +28 -4
- package/src/domain/wasm-worker-protocol.ts +4 -0
- package/src/extractors/dart.ts +48 -3
- package/src/extractors/groovy.ts +62 -2
- package/src/extractors/helpers.ts +5 -2
- package/src/extractors/java.ts +80 -1
- package/src/extractors/javascript.ts +566 -161
- package/src/extractors/kotlin.ts +57 -3
- package/src/extractors/objc.ts +25 -1
- package/src/extractors/scala.ts +63 -1
- package/src/extractors/swift.ts +46 -3
- package/src/features/audit.ts +43 -34
- package/src/features/boundaries.ts +17 -9
- package/src/features/cfg.ts +31 -22
- package/src/features/check.ts +21 -5
- package/src/features/communities.ts +28 -19
- package/src/features/dataflow.ts +755 -6
- package/src/features/manifesto.ts +76 -75
- package/src/features/sequence.ts +29 -23
- package/src/features/snapshot.ts +36 -25
- package/src/features/structure.ts +185 -55
- package/src/features/triage.ts +28 -15
- package/src/graph/algorithms/bfs.ts +13 -12
- package/src/graph/algorithms/tarjan.ts +5 -0
- package/src/graph/builders/dependency.ts +35 -23
- package/src/graph/classifiers/roles.ts +74 -7
- package/src/infrastructure/config.ts +32 -3
- package/src/infrastructure/registry.ts +44 -20
- package/src/infrastructure/update-check.ts +55 -33
- package/src/mcp/server.ts +2 -8
- package/src/mcp/tools/ast-query.ts +1 -1
- package/src/mcp/tools/audit.ts +1 -1
- package/src/mcp/tools/batch-query.ts +1 -1
- package/src/mcp/tools/branch-compare.ts +1 -1
- package/src/mcp/tools/brief.ts +1 -1
- package/src/mcp/tools/cfg.ts +1 -1
- package/src/mcp/tools/check.ts +1 -1
- package/src/mcp/tools/co-changes.ts +1 -1
- package/src/mcp/tools/code-owners.ts +1 -1
- package/src/mcp/tools/communities.ts +1 -1
- package/src/mcp/tools/complexity.ts +1 -1
- package/src/mcp/tools/context.ts +1 -1
- package/src/mcp/tools/dataflow.ts +1 -1
- package/src/mcp/tools/diff-impact.ts +1 -1
- package/src/mcp/tools/execution-flow.ts +1 -1
- package/src/mcp/tools/export-graph.ts +1 -1
- package/src/mcp/tools/file-deps.ts +1 -1
- package/src/mcp/tools/file-exports.ts +1 -1
- package/src/mcp/tools/find-cycles.ts +1 -1
- package/src/mcp/tools/fn-impact.ts +1 -1
- package/src/mcp/tools/impact-analysis.ts +1 -1
- package/src/mcp/tools/implementations.ts +1 -1
- package/src/mcp/tools/index.ts +2 -5
- package/src/mcp/tools/interfaces.ts +1 -1
- package/src/mcp/tools/list-functions.ts +1 -1
- package/src/mcp/tools/list-repos.ts +1 -1
- package/src/mcp/tools/module-map.ts +1 -1
- package/src/mcp/tools/node-roles.ts +1 -1
- package/src/mcp/tools/path.ts +1 -1
- package/src/mcp/tools/query.ts +1 -1
- package/src/mcp/tools/semantic-search.ts +1 -1
- package/src/mcp/tools/sequence.ts +1 -1
- package/src/mcp/tools/structure.ts +1 -1
- package/src/mcp/tools/symbol-children.ts +1 -1
- package/src/mcp/tools/triage.ts +1 -1
- package/src/mcp/tools/where.ts +1 -1
- package/src/mcp/types.ts +21 -0
- package/src/presentation/queries-cli/index.ts +1 -1
- package/src/presentation/queries-cli/overview.ts +35 -1
- package/src/presentation/queries-cli.ts +1 -0
- package/src/presentation/viewer.ts +98 -87
- package/src/shared/constants.ts +26 -0
- package/src/shared/normalize.ts +13 -22
- package/src/shared/paginate.ts +4 -18
- package/src/types.ts +86 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* The orchestrator-selection strategy lives here so `pipeline.ts` stays a thin
|
|
12
12
|
* top-level controller: detect changes, try native, fall back to JS stages.
|
|
13
13
|
*/
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
14
15
|
import path from 'node:path';
|
|
15
16
|
import { performance } from 'node:perf_hooks';
|
|
16
17
|
import {
|
|
@@ -24,12 +25,13 @@ import {
|
|
|
24
25
|
import { debug, info, warn } from '../../../../infrastructure/logger.js';
|
|
25
26
|
import { loadNative } from '../../../../infrastructure/native.js';
|
|
26
27
|
import { semverCompare } from '../../../../infrastructure/update-check.js';
|
|
27
|
-
import { normalizePath } from '../../../../shared/constants.js';
|
|
28
|
+
import { normalizePath, TS_NATIVE_CONFIDENCE_FLOOR } from '../../../../shared/constants.js';
|
|
28
29
|
import { toErrorMessage } from '../../../../shared/errors.js';
|
|
29
30
|
import { CODEGRAPH_VERSION } from '../../../../shared/version.js';
|
|
30
31
|
import type {
|
|
31
32
|
BetterSqlite3Database,
|
|
32
33
|
BuildResult,
|
|
34
|
+
DataflowResult,
|
|
33
35
|
Definition,
|
|
34
36
|
ExtractorOutput,
|
|
35
37
|
SqliteStatement,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
getInstalledWasmExtensions,
|
|
41
43
|
NATIVE_SUPPORTED_EXTENSIONS,
|
|
42
44
|
parseFilesWasmForBackfill,
|
|
45
|
+
patchDataflowResult,
|
|
43
46
|
} from '../../../parser.js';
|
|
44
47
|
import { computeConfidence } from '../../resolve.js';
|
|
45
48
|
import type { CallNodeLookup } from '../call-resolver.js';
|
|
@@ -294,6 +297,257 @@ async function runPostNativeStructure(
|
|
|
294
297
|
return performance.now() - structureStart;
|
|
295
298
|
}
|
|
296
299
|
|
|
300
|
+
/**
|
|
301
|
+
* P6: Build dataflow_vertices and inter-procedural edges after the Rust
|
|
302
|
+
* orchestrator completes.
|
|
303
|
+
*
|
|
304
|
+
* The Rust pipeline writes flows_to/returns/mutates edges directly to the DB
|
|
305
|
+
* but never writes to dataflow_vertices or dataflow_summary. This pass re-runs
|
|
306
|
+
* the Rust dataflow visitor (via extractDataflowAnalysis — fast, no re-parse)
|
|
307
|
+
* to get the DataflowResult and calls buildDataflowVerticesFromMap.
|
|
308
|
+
*
|
|
309
|
+
* Languages for which Rust has no dataflow rules return null from
|
|
310
|
+
* extractDataflowAnalysis and are silently skipped here. A follow-up issue
|
|
311
|
+
* (#1614 adjacent) will add WASM fallback for those languages.
|
|
312
|
+
*/
|
|
313
|
+
async function runDataflowVertexPass(
|
|
314
|
+
ctx: PipelineContext,
|
|
315
|
+
changedFiles: string[] | undefined,
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
if (ctx.opts.dataflow === false) return;
|
|
318
|
+
|
|
319
|
+
const native = loadNative();
|
|
320
|
+
if (!native?.extractDataflowAnalysis) return;
|
|
321
|
+
|
|
322
|
+
// Determine which files to process: changed files for incremental, all for full builds.
|
|
323
|
+
let filesToProcess: string[];
|
|
324
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
325
|
+
filesToProcess = changedFiles;
|
|
326
|
+
} else {
|
|
327
|
+
// Full build: scope to files that need vertex extraction rather than scanning every
|
|
328
|
+
// file in the project. Two categories:
|
|
329
|
+
// (a) Non-native language files — NATIVE_SUPPORTED_EXTENSIONS doesn't cover them,
|
|
330
|
+
// so extractDataflowAnalysis returns null; the wasmStubs path calls buildDataflowEdges
|
|
331
|
+
// which writes both edges AND vertices for those files.
|
|
332
|
+
// (b) Native-language files with dataflow edges already written by the Rust orchestrator
|
|
333
|
+
// (flows_to/returns/mutates) — those need vertex rows to connect them.
|
|
334
|
+
//
|
|
335
|
+
// Skipping native-language files with no dataflow edges is safe: extractDataflowAnalysis
|
|
336
|
+
// would return argFlows=[], assignments=[], mutations=[] for them, producing zero vertices
|
|
337
|
+
// and zero inter-procedural edges. Excluding them avoids O(n_total_files) re-analysis on
|
|
338
|
+
// every full build (codegraph itself: ~2000 files, ~50-80% with no dataflow edges).
|
|
339
|
+
const filesWithDataflow = new Set(
|
|
340
|
+
(
|
|
341
|
+
ctx.db
|
|
342
|
+
.prepare(
|
|
343
|
+
`SELECT DISTINCT n.file
|
|
344
|
+
FROM dataflow d
|
|
345
|
+
JOIN nodes n ON n.id = d.source_id
|
|
346
|
+
WHERE n.file IS NOT NULL`,
|
|
347
|
+
)
|
|
348
|
+
.all() as { file: string }[]
|
|
349
|
+
).map((r) => r.file),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
filesToProcess = (
|
|
353
|
+
ctx.db
|
|
354
|
+
.prepare(`SELECT DISTINCT file FROM nodes WHERE file IS NOT NULL AND kind != 'directory'`)
|
|
355
|
+
.all() as { file: string }[]
|
|
356
|
+
)
|
|
357
|
+
.map((r) => r.file)
|
|
358
|
+
.filter((f) => {
|
|
359
|
+
const ext = path.extname(f).toLowerCase();
|
|
360
|
+
// Non-native files: always include (WASM handles them via wasmStubs path).
|
|
361
|
+
if (!NATIVE_SUPPORTED_EXTENSIONS.has(ext)) return true;
|
|
362
|
+
// Native files: only include when Rust wrote dataflow edges for them.
|
|
363
|
+
return filesWithDataflow.has(f);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Split files into two buckets:
|
|
368
|
+
// nativeDataflow — Rust extracted data (vertex-only pass; edges already in DB)
|
|
369
|
+
// wasmStubs — Rust returned null (WASM will handle edges + vertices)
|
|
370
|
+
const nativeDataflow = new Map<string, DataflowResult>();
|
|
371
|
+
const wasmStubs = new Map<string, { definitions: []; _langId: null; _tree: null }>();
|
|
372
|
+
|
|
373
|
+
const absPaths = filesToProcess.map((relPath) => path.join(ctx.rootDir, relPath));
|
|
374
|
+
|
|
375
|
+
// Batch the per-file dataflow extraction into one NAPI call so the parses run
|
|
376
|
+
// across the rayon thread pool instead of serially on the event loop — this is
|
|
377
|
+
// the dominant cost of a native full build (#perf). Older addons predate the
|
|
378
|
+
// batch export, so fall back to the per-file path when it is unavailable.
|
|
379
|
+
let batchResults: (DataflowResult | null)[] | null = null;
|
|
380
|
+
if (typeof native.extractDataflowAnalysisBatch === 'function') {
|
|
381
|
+
try {
|
|
382
|
+
batchResults = native.extractDataflowAnalysisBatch(absPaths);
|
|
383
|
+
} catch {
|
|
384
|
+
batchResults = null; // fall through to per-file extraction below
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (let i = 0; i < filesToProcess.length; i++) {
|
|
389
|
+
const relPath = filesToProcess[i]!;
|
|
390
|
+
let result: DataflowResult | null = null;
|
|
391
|
+
if (batchResults) {
|
|
392
|
+
result = batchResults[i] ?? null;
|
|
393
|
+
} else {
|
|
394
|
+
let source: string;
|
|
395
|
+
try {
|
|
396
|
+
source = readFileSafe(absPaths[i]!);
|
|
397
|
+
} catch {
|
|
398
|
+
// Unreadable file — mirror batch-path behaviour and route to WASM.
|
|
399
|
+
wasmStubs.set(relPath, { definitions: [], _langId: null, _tree: null });
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (!source) {
|
|
403
|
+
// Empty file — same treatment as batch returning null.
|
|
404
|
+
wasmStubs.set(relPath, { definitions: [], _langId: null, _tree: null });
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
result = native.extractDataflowAnalysis(source, absPaths[i]!);
|
|
409
|
+
} catch {
|
|
410
|
+
// Language-specific parse failure — fall through to WASM.
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (result) {
|
|
414
|
+
// Normalise the native DataflowResult: Rust emits `bindingType: string | null`
|
|
415
|
+
// (flat) while the TS dataflow layer expects `binding: { type, index? }` (object).
|
|
416
|
+
// patchNativeResult handles this via patchDataflow for the full parse path;
|
|
417
|
+
// extractDataflowAnalysis(Batch) is a vertex-only fast path that bypasses
|
|
418
|
+
// patchNativeResult, so we apply the same normalisation here.
|
|
419
|
+
patchDataflowResult(result);
|
|
420
|
+
nativeDataflow.set(relPath, result);
|
|
421
|
+
} else {
|
|
422
|
+
// Rust has no dataflow rules for this language; WASM fallback will handle
|
|
423
|
+
// both edge insertion and vertex extraction. Since Rust inserted 0 dataflow
|
|
424
|
+
// edges for these files, there is no risk of duplicates.
|
|
425
|
+
wasmStubs.set(relPath, { definitions: [], _langId: null, _tree: null });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const { buildExtToLangMap } = (await import('../../../../ast-analysis/shared.js')) as {
|
|
430
|
+
buildExtToLangMap: () => Map<string, string>;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const {
|
|
434
|
+
buildDataflowVerticesFromMap,
|
|
435
|
+
buildDataflowEdges,
|
|
436
|
+
collectCallerStitchCandidates,
|
|
437
|
+
collectFuncIdsForFiles,
|
|
438
|
+
} = (await import('../../../../features/dataflow.js')) as {
|
|
439
|
+
buildDataflowVerticesFromMap: (
|
|
440
|
+
db: BetterSqlite3Database,
|
|
441
|
+
dataflowMap: Map<string, DataflowResult>,
|
|
442
|
+
extraCandidates?: Array<{
|
|
443
|
+
callerFuncId: number;
|
|
444
|
+
calleeFuncId: number;
|
|
445
|
+
argIndex: number;
|
|
446
|
+
bindingType: string;
|
|
447
|
+
bindingIndex: number;
|
|
448
|
+
argName: string;
|
|
449
|
+
expression: string | null;
|
|
450
|
+
line: number;
|
|
451
|
+
confidence: number;
|
|
452
|
+
}>,
|
|
453
|
+
extraCaptures?: Array<{ callerFuncId: number; calleeFuncId: number; varName: string }>,
|
|
454
|
+
) => number;
|
|
455
|
+
buildDataflowEdges: (
|
|
456
|
+
db: BetterSqlite3Database,
|
|
457
|
+
fileSymbols: Map<string, unknown>,
|
|
458
|
+
rootDir: string,
|
|
459
|
+
engineOpts?: unknown,
|
|
460
|
+
) => Promise<void>;
|
|
461
|
+
collectCallerStitchCandidates: (
|
|
462
|
+
db: BetterSqlite3Database,
|
|
463
|
+
changedFuncIds: number[],
|
|
464
|
+
changedRelPaths: Set<string>,
|
|
465
|
+
rootDir: string,
|
|
466
|
+
extToLang: Map<string, string>,
|
|
467
|
+
parsers: unknown,
|
|
468
|
+
getParserFn: unknown,
|
|
469
|
+
) => Promise<{
|
|
470
|
+
candidates: Array<{
|
|
471
|
+
callerFuncId: number;
|
|
472
|
+
calleeFuncId: number;
|
|
473
|
+
argIndex: number;
|
|
474
|
+
bindingType: string;
|
|
475
|
+
bindingIndex: number;
|
|
476
|
+
argName: string;
|
|
477
|
+
expression: string | null;
|
|
478
|
+
line: number;
|
|
479
|
+
confidence: number;
|
|
480
|
+
}>;
|
|
481
|
+
captures: Array<{ callerFuncId: number; calleeFuncId: number; varName: string }>;
|
|
482
|
+
}>;
|
|
483
|
+
collectFuncIdsForFiles: (db: BetterSqlite3Database, relPaths: Iterable<string>) => number[];
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// Rust-supported languages: build vertices only (edges already written by Rust orchestrator).
|
|
487
|
+
if (nativeDataflow.size > 0) {
|
|
488
|
+
// P4: On incremental builds, unchanged caller files' arg_in edges were deleted when
|
|
489
|
+
// the changed files' param vertices were purged and recreated. Re-collect stitch
|
|
490
|
+
// candidates from those caller files so buildInterproceduralStitch can reconnect them.
|
|
491
|
+
// Skip on full builds (changedFiles absent/empty) — nativeDataflow covers all files.
|
|
492
|
+
let p4Candidates: Array<{
|
|
493
|
+
callerFuncId: number;
|
|
494
|
+
calleeFuncId: number;
|
|
495
|
+
argIndex: number;
|
|
496
|
+
bindingType: string;
|
|
497
|
+
bindingIndex: number;
|
|
498
|
+
argName: string;
|
|
499
|
+
expression: string | null;
|
|
500
|
+
line: number;
|
|
501
|
+
confidence: number;
|
|
502
|
+
}> = [];
|
|
503
|
+
let p4Captures: Array<{ callerFuncId: number; calleeFuncId: number; varName: string }> = [];
|
|
504
|
+
|
|
505
|
+
if (changedFiles && changedFiles.length > 0) {
|
|
506
|
+
const changedSet = new Set(changedFiles);
|
|
507
|
+
const totalFilesInDb = (
|
|
508
|
+
ctx.db.prepare(`SELECT COUNT(DISTINCT file) AS n FROM nodes`).get() as { n: number }
|
|
509
|
+
).n;
|
|
510
|
+
// Only run P4 when this is a real incremental build (not all files changed).
|
|
511
|
+
if (nativeDataflow.size < totalFilesInDb) {
|
|
512
|
+
const changedFuncIds = collectFuncIdsForFiles(ctx.db, changedSet);
|
|
513
|
+
if (changedFuncIds.length > 0) {
|
|
514
|
+
const extra = await collectCallerStitchCandidates(
|
|
515
|
+
ctx.db,
|
|
516
|
+
changedFuncIds,
|
|
517
|
+
changedSet,
|
|
518
|
+
ctx.rootDir,
|
|
519
|
+
buildExtToLangMap(),
|
|
520
|
+
null, // parsers — lazily loaded inside collectCallerStitchCandidates
|
|
521
|
+
null, // getParserFn — lazily loaded inside collectCallerStitchCandidates
|
|
522
|
+
);
|
|
523
|
+
p4Candidates = extra.candidates as typeof p4Candidates;
|
|
524
|
+
p4Captures = extra.captures;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const interCount = buildDataflowVerticesFromMap(
|
|
530
|
+
ctx.db,
|
|
531
|
+
nativeDataflow,
|
|
532
|
+
p4Candidates.length > 0 ? p4Candidates : undefined,
|
|
533
|
+
p4Captures.length > 0 ? p4Captures : undefined,
|
|
534
|
+
);
|
|
535
|
+
if (interCount > 0) {
|
|
536
|
+
info(
|
|
537
|
+
`Dataflow (native orchestrator): ${interCount} inter-procedural edges inserted${p4Candidates.length > 0 ? ` (P4: ${p4Candidates.length} re-stitch candidate(s) from unchanged callers)` : ''}`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Rust-unsupported languages: run the full WASM extraction (edges + vertices).
|
|
543
|
+
// wasmStubs entries have no `.dataflow` property, so the native bulk-insert
|
|
544
|
+
// fast path in buildDataflowEdges is always skipped for them — WASM runs
|
|
545
|
+
// both edge insertion and vertex extraction end-to-end.
|
|
546
|
+
if (wasmStubs.size > 0) {
|
|
547
|
+
await buildDataflowEdges(ctx.db, wasmStubs, ctx.rootDir);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
297
551
|
/**
|
|
298
552
|
* JS fallback for AST/complexity/CFG/dataflow analysis after native orchestrator.
|
|
299
553
|
* Used when the Rust addon doesn't include analysis persistence (older addon
|
|
@@ -389,53 +643,10 @@ async function runPostNativeAnalysis(
|
|
|
389
643
|
return timing;
|
|
390
644
|
}
|
|
391
645
|
|
|
392
|
-
|
|
393
|
-
* Phase 8.6: CHA expansion post-pass for the native orchestrator path.
|
|
394
|
-
*
|
|
395
|
-
* The Rust build pipeline resolves typed receiver calls (e.g. `worker.doWork()`
|
|
396
|
-
* where `worker: IWorker`) to the interface method declaration only. This
|
|
397
|
-
* post-pass reads the class hierarchy (via `implements`/`extends` edges) and
|
|
398
|
-
* instantiated types (via `calls` edges to class nodes) from the DB and expands
|
|
399
|
-
* each call to an interface/abstract method to ALL RTA-filtered concrete
|
|
400
|
-
* implementations.
|
|
401
|
-
*
|
|
402
|
-
* Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
|
|
403
|
-
* which WASM-re-parses JS/TS files to obtain raw call site receiver info.
|
|
404
|
-
*
|
|
405
|
-
* `changedFiles` controls candidate scoping on incremental builds:
|
|
406
|
-
* - null → full build; scan all call→method edges (existing behaviour).
|
|
407
|
-
* - array → incremental; two cheap gate queries decide scope:
|
|
408
|
-
* Gate A: any class/interface/trait/struct/record nodes in changed files?
|
|
409
|
-
* If yes, a new implementor may have appeared — full scan required.
|
|
410
|
-
* Gate B: any `calls` edges from changed-file sources targeting
|
|
411
|
-
* class/constructor/function-kind nodes? If yes, the RTA set may
|
|
412
|
-
* have grown (also covers the older-schema fallback where
|
|
413
|
-
* constructor calls target `constructor`/`function` nodes instead
|
|
414
|
-
* of `class` nodes) — full scan required.
|
|
415
|
-
* If neither gate fires: scope `callToMethods` to `src.file IN changedFiles`
|
|
416
|
-
* (safe because no hierarchy or RTA evidence changed).
|
|
417
|
-
*
|
|
418
|
-
* Returns the count of newly inserted CHA edges plus the set of files containing
|
|
419
|
-
* the new edges' endpoints, so the caller can scope role re-classification to the
|
|
420
|
-
* nodes whose fan-in/out actually changed. A zero count means no edges were added
|
|
421
|
-
* and role re-classification is unnecessary.
|
|
422
|
-
*/
|
|
423
|
-
function runPostNativeCha(
|
|
424
|
-
db: BetterSqlite3Database,
|
|
425
|
-
changedFiles: string[] | null,
|
|
426
|
-
): {
|
|
427
|
-
newEdgeCount: number;
|
|
428
|
-
affectedFiles: Set<string>;
|
|
429
|
-
} {
|
|
430
|
-
const affectedFiles = new Set<string>();
|
|
431
|
-
const empty = { newEdgeCount: 0, affectedFiles };
|
|
432
|
-
// Fast guard: no hierarchy edges → no CHA work
|
|
433
|
-
const hasHierarchy = db
|
|
434
|
-
.prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
|
|
435
|
-
.get();
|
|
436
|
-
if (!hasHierarchy) return empty;
|
|
646
|
+
// ── CHA post-pass helpers ────────────────────────────────────────────────────
|
|
437
647
|
|
|
438
|
-
|
|
648
|
+
/** Build implementors map: parent/interface name → [child/implementing class names]. */
|
|
649
|
+
function buildChaImplementorsMap(db: BetterSqlite3Database): Map<string, string[]> {
|
|
439
650
|
const hierarchyRows = db
|
|
440
651
|
.prepare(`
|
|
441
652
|
SELECT src.name AS child_name, tgt.name AS parent_name
|
|
@@ -455,12 +666,22 @@ function runPostNativeCha(
|
|
|
455
666
|
}
|
|
456
667
|
if (!list.includes(row.child_name)) list.push(row.child_name);
|
|
457
668
|
}
|
|
458
|
-
|
|
669
|
+
return implementors;
|
|
670
|
+
}
|
|
459
671
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Build RTA set: class names actually instantiated via `new X()`.
|
|
674
|
+
* Primary query targets `class`-kind nodes (the canonical schema).
|
|
675
|
+
* Fallback also matches `constructor`/`function`-kind nodes because some native
|
|
676
|
+
* engine versions record constructor calls against those kinds instead of `class`.
|
|
677
|
+
* Returns `{ instantiated, noRtaEvidence }` where `noRtaEvidence` means no
|
|
678
|
+
* constructor-call evidence exists — skip RTA filtering so interface dispatch
|
|
679
|
+
* still produces edges.
|
|
680
|
+
*/
|
|
681
|
+
function buildChaRtaSet(db: BetterSqlite3Database): {
|
|
682
|
+
instantiated: Set<string>;
|
|
683
|
+
noRtaEvidence: boolean;
|
|
684
|
+
} {
|
|
464
685
|
let rtaRows = db
|
|
465
686
|
.prepare(`
|
|
466
687
|
SELECT DISTINCT tgt.name
|
|
@@ -482,110 +703,104 @@ function runPostNativeCha(
|
|
|
482
703
|
.all() as Array<{ name: string }>;
|
|
483
704
|
}
|
|
484
705
|
const instantiated = new Set(rtaRows.map((r) => r.name));
|
|
485
|
-
// noRtaEvidence: true when no constructor-call evidence exists in the DB (e.g. graph
|
|
486
|
-
// built by an older native engine that doesn't emit constructor call edges at all).
|
|
487
|
-
// In that case we skip RTA filtering so interface dispatch still produces edges —
|
|
488
|
-
// all instantiated implementors are admitted rather than silently dropping everything.
|
|
489
706
|
const noRtaEvidence = instantiated.size === 0;
|
|
490
707
|
if (noRtaEvidence) {
|
|
491
708
|
debug('runPostNativeCha: no constructor-call evidence found — proceeding without RTA filter');
|
|
492
709
|
}
|
|
710
|
+
return { instantiated, noRtaEvidence };
|
|
711
|
+
}
|
|
493
712
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (changedFiles
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
713
|
+
/**
|
|
714
|
+
* Determine CHA candidate scope for incremental builds.
|
|
715
|
+
*
|
|
716
|
+
* Gate A: did a changed file add/change a class hierarchy node?
|
|
717
|
+
* A new `extends`/`implements` edge means a previously-untracked implementor
|
|
718
|
+
* is now in the hierarchy — unchanged call sites in OTHER files may gain new
|
|
719
|
+
* valid expansions, so the full scan is required.
|
|
720
|
+
* Note: *removed* class nodes are safe — Rust's `purge_changed_files` runs
|
|
721
|
+
* before this post-pass and deletes stale nodes and their hierarchy edges, so
|
|
722
|
+
* Gate A queries the post-purge DB. A deleted class returns no row here, which
|
|
723
|
+
* is correct: its stale CHA edges were already cleaned up by the Rust purge.
|
|
724
|
+
*
|
|
725
|
+
* Gate B: did a changed file add new RTA evidence (`new ConcreteX()`)?
|
|
726
|
+
* A new `calls` edge to a class/constructor/function-kind target means the
|
|
727
|
+
* instantiated set grew — previously RTA-filtered expansions in unchanged
|
|
728
|
+
* caller files become admissible, so the full scan is required.
|
|
729
|
+
* (`constructor`/`function` cover the older native engine fallback schema.)
|
|
730
|
+
*
|
|
731
|
+
* Returns `true` when the scan should be scoped to changed-file sources only.
|
|
732
|
+
* Returns `false` (full scan) when changedFiles is null, empty, or either gate fires.
|
|
733
|
+
*/
|
|
734
|
+
function computeChaScope(db: BetterSqlite3Database, changedFiles: string[] | null): boolean {
|
|
735
|
+
if (changedFiles === null || changedFiles.length === 0) return false;
|
|
736
|
+
|
|
737
|
+
const CHUNK_SIZE = 500;
|
|
738
|
+
let gateAFired = false;
|
|
739
|
+
for (let i = 0; i < changedFiles.length && !gateAFired; i += CHUNK_SIZE) {
|
|
740
|
+
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
741
|
+
const ph = chunk.map(() => '?').join(',');
|
|
742
|
+
const row = db
|
|
743
|
+
.prepare(
|
|
744
|
+
`SELECT 1 FROM nodes
|
|
745
|
+
WHERE file IN (${ph})
|
|
746
|
+
AND kind IN ('class', 'interface', 'trait', 'struct', 'record')
|
|
747
|
+
LIMIT 1`,
|
|
748
|
+
)
|
|
749
|
+
.get(...chunk);
|
|
750
|
+
if (row) gateAFired = true;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
let gateBFired = false;
|
|
754
|
+
if (!gateAFired) {
|
|
755
|
+
for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) {
|
|
521
756
|
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
522
757
|
const ph = chunk.map(() => '?').join(',');
|
|
523
758
|
const row = db
|
|
524
759
|
.prepare(
|
|
525
|
-
`SELECT 1 FROM
|
|
526
|
-
|
|
527
|
-
|
|
760
|
+
`SELECT 1 FROM edges e
|
|
761
|
+
JOIN nodes src ON e.source_id = src.id
|
|
762
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
763
|
+
WHERE e.kind = 'calls'
|
|
764
|
+
AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record', 'constructor', 'function')
|
|
765
|
+
AND src.file IN (${ph})
|
|
528
766
|
LIMIT 1`,
|
|
529
767
|
)
|
|
530
768
|
.get(...chunk);
|
|
531
|
-
if (row)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Gate B: calls from changed-file sources to class/instantiable-kind targets
|
|
535
|
-
// (also covers older-schema fallback and future CHA extensions to struct/record).
|
|
536
|
-
// Includes class/interface/trait/struct/record (future CHA extension safety) and
|
|
537
|
-
// constructor/function (older native engine schema fallback).
|
|
538
|
-
let gateBFired = false;
|
|
539
|
-
if (!gateAFired) {
|
|
540
|
-
for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) {
|
|
541
|
-
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
542
|
-
const ph = chunk.map(() => '?').join(',');
|
|
543
|
-
const row = db
|
|
544
|
-
.prepare(
|
|
545
|
-
`SELECT 1 FROM edges e
|
|
546
|
-
JOIN nodes src ON e.source_id = src.id
|
|
547
|
-
JOIN nodes tgt ON e.target_id = tgt.id
|
|
548
|
-
WHERE e.kind = 'calls'
|
|
549
|
-
AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record', 'constructor', 'function')
|
|
550
|
-
AND src.file IN (${ph})
|
|
551
|
-
LIMIT 1`,
|
|
552
|
-
)
|
|
553
|
-
.get(...chunk);
|
|
554
|
-
if (row) gateBFired = true;
|
|
555
|
-
}
|
|
769
|
+
if (row) gateBFired = true;
|
|
556
770
|
}
|
|
771
|
+
}
|
|
557
772
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
} else {
|
|
564
|
-
debug(
|
|
565
|
-
`runPostNativeCha: ${gateAFired ? 'Gate A (hierarchy)' : 'Gate B (RTA)'} fired — running full scan`,
|
|
566
|
-
);
|
|
567
|
-
}
|
|
773
|
+
if (!gateAFired && !gateBFired) {
|
|
774
|
+
debug(
|
|
775
|
+
`runPostNativeCha: neither gate fired — scoping candidate scan to ${changedFiles.length} changed file(s)`,
|
|
776
|
+
);
|
|
777
|
+
return true;
|
|
568
778
|
}
|
|
779
|
+
debug(
|
|
780
|
+
`runPostNativeCha: ${gateAFired ? 'Gate A (hierarchy)' : 'Gate B (RTA)'} fired — running full scan`,
|
|
781
|
+
);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
569
784
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
785
|
+
type ChaCallRow = {
|
|
786
|
+
source_id: number;
|
|
787
|
+
caller_name: string;
|
|
788
|
+
method_name: string;
|
|
789
|
+
caller_file: string | null;
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Fetch call→method rows that are candidates for CHA expansion.
|
|
794
|
+
* When `scopeToChangedFiles` is true, restricts to source nodes in `changedFiles`.
|
|
795
|
+
*/
|
|
796
|
+
function fetchChaCallToMethods(
|
|
797
|
+
db: BetterSqlite3Database,
|
|
798
|
+
changedFiles: string[] | null,
|
|
799
|
+
scopeToChangedFiles: boolean,
|
|
800
|
+
): ChaCallRow[] {
|
|
581
801
|
if (scopeToChangedFiles && changedFiles && changedFiles.length > 0) {
|
|
582
802
|
const CHUNK_SIZE = 500;
|
|
583
|
-
const rows:
|
|
584
|
-
source_id: number;
|
|
585
|
-
caller_name: string;
|
|
586
|
-
method_name: string;
|
|
587
|
-
caller_file: string | null;
|
|
588
|
-
}> = [];
|
|
803
|
+
const rows: ChaCallRow[] = [];
|
|
589
804
|
for (let i = 0; i < changedFiles.length; i += CHUNK_SIZE) {
|
|
590
805
|
const chunk = changedFiles.slice(i, i + CHUNK_SIZE);
|
|
591
806
|
const ph = chunk.map(() => '?').join(',');
|
|
@@ -600,33 +815,36 @@ function runPostNativeCha(
|
|
|
600
815
|
AND (e.technique IS NULL OR e.technique != 'cha-expanded')
|
|
601
816
|
AND src.file IN (${ph})`,
|
|
602
817
|
)
|
|
603
|
-
.all(...chunk) as
|
|
604
|
-
source_id: number;
|
|
605
|
-
caller_name: string;
|
|
606
|
-
method_name: string;
|
|
607
|
-
caller_file: string | null;
|
|
608
|
-
}>;
|
|
818
|
+
.all(...chunk) as ChaCallRow[];
|
|
609
819
|
rows.push(...chunkRows);
|
|
610
820
|
}
|
|
611
|
-
|
|
612
|
-
} else {
|
|
613
|
-
callToMethods = db
|
|
614
|
-
.prepare(`
|
|
615
|
-
SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
|
|
616
|
-
FROM edges e
|
|
617
|
-
JOIN nodes tgt ON e.target_id = tgt.id
|
|
618
|
-
JOIN nodes src ON e.source_id = src.id
|
|
619
|
-
WHERE e.kind = 'calls' AND tgt.kind = 'method'
|
|
620
|
-
AND INSTR(tgt.name, '.') > 0
|
|
621
|
-
AND (e.technique IS NULL OR e.technique != 'cha-expanded')
|
|
622
|
-
`)
|
|
623
|
-
.all() as Array<{
|
|
624
|
-
source_id: number;
|
|
625
|
-
caller_name: string;
|
|
626
|
-
method_name: string;
|
|
627
|
-
caller_file: string | null;
|
|
628
|
-
}>;
|
|
821
|
+
return rows;
|
|
629
822
|
}
|
|
823
|
+
return db
|
|
824
|
+
.prepare(`
|
|
825
|
+
SELECT e.source_id, src.name AS caller_name, tgt.name AS method_name, src.file AS caller_file
|
|
826
|
+
FROM edges e
|
|
827
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
828
|
+
JOIN nodes src ON e.source_id = src.id
|
|
829
|
+
WHERE e.kind = 'calls' AND tgt.kind = 'method'
|
|
830
|
+
AND INSTR(tgt.name, '.') > 0
|
|
831
|
+
AND (e.technique IS NULL OR e.technique != 'cha-expanded')
|
|
832
|
+
`)
|
|
833
|
+
.all() as ChaCallRow[];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* BFS-expand CHA call edges and insert new edges into the DB.
|
|
838
|
+
* Returns `{ newEdgeCount, affectedFiles }` for role re-classification scoping.
|
|
839
|
+
*/
|
|
840
|
+
function expandChaEdges(
|
|
841
|
+
db: BetterSqlite3Database,
|
|
842
|
+
callToMethods: ChaCallRow[],
|
|
843
|
+
implementors: Map<string, string[]>,
|
|
844
|
+
instantiated: Set<string>,
|
|
845
|
+
noRtaEvidence: boolean,
|
|
846
|
+
): { newEdgeCount: number; affectedFiles: Set<string> } {
|
|
847
|
+
const affectedFiles = new Set<string>();
|
|
630
848
|
|
|
631
849
|
// Seed seen-pairs only from the source_ids we'll be expanding — avoids loading every
|
|
632
850
|
// call edge in the DB (which would be O(all edges)) for large codebases.
|
|
@@ -707,88 +925,107 @@ function runPostNativeCha(
|
|
|
707
925
|
return { newEdgeCount, affectedFiles };
|
|
708
926
|
}
|
|
709
927
|
|
|
710
|
-
// Extensions where `this`/`super` dispatch can occur (JS/TS family)
|
|
711
|
-
const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
|
|
712
|
-
|
|
713
928
|
/**
|
|
714
|
-
* Phase 8.
|
|
929
|
+
* Phase 8.6: CHA expansion post-pass for the native orchestrator path.
|
|
715
930
|
*
|
|
716
|
-
* The Rust build pipeline resolves typed receiver calls
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
* `
|
|
720
|
-
*
|
|
931
|
+
* The Rust build pipeline resolves typed receiver calls (e.g. `worker.doWork()`
|
|
932
|
+
* where `worker: IWorker`) to the interface method declaration only. This
|
|
933
|
+
* post-pass reads the class hierarchy (via `implements`/`extends` edges) and
|
|
934
|
+
* instantiated types (via `calls` edges to class nodes) from the DB and expands
|
|
935
|
+
* each call to an interface/abstract method to ALL RTA-filtered concrete
|
|
936
|
+
* implementations.
|
|
721
937
|
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
* `resolveThisDispatch` resolves them by treating the dot-prefix of the caller name
|
|
725
|
-
* (`f` from `f.h`) as the class and looking up `f.g` directly — no `extends` edge needed.
|
|
938
|
+
* Note: `this`/`super` dispatch is handled separately by `runPostNativeThisDispatch`,
|
|
939
|
+
* which WASM-re-parses JS/TS files to obtain raw call site receiver info.
|
|
726
940
|
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
941
|
+
* `changedFiles` controls candidate scoping on incremental builds:
|
|
942
|
+
* - null → full build; scan all call→method edges (existing behaviour).
|
|
943
|
+
* - array → incremental; two cheap gate queries decide scope:
|
|
944
|
+
* Gate A: any class/interface/trait/struct/record nodes in changed files?
|
|
945
|
+
* If yes, a new implementor may have appeared — full scan required.
|
|
946
|
+
* Gate B: any `calls` edges from changed-file sources targeting
|
|
947
|
+
* class/constructor/function-kind nodes? If yes, the RTA set may
|
|
948
|
+
* have grown (also covers the older-schema fallback where
|
|
949
|
+
* constructor calls target `constructor`/`function` nodes instead
|
|
950
|
+
* of `class` nodes) — full scan required.
|
|
951
|
+
* If neither gate fires: scope `callToMethods` to `src.file IN changedFiles`
|
|
952
|
+
* (safe because no hierarchy or RTA evidence changed).
|
|
953
|
+
*
|
|
954
|
+
* Returns the count of newly inserted CHA edges plus the set of files containing
|
|
955
|
+
* the new edges' endpoints, so the caller can scope role re-classification to the
|
|
956
|
+
* nodes whose fan-in/out actually changed. A zero count means no edges were added
|
|
957
|
+
* and role re-classification is unnecessary.
|
|
729
958
|
*/
|
|
730
|
-
|
|
959
|
+
function runPostNativeCha(
|
|
731
960
|
db: BetterSqlite3Database,
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const targetIds = new Set<number>();
|
|
738
|
-
// Files containing endpoints of newly inserted edges — lets the caller scope
|
|
739
|
-
// role re-classification to the nodes whose fan-in/out actually changed.
|
|
961
|
+
changedFiles: string[] | null,
|
|
962
|
+
): {
|
|
963
|
+
newEdgeCount: number;
|
|
964
|
+
affectedFiles: Set<string>;
|
|
965
|
+
} {
|
|
740
966
|
const affectedFiles = new Set<string>();
|
|
741
|
-
|
|
742
|
-
// Fast guard:
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
|
|
746
|
-
const hasFuncPropMethod = db
|
|
747
|
-
.prepare(`SELECT 1 FROM nodes WHERE kind = 'method' AND INSTR(name, '.') > 0 LIMIT 1`)
|
|
967
|
+
const empty = { newEdgeCount: 0, affectedFiles };
|
|
968
|
+
// Fast guard: no hierarchy edges → no CHA work
|
|
969
|
+
const hasHierarchy = db
|
|
970
|
+
.prepare(`SELECT 1 FROM edges WHERE kind IN ('extends', 'implements') LIMIT 1`)
|
|
748
971
|
.get();
|
|
749
|
-
if (!
|
|
750
|
-
|
|
751
|
-
// Build parents map: child class → direct parent class (from `extends` edges).
|
|
752
|
-
// May be empty when only func-prop methods exist (no class inheritance) —
|
|
753
|
-
// resolveThisDispatch handles that case via direct class-prefix lookup.
|
|
754
|
-
const parentRows = hasExtends
|
|
755
|
-
? (db
|
|
756
|
-
.prepare(`
|
|
757
|
-
SELECT src.name AS child_name, tgt.name AS parent_name
|
|
758
|
-
FROM edges e
|
|
759
|
-
JOIN nodes src ON e.source_id = src.id
|
|
760
|
-
JOIN nodes tgt ON e.target_id = tgt.id
|
|
761
|
-
WHERE e.kind = 'extends'
|
|
762
|
-
`)
|
|
763
|
-
.all() as Array<{ child_name: string; parent_name: string }>)
|
|
764
|
-
: [];
|
|
972
|
+
if (!hasHierarchy) return empty;
|
|
765
973
|
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
if (!parents.has(row.child_name)) parents.set(row.child_name, row.parent_name);
|
|
769
|
-
}
|
|
770
|
-
// Note: parents may be empty when hasFuncPropMethod but !hasExtends — that is
|
|
771
|
-
// intentional. resolveThisDispatch still resolves `this.g()` inside `f.h` by
|
|
772
|
-
// treating `f` (the dot-prefix of callerName `f.h`) as the class and looking
|
|
773
|
-
// up `f.g` directly via lookup.byName(), without traversing the parents chain.
|
|
974
|
+
const implementors = buildChaImplementorsMap(db);
|
|
975
|
+
if (implementors.size === 0) return empty;
|
|
774
976
|
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
instantiatedTypes: new Set(), // not needed for this/super resolution
|
|
779
|
-
};
|
|
977
|
+
const { instantiated, noRtaEvidence } = buildChaRtaSet(db);
|
|
978
|
+
const scopeToChangedFiles = computeChaScope(db, changedFiles);
|
|
979
|
+
const callToMethods = fetchChaCallToMethods(db, changedFiles, scopeToChangedFiles);
|
|
780
980
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
981
|
+
return expandChaEdges(db, callToMethods, implementors, instantiated, noRtaEvidence);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Extensions where `this`/`super` dispatch can occur (JS/TS family)
|
|
985
|
+
const THIS_DISPATCH_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
|
|
986
|
+
|
|
987
|
+
// ── this/super dispatch post-pass helpers ───────────────────────────────────
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Build parents map: child class → direct parent class (from `extends` edges).
|
|
991
|
+
* May be empty when only func-prop methods exist (no class inheritance) —
|
|
992
|
+
* resolveThisDispatch handles that case via direct class-prefix lookup.
|
|
993
|
+
*/
|
|
994
|
+
function buildThisDispatchParentsMap(
|
|
995
|
+
db: BetterSqlite3Database,
|
|
996
|
+
hasExtends: unknown,
|
|
997
|
+
): Map<string, string> {
|
|
998
|
+
const parents = new Map<string, string>();
|
|
999
|
+
if (!hasExtends) return parents;
|
|
1000
|
+
const parentRows = db
|
|
1001
|
+
.prepare(`
|
|
1002
|
+
SELECT src.name AS child_name, tgt.name AS parent_name
|
|
1003
|
+
FROM edges e
|
|
1004
|
+
JOIN nodes src ON e.source_id = src.id
|
|
1005
|
+
JOIN nodes tgt ON e.target_id = tgt.id
|
|
1006
|
+
WHERE e.kind = 'extends'
|
|
1007
|
+
`)
|
|
1008
|
+
.all() as Array<{ child_name: string; parent_name: string }>;
|
|
1009
|
+
for (const row of parentRows) {
|
|
1010
|
+
if (!parents.has(row.child_name)) parents.set(row.child_name, row.parent_name);
|
|
1011
|
+
}
|
|
1012
|
+
return parents;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Determine the set of relative file paths to re-parse for this/super dispatch.
|
|
1017
|
+
*
|
|
1018
|
+
* On a full build we do NOT re-parse every JS/TS file — that would WASM-parse
|
|
1019
|
+
* the entire project on top of the native pass, causing a massive regression
|
|
1020
|
+
* (measured: +358% ms/file on codegraph itself). Instead we restrict to files
|
|
1021
|
+
* that are part of the class inheritance hierarchy OR that contain dot-named
|
|
1022
|
+
* method nodes (func-prop assignments whose bodies may call `this.sibling()`).
|
|
1023
|
+
*/
|
|
1024
|
+
function selectThisDispatchFiles(
|
|
1025
|
+
db: BetterSqlite3Database,
|
|
1026
|
+
changedFiles: string[] | undefined,
|
|
1027
|
+
isFullBuild: boolean,
|
|
1028
|
+
): string[] {
|
|
792
1029
|
if (isFullBuild || !changedFiles) {
|
|
793
1030
|
const rows = db
|
|
794
1031
|
.prepare(`
|
|
@@ -820,76 +1057,42 @@ async function runPostNativeThisDispatch(
|
|
|
820
1057
|
)
|
|
821
1058
|
`)
|
|
822
1059
|
.all() as Array<{ file: string }>;
|
|
823
|
-
|
|
1060
|
+
return rows
|
|
824
1061
|
.map((r) => r.file)
|
|
825
1062
|
.filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
|
|
826
|
-
} else {
|
|
827
|
-
// NOTE: Only files explicitly listed in changedFiles are re-parsed.
|
|
828
|
-
// If a parent-class method is replaced (new node ID) but the child file is
|
|
829
|
-
// unchanged, the stale super.method() edge is not refreshed here. A full
|
|
830
|
-
// rebuild (isFullBuild=true) is required to recover in that scenario.
|
|
831
|
-
relFiles = changedFiles.filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
|
|
832
|
-
}
|
|
833
|
-
if (relFiles.length === 0) return { elapsedMs: 0, targetIds, affectedFiles };
|
|
834
|
-
|
|
835
|
-
// DB-backed CallNodeLookup — resolveThisDispatch only calls byName()
|
|
836
|
-
const findByNameStmt = db.prepare(`SELECT id, file, kind FROM nodes WHERE name = ?`);
|
|
837
|
-
const lookup: CallNodeLookup = {
|
|
838
|
-
byName: (name) => findByNameStmt.all(name) as Array<{ id: number; file: string; kind: string }>,
|
|
839
|
-
byNameAndFile: (name, file) =>
|
|
840
|
-
(findByNameStmt.all(name) as Array<{ id: number; file: string; kind: string }>).filter(
|
|
841
|
-
(n) => n.file === file,
|
|
842
|
-
),
|
|
843
|
-
isBarrel: () => false,
|
|
844
|
-
resolveBarrel: () => null,
|
|
845
|
-
nodeId: () => undefined,
|
|
846
|
-
};
|
|
847
|
-
|
|
848
|
-
// Seed seen-pairs from existing call edges on source nodes in our file set
|
|
849
|
-
const seen = new Set<string>();
|
|
850
|
-
const CHUNK = 500;
|
|
851
|
-
for (let i = 0; i < relFiles.length; i += CHUNK) {
|
|
852
|
-
const chunk = relFiles.slice(i, i + CHUNK);
|
|
853
|
-
const ph = chunk.map(() => '?').join(',');
|
|
854
|
-
const rows = db
|
|
855
|
-
.prepare(
|
|
856
|
-
`SELECT e.source_id, e.target_id
|
|
857
|
-
FROM edges e
|
|
858
|
-
JOIN nodes n ON e.source_id = n.id
|
|
859
|
-
WHERE e.kind = 'calls' AND n.file IN (${ph})`,
|
|
860
|
-
)
|
|
861
|
-
.all(...chunk) as Array<{ source_id: number; target_id: number }>;
|
|
862
|
-
for (const r of rows) seen.add(`${r.source_id}|${r.target_id}`);
|
|
863
1063
|
}
|
|
1064
|
+
// NOTE: Only files explicitly listed in changedFiles are re-parsed.
|
|
1065
|
+
// If a parent-class method is replaced (new node ID) but the child file is
|
|
1066
|
+
// unchanged, the stale super.method() edge is not refreshed here. A full
|
|
1067
|
+
// rebuild (isFullBuild=true) is required to recover in that scenario.
|
|
1068
|
+
return changedFiles.filter((f) => THIS_DISPATCH_EXTS.has(path.extname(f).toLowerCase()));
|
|
1069
|
+
}
|
|
864
1070
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
// failures fall back to the WASM backfill path so the sweep stays complete.
|
|
888
|
-
const absFiles = relFiles.map((f) => path.join(rootDir, f));
|
|
1071
|
+
/**
|
|
1072
|
+
* Re-parse files via native (preferred) + WASM fallback to obtain call sites
|
|
1073
|
+
* with receiver info. Returns a map of relPath → calls array.
|
|
1074
|
+
*
|
|
1075
|
+
* The native engine is preferred: this pass only runs after a native
|
|
1076
|
+
* orchestrator build, so the addon is already loaded and re-parses the
|
|
1077
|
+
* hierarchy file set in single-digit milliseconds with the same
|
|
1078
|
+
* receiver-annotated call sites as the WASM extractor. Booting the WASM
|
|
1079
|
+
* runtime here instead cost ~40–110ms per full build (in-process
|
|
1080
|
+
* web-tree-sitter + grammar init dominated) — part of the v3.12.0
|
|
1081
|
+
* publish-gate regression. Files the native engine cannot parse (extension
|
|
1082
|
+
* outside NATIVE_SUPPORTED_EXTENSIONS, e.g. .mts/.cts) and native parse
|
|
1083
|
+
* failures fall back to the WASM backfill path so the sweep stays complete.
|
|
1084
|
+
*/
|
|
1085
|
+
async function parseFilesForThisDispatch(
|
|
1086
|
+
absFiles: string[],
|
|
1087
|
+
rootDir: string,
|
|
1088
|
+
): Promise<{
|
|
1089
|
+
callsByRel: Map<string, { name: string; receiver?: string; line: number }[]>;
|
|
1090
|
+
wasmResults: Map<string, ExtractorOutput>;
|
|
1091
|
+
}> {
|
|
1092
|
+
const callsByRel = new Map<string, { name: string; receiver?: string; line: number }[]>();
|
|
889
1093
|
const nativeAbs = absFiles.filter((f) =>
|
|
890
1094
|
NATIVE_SUPPORTED_EXTENSIONS.has(path.extname(f).toLowerCase()),
|
|
891
1095
|
);
|
|
892
|
-
const callsByRel = new Map<string, { name: string; receiver?: string; line: number }[]>();
|
|
893
1096
|
// Track native-supported files that returned null (per-file parse error) so
|
|
894
1097
|
// they can be included in the WASM fallback set below, ensuring no file's
|
|
895
1098
|
// this/super call sites are silently discarded.
|
|
@@ -935,8 +1138,35 @@ async function runPostNativeThisDispatch(
|
|
|
935
1138
|
for (const [relPath, symbols] of wasmResults) {
|
|
936
1139
|
callsByRel.set(relPath, symbols.calls ?? []);
|
|
937
1140
|
}
|
|
1141
|
+
return { callsByRel, wasmResults };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/** Emit this/super dispatch edges from re-parsed call sites. */
|
|
1145
|
+
function emitThisDispatchEdges(
|
|
1146
|
+
db: BetterSqlite3Database,
|
|
1147
|
+
callsByRel: Map<string, { name: string; receiver?: string; line: number }[]>,
|
|
1148
|
+
chaCtx: ChaContext,
|
|
1149
|
+
lookup: CallNodeLookup,
|
|
1150
|
+
seen: Set<string>,
|
|
1151
|
+
): {
|
|
1152
|
+
newEdges: Array<[number, number, string, number, number, string]>;
|
|
1153
|
+
targetIds: Set<number>;
|
|
1154
|
+
affectedFiles: Set<string>;
|
|
1155
|
+
} {
|
|
1156
|
+
// Find the innermost containing method/function for a call at `line` in `file`.
|
|
1157
|
+
// COALESCE maps NULL end_line to a large sentinel so unbounded nodes sort last
|
|
1158
|
+
// (SQLite ASC orders NULLs first, so a raw `end_line - line` would pick them first).
|
|
1159
|
+
const findCallerByLineStmt = db.prepare(`
|
|
1160
|
+
SELECT id, name FROM nodes
|
|
1161
|
+
WHERE file = ? AND kind IN ('method', 'function')
|
|
1162
|
+
AND line <= ? AND (end_line IS NULL OR end_line >= ?)
|
|
1163
|
+
ORDER BY COALESCE(end_line - line, 999999999) ASC
|
|
1164
|
+
LIMIT 1
|
|
1165
|
+
`);
|
|
938
1166
|
|
|
939
1167
|
const newEdges: Array<[number, number, string, number, number, string]> = [];
|
|
1168
|
+
const targetIds = new Set<number>();
|
|
1169
|
+
const affectedFiles = new Set<string>();
|
|
940
1170
|
|
|
941
1171
|
for (const [relPath, calls] of callsByRel) {
|
|
942
1172
|
for (const call of calls) {
|
|
@@ -976,13 +1206,11 @@ async function runPostNativeThisDispatch(
|
|
|
976
1206
|
}
|
|
977
1207
|
}
|
|
978
1208
|
}
|
|
1209
|
+
return { newEdges, targetIds, affectedFiles };
|
|
1210
|
+
}
|
|
979
1211
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
debug(`this/super dispatch post-pass: inserted ${newEdges.length} edge(s)`);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// Free WASM parse trees — mirrors the cleanup in backfillNativeDroppedFiles
|
|
1212
|
+
/** Free WASM parse trees after this-dispatch post-pass to prevent memory leaks. */
|
|
1213
|
+
function cleanupThisDispatchWasmTrees(wasmResults: Map<string, ExtractorOutput>): void {
|
|
986
1214
|
for (const [, symbols] of wasmResults) {
|
|
987
1215
|
const tree = (symbols as { _tree?: { delete?: () => void } })._tree;
|
|
988
1216
|
if (tree && typeof tree.delete === 'function') {
|
|
@@ -995,6 +1223,109 @@ async function runPostNativeThisDispatch(
|
|
|
995
1223
|
(symbols as { _tree?: unknown; _langId?: unknown })._tree = undefined;
|
|
996
1224
|
(symbols as { _tree?: unknown; _langId?: unknown })._langId = undefined;
|
|
997
1225
|
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Phase 8.5: this/super dispatch post-pass for the native orchestrator path.
|
|
1230
|
+
*
|
|
1231
|
+
* The Rust build pipeline resolves typed receiver calls but does NOT persist raw
|
|
1232
|
+
* unresolved call site receiver info (e.g. `this`, `super`) to the DB. This
|
|
1233
|
+
* hybrid post-pass re-parses JS/TS/TSX files via WASM to collect call sites with
|
|
1234
|
+
* `this`/`super` receivers, then resolves them through the class hierarchy stored
|
|
1235
|
+
* in DB `extends` edges — mirroring what `buildChaPostPass` does on the WASM path.
|
|
1236
|
+
*
|
|
1237
|
+
* Also handles function-as-object-property methods (`f.h = function() { this.g() }`):
|
|
1238
|
+
* these use `this` to reference sibling properties on the same object (`f`), so
|
|
1239
|
+
* `resolveThisDispatch` resolves them by treating the dot-prefix of the caller name
|
|
1240
|
+
* (`f` from `f.h`) as the class and looking up `f.g` directly — no `extends` edge needed.
|
|
1241
|
+
*
|
|
1242
|
+
* Runs when either `extends` edges exist (class inheritance) OR dot-named `method`
|
|
1243
|
+
* nodes exist (func-prop assignments); skips only when neither is present.
|
|
1244
|
+
*/
|
|
1245
|
+
async function runPostNativeThisDispatch(
|
|
1246
|
+
db: BetterSqlite3Database,
|
|
1247
|
+
rootDir: string,
|
|
1248
|
+
changedFiles: string[] | undefined,
|
|
1249
|
+
isFullBuild: boolean,
|
|
1250
|
+
): Promise<{ elapsedMs: number; targetIds: Set<number>; affectedFiles: Set<string> }> {
|
|
1251
|
+
const t0 = performance.now();
|
|
1252
|
+
|
|
1253
|
+
// Fast guard: need at least one extends edge (class inheritance) OR a dot-named
|
|
1254
|
+
// method node (func-prop assignment: `f.h = function() { this.g() }`) for
|
|
1255
|
+
// this/super dispatch to produce any edges.
|
|
1256
|
+
const hasExtends = db.prepare(`SELECT 1 FROM edges WHERE kind = 'extends' LIMIT 1`).get();
|
|
1257
|
+
const hasFuncPropMethod = db
|
|
1258
|
+
.prepare(`SELECT 1 FROM nodes WHERE kind = 'method' AND INSTR(name, '.') > 0 LIMIT 1`)
|
|
1259
|
+
.get();
|
|
1260
|
+
const emptyResult = {
|
|
1261
|
+
elapsedMs: 0,
|
|
1262
|
+
targetIds: new Set<number>(),
|
|
1263
|
+
affectedFiles: new Set<string>(),
|
|
1264
|
+
};
|
|
1265
|
+
if (!hasExtends && !hasFuncPropMethod) return emptyResult;
|
|
1266
|
+
|
|
1267
|
+
const parents = buildThisDispatchParentsMap(db, hasExtends);
|
|
1268
|
+
// Note: parents may be empty when hasFuncPropMethod but !hasExtends — that is
|
|
1269
|
+
// intentional. resolveThisDispatch still resolves `this.g()` inside `f.h` by
|
|
1270
|
+
// treating `f` (the dot-prefix of callerName `f.h`) as the class and looking
|
|
1271
|
+
// up `f.g` directly via lookup.byName(), without traversing the parents chain.
|
|
1272
|
+
|
|
1273
|
+
const chaCtx: ChaContext = {
|
|
1274
|
+
implementors: new Map(), // not needed for this/super resolution
|
|
1275
|
+
parents,
|
|
1276
|
+
instantiatedTypes: new Set(), // not needed for this/super resolution
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
const relFiles = selectThisDispatchFiles(db, changedFiles, isFullBuild);
|
|
1280
|
+
if (relFiles.length === 0) return emptyResult;
|
|
1281
|
+
|
|
1282
|
+
// DB-backed CallNodeLookup — resolveThisDispatch only calls byName()
|
|
1283
|
+
const findByNameStmt = db.prepare(`SELECT id, file, kind FROM nodes WHERE name = ?`);
|
|
1284
|
+
const lookup: CallNodeLookup = {
|
|
1285
|
+
byName: (name) => findByNameStmt.all(name) as Array<{ id: number; file: string; kind: string }>,
|
|
1286
|
+
byNameAndFile: (name, file) =>
|
|
1287
|
+
(findByNameStmt.all(name) as Array<{ id: number; file: string; kind: string }>).filter(
|
|
1288
|
+
(n) => n.file === file,
|
|
1289
|
+
),
|
|
1290
|
+
isBarrel: () => false,
|
|
1291
|
+
resolveBarrel: () => null,
|
|
1292
|
+
nodeId: () => undefined,
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
// Seed seen-pairs from existing call edges on source nodes in our file set
|
|
1296
|
+
const seen = new Set<string>();
|
|
1297
|
+
const CHUNK = 500;
|
|
1298
|
+
for (let i = 0; i < relFiles.length; i += CHUNK) {
|
|
1299
|
+
const chunk = relFiles.slice(i, i + CHUNK);
|
|
1300
|
+
const ph = chunk.map(() => '?').join(',');
|
|
1301
|
+
const rows = db
|
|
1302
|
+
.prepare(
|
|
1303
|
+
`SELECT e.source_id, e.target_id
|
|
1304
|
+
FROM edges e
|
|
1305
|
+
JOIN nodes n ON e.source_id = n.id
|
|
1306
|
+
WHERE e.kind = 'calls' AND n.file IN (${ph})`,
|
|
1307
|
+
)
|
|
1308
|
+
.all(...chunk) as Array<{ source_id: number; target_id: number }>;
|
|
1309
|
+
for (const r of rows) seen.add(`${r.source_id}|${r.target_id}`);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const absFiles = relFiles.map((f) => path.join(rootDir, f));
|
|
1313
|
+
const { callsByRel, wasmResults } = await parseFilesForThisDispatch(absFiles, rootDir);
|
|
1314
|
+
|
|
1315
|
+
const { newEdges, targetIds, affectedFiles } = emitThisDispatchEdges(
|
|
1316
|
+
db,
|
|
1317
|
+
callsByRel,
|
|
1318
|
+
chaCtx,
|
|
1319
|
+
lookup,
|
|
1320
|
+
seen,
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
if (newEdges.length > 0) {
|
|
1324
|
+
db.transaction(() => batchInsertEdges(db, newEdges))();
|
|
1325
|
+
debug(`this/super dispatch post-pass: inserted ${newEdges.length} edge(s)`);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
cleanupThisDispatchWasmTrees(wasmResults);
|
|
998
1329
|
|
|
999
1330
|
return { elapsedMs: performance.now() - t0, targetIds, affectedFiles };
|
|
1000
1331
|
}
|
|
@@ -1110,6 +1441,55 @@ function groupByExtension(relPaths: Iterable<string>): Map<string, string[]> {
|
|
|
1110
1441
|
return buckets;
|
|
1111
1442
|
}
|
|
1112
1443
|
|
|
1444
|
+
/**
|
|
1445
|
+
* Return the subset of relative paths that are gitignored in `rootDir`.
|
|
1446
|
+
*
|
|
1447
|
+
* Runs `git check-ignore --stdin` with all candidate paths piped in. Any
|
|
1448
|
+
* path that git echoes back is gitignored. Fails silently (returns an empty
|
|
1449
|
+
* set) when git is unavailable, the directory is not a git repo, or the
|
|
1450
|
+
* check-ignore call throws — the gap-detection logic handles those cases
|
|
1451
|
+
* gracefully without this filter.
|
|
1452
|
+
*
|
|
1453
|
+
* Uses relative paths (forward-slash separated) as both input and output so
|
|
1454
|
+
* the result set can be matched directly against the `expected` set in
|
|
1455
|
+
* `detectDroppedLanguageGap` without any further path manipulation.
|
|
1456
|
+
*/
|
|
1457
|
+
function queryGitIgnoredFiles(rootDir: string, relPaths: Iterable<string>): Set<string> {
|
|
1458
|
+
const ignored = new Set<string>();
|
|
1459
|
+
const paths = [...relPaths];
|
|
1460
|
+
if (paths.length === 0) return ignored;
|
|
1461
|
+
try {
|
|
1462
|
+
const stdin = paths.join('\n');
|
|
1463
|
+
const output = execFileSync('git', ['check-ignore', '--stdin'], {
|
|
1464
|
+
cwd: rootDir,
|
|
1465
|
+
input: stdin,
|
|
1466
|
+
encoding: 'utf-8',
|
|
1467
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
1468
|
+
// git check-ignore exits with 1 when none of the paths are ignored —
|
|
1469
|
+
// that is not an error for our purposes. stdio: 'pipe' lets us capture
|
|
1470
|
+
// stdout without swallowing stderr, and the try/catch handles the
|
|
1471
|
+
// non-zero exit from execFileSync when ALL paths are non-ignored
|
|
1472
|
+
// (exit code 1 from git check-ignore means "no matches").
|
|
1473
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1474
|
+
});
|
|
1475
|
+
for (const line of output.split('\n')) {
|
|
1476
|
+
const trimmed = normalizePath(line.trim());
|
|
1477
|
+
if (trimmed) ignored.add(trimmed);
|
|
1478
|
+
}
|
|
1479
|
+
} catch (e: unknown) {
|
|
1480
|
+
// Exit code 1 means no paths were ignored — not an error. Any other
|
|
1481
|
+
// failure (git unavailable, not a repo, etc.) is silently swallowed
|
|
1482
|
+
// so the caller proceeds with the unfiltered set.
|
|
1483
|
+
const exitCode = (e as { status?: number }).status;
|
|
1484
|
+
if (exitCode !== 1) {
|
|
1485
|
+
debug(`queryGitIgnoredFiles: git check-ignore failed: ${toErrorMessage(e)}`);
|
|
1486
|
+
}
|
|
1487
|
+
// On exit code 1, output is empty so ignored stays empty — correct.
|
|
1488
|
+
// On other errors we also proceed with the empty set (safe degradation).
|
|
1489
|
+
}
|
|
1490
|
+
return ignored;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1113
1493
|
/**
|
|
1114
1494
|
* Detect files the native orchestrator silently dropped.
|
|
1115
1495
|
*
|
|
@@ -1139,8 +1519,17 @@ function groupByExtension(relPaths: Iterable<string>): Map<string, string[]> {
|
|
|
1139
1519
|
*/
|
|
1140
1520
|
function detectDroppedLanguageGap(ctx: PipelineContext): DroppedLanguageGap {
|
|
1141
1521
|
const collected = collectFilesUtil(ctx.rootDir, [], ctx.config, new Set<string>());
|
|
1522
|
+
const expectedRaw = collected.files.map((f) => normalizePath(path.relative(ctx.rootDir, f)));
|
|
1523
|
+
|
|
1524
|
+
// The native Rust engine uses the `ignore` crate with git_ignore(true), so it
|
|
1525
|
+
// respects .gitignore and never processes gitignored files. The JS collectFiles
|
|
1526
|
+
// walker has no gitignore awareness, so without this filter gitignored files
|
|
1527
|
+
// (e.g. NAPI-RS generated crates/codegraph-core/index.js / index.d.ts) appear
|
|
1528
|
+
// in `expected` but not in the DB, causing a spurious "native extractor bug"
|
|
1529
|
+
// WARN and triggering an unnecessary WASM backfill (#1626).
|
|
1530
|
+
const gitIgnored = queryGitIgnoredFiles(ctx.rootDir, expectedRaw);
|
|
1142
1531
|
const expected = new Set(
|
|
1143
|
-
|
|
1532
|
+
gitIgnored.size > 0 ? expectedRaw.filter((r) => !gitIgnored.has(r)) : expectedRaw,
|
|
1144
1533
|
);
|
|
1145
1534
|
|
|
1146
1535
|
const existingNodeRows = ctx.db
|
|
@@ -1184,96 +1573,41 @@ function detectDroppedLanguageGap(ctx: PipelineContext): DroppedLanguageGap {
|
|
|
1184
1573
|
return { missingRel, missingAbs, staleRel };
|
|
1185
1574
|
}
|
|
1186
1575
|
|
|
1187
|
-
|
|
1188
|
-
* Backfill files that the native orchestrator silently dropped during parse.
|
|
1189
|
-
* Falls back to WASM + inserts file/symbol nodes so engine counts match (#967).
|
|
1190
|
-
*
|
|
1191
|
-
* Also purges stale rows for WASM-only files deleted from disk (#1073), which
|
|
1192
|
-
* Rust's `detect_removed_files` filter (#1070) skips.
|
|
1193
|
-
*
|
|
1194
|
-
* Accepts a pre-computed `gap` from `detectDroppedLanguageGap` so the caller
|
|
1195
|
-
* can use the same scan for both gating and the actual backfill — avoiding
|
|
1196
|
-
* a redundant fs walk when the orchestrator's signals already triggered.
|
|
1197
|
-
*/
|
|
1198
|
-
async function backfillNativeDroppedFiles(
|
|
1199
|
-
ctx: PipelineContext,
|
|
1200
|
-
gap: DroppedLanguageGap,
|
|
1201
|
-
): Promise<void> {
|
|
1202
|
-
const { missingRel, missingAbs, staleRel } = gap;
|
|
1203
|
-
if (missingAbs.length === 0 && staleRel.length === 0) return;
|
|
1204
|
-
|
|
1205
|
-
// Now that we know there's work to do, hand off to better-sqlite3 (needed
|
|
1206
|
-
// for the INSERT path below).
|
|
1207
|
-
if (ctx.nativeFirstProxy) {
|
|
1208
|
-
closeNativeDb(ctx, 'pre-parity-backfill');
|
|
1209
|
-
ctx.db = openDb(ctx.dbPath);
|
|
1210
|
-
ctx.nativeFirstProxy = false;
|
|
1211
|
-
}
|
|
1576
|
+
// ── backfillNativeDroppedFiles helpers ───────────────────────────────────────
|
|
1212
1577
|
|
|
1213
|
-
|
|
1578
|
+
/** Purge stale WASM-only files deleted from disk (#1073). */
|
|
1579
|
+
function purgeStaleWasmOnlyFiles(db: BetterSqlite3Database, staleRel: string[]): void {
|
|
1580
|
+
// `computeWasmOnlyStaleFiles` guarantees every path here has an extension
|
|
1581
|
+
// outside NATIVE_SUPPORTED_EXTENSIONS, so `classifyNativeDrops` would
|
|
1582
|
+
// always bucket 100% into `unsupported-by-native`. Build the extension
|
|
1583
|
+
// summary directly to avoid a redundant classification pass.
|
|
1584
|
+
const staleByExt = groupByExtension(staleRel);
|
|
1585
|
+
info(
|
|
1586
|
+
`Detected ${staleRel.length} deleted WASM-only file(s) across ${staleByExt.size} extension(s) the native orchestrator skipped; purging stale rows:${formatDropExtensionSummary(staleByExt)}`,
|
|
1587
|
+
);
|
|
1588
|
+
purgeFilesData(db, staleRel);
|
|
1589
|
+
}
|
|
1214
1590
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1591
|
+
/**
|
|
1592
|
+
* Classify and log dropped file buckets.
|
|
1593
|
+
* Three-way split of native-extractor-failure files:
|
|
1594
|
+
* realFailureBuckets — WASM found symbols → real Rust extractor bug (WARN)
|
|
1595
|
+
* emptyFileBuckets — WASM parsed but found 0 symbols → gitignored/empty (debug)
|
|
1596
|
+
* wasmSkipBuckets — WASM skipped entirely → no file-node insert (debug)
|
|
1597
|
+
*/
|
|
1598
|
+
function classifyAndLogDroppedFiles(
|
|
1599
|
+
missingRel: string[],
|
|
1600
|
+
wasmParsedFiles: Set<string>,
|
|
1601
|
+
wasmFoundSymbols: Set<string>,
|
|
1602
|
+
): void {
|
|
1603
|
+
const { byReason, totals } = classifyNativeDrops(missingRel);
|
|
1604
|
+
if (totals['unsupported-by-native'] > 0) {
|
|
1605
|
+
const buckets = byReason['unsupported-by-native'];
|
|
1225
1606
|
info(
|
|
1226
|
-
`
|
|
1227
|
-
);
|
|
1228
|
-
purgeFilesData(dbConn, staleRel);
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
if (missingAbs.length === 0) return;
|
|
1232
|
-
|
|
1233
|
-
// Parse all missing files via WASM first so we can distinguish real native
|
|
1234
|
-
// extractor failures (WASM finds symbols but native didn't) from files the
|
|
1235
|
-
// Rust engine legitimately skipped (gitignored artifacts, empty declaration
|
|
1236
|
-
// files, etc. where WASM also produces 0 symbols). Both categories are
|
|
1237
|
-
// backfilled — only the former triggers a WARN (#1566).
|
|
1238
|
-
const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
|
|
1239
|
-
|
|
1240
|
-
// Build two sets from wasmResults:
|
|
1241
|
-
// wasmParsedFiles — rel-paths present in wasmResults (WASM succeeded, even 0 symbols)
|
|
1242
|
-
// wasmFoundSymbols — subset where WASM found ≥1 symbol
|
|
1243
|
-
// Files absent from wasmParsedFiles were skipped by WASM entirely (extension
|
|
1244
|
-
// not in _extToLang, wasmExtractSymbols returned null, or a read error).
|
|
1245
|
-
// Those files do NOT end up in the batchInsertNodes loop below.
|
|
1246
|
-
const wasmParsedFiles = new Set<string>();
|
|
1247
|
-
const wasmFoundSymbols = new Set<string>();
|
|
1248
|
-
for (const [relPath, symbols] of wasmResults) {
|
|
1249
|
-
wasmParsedFiles.add(relPath);
|
|
1250
|
-
if ((symbols.definitions?.length ?? 0) > 0 || (symbols.exports?.length ?? 0) > 0) {
|
|
1251
|
-
wasmFoundSymbols.add(relPath);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
// Classify drops so users see per-extension reasons instead of just a count
|
|
1256
|
-
// (#1011). `unsupported-by-native` is a legitimate parser limit (no Rust
|
|
1257
|
-
// extractor); `native-extractor-failure` indicates a real native bug since
|
|
1258
|
-
// the language IS supported by the addon yet WASM found symbols the native
|
|
1259
|
-
// engine should have extracted. Files where both engines produce 0 symbols
|
|
1260
|
-
// are legitimately empty (e.g. gitignored napi-generated declaration stubs)
|
|
1261
|
-
// and logged at debug level only.
|
|
1262
|
-
const { byReason, totals } = classifyNativeDrops(missingRel);
|
|
1263
|
-
if (totals['unsupported-by-native'] > 0) {
|
|
1264
|
-
const buckets = byReason['unsupported-by-native'];
|
|
1265
|
-
info(
|
|
1266
|
-
`Native orchestrator skipped ${totals['unsupported-by-native']} file(s) across ${buckets.size} extension(s) in languages without a Rust extractor; backfilling via WASM:${formatDropExtensionSummary(buckets)}`,
|
|
1607
|
+
`Native orchestrator skipped ${totals['unsupported-by-native']} file(s) across ${buckets.size} extension(s) in languages without a Rust extractor; backfilling via WASM:${formatDropExtensionSummary(buckets)}`,
|
|
1267
1608
|
);
|
|
1268
1609
|
}
|
|
1269
1610
|
if (totals['native-extractor-failure'] > 0) {
|
|
1270
|
-
// Three-way split of native-extractor-failure files:
|
|
1271
|
-
// realFailureBuckets — WASM found symbols → real Rust extractor bug (WARN)
|
|
1272
|
-
// emptyFileBuckets — WASM parsed but found 0 symbols → gitignored/empty (debug)
|
|
1273
|
-
// These DO receive a file-node insert in the loop below.
|
|
1274
|
-
// wasmSkipBuckets — WASM skipped entirely (ext unknown or parse error) →
|
|
1275
|
-
// no file-node insert, and no WARN (debug only, distinct
|
|
1276
|
-
// message to avoid overstating backfill coverage).
|
|
1277
1611
|
const allFailurePaths = byReason['native-extractor-failure'];
|
|
1278
1612
|
const realFailureBuckets = new Map<string, string[]>();
|
|
1279
1613
|
const emptyFileBuckets = new Map<string, string[]>();
|
|
@@ -1315,7 +1649,13 @@ async function backfillNativeDroppedFiles(
|
|
|
1315
1649
|
);
|
|
1316
1650
|
}
|
|
1317
1651
|
}
|
|
1652
|
+
}
|
|
1318
1653
|
|
|
1654
|
+
/** Insert node rows for all backfilled files and mark exported symbols. */
|
|
1655
|
+
function insertBackfilledNodes(
|
|
1656
|
+
db: BetterSqlite3Database,
|
|
1657
|
+
wasmResults: Map<string, ExtractorOutput>,
|
|
1658
|
+
): void {
|
|
1319
1659
|
const rows: unknown[][] = [];
|
|
1320
1660
|
const exportKeys: unknown[][] = [];
|
|
1321
1661
|
for (const [relPath, symbols] of wasmResults) {
|
|
@@ -1347,7 +1687,6 @@ async function backfillNativeDroppedFiles(
|
|
|
1347
1687
|
exportKeys.push([exp.name, exp.kind, relPath, exp.line]);
|
|
1348
1688
|
}
|
|
1349
1689
|
}
|
|
1350
|
-
const db = dbConn;
|
|
1351
1690
|
batchInsertNodes(db, rows);
|
|
1352
1691
|
|
|
1353
1692
|
// Mark exported symbols in batches — mirrors insertDefinitionsAndExports.
|
|
@@ -1374,18 +1713,26 @@ async function backfillNativeDroppedFiles(
|
|
|
1374
1713
|
updateStmt.run(...vals);
|
|
1375
1714
|
}
|
|
1376
1715
|
}
|
|
1716
|
+
}
|
|
1377
1717
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1718
|
+
/**
|
|
1719
|
+
* Persist file_hashes rows for every backfilled file.
|
|
1720
|
+
*
|
|
1721
|
+
* The Rust orchestrator only hashes files it parsed itself, so without this
|
|
1722
|
+
* step files in optional-language extensions (e.g. .clj when no Rust extractor
|
|
1723
|
+
* exists) would be missing from `file_hashes` — permanently breaking the JS-side
|
|
1724
|
+
* fast-skip pre-flight (#1054), which rejects on `collected file missing
|
|
1725
|
+
* from file_hashes` and forces every no-op rebuild back through the full
|
|
1726
|
+
* ~2s native pipeline (#1068).
|
|
1727
|
+
*
|
|
1728
|
+
* Iterates `missingRel` (every collected file the Rust orchestrator dropped),
|
|
1729
|
+
* not `wasmResults`, so files that produced zero symbols still get a row.
|
|
1730
|
+
*/
|
|
1731
|
+
function backfillFileHashes(
|
|
1732
|
+
db: BetterSqlite3Database,
|
|
1733
|
+
missingRel: string[],
|
|
1734
|
+
missingAbs: string[],
|
|
1735
|
+
): void {
|
|
1389
1736
|
try {
|
|
1390
1737
|
const upsertHash = db.prepare(
|
|
1391
1738
|
'INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)',
|
|
@@ -1415,6 +1762,79 @@ async function backfillNativeDroppedFiles(
|
|
|
1415
1762
|
`backfillNativeDroppedFiles: file_hashes write failed (table may not exist): ${toErrorMessage(e)}`,
|
|
1416
1763
|
);
|
|
1417
1764
|
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Backfill files that the native orchestrator silently dropped during parse.
|
|
1769
|
+
* Falls back to WASM + inserts file/symbol nodes so engine counts match (#967).
|
|
1770
|
+
*
|
|
1771
|
+
* Also purges stale rows for WASM-only files deleted from disk (#1073), which
|
|
1772
|
+
* Rust's `detect_removed_files` filter (#1070) skips.
|
|
1773
|
+
*
|
|
1774
|
+
* Accepts a pre-computed `gap` from `detectDroppedLanguageGap` so the caller
|
|
1775
|
+
* can use the same scan for both gating and the actual backfill — avoiding
|
|
1776
|
+
* a redundant fs walk when the orchestrator's signals already triggered.
|
|
1777
|
+
*/
|
|
1778
|
+
async function backfillNativeDroppedFiles(
|
|
1779
|
+
ctx: PipelineContext,
|
|
1780
|
+
gap: DroppedLanguageGap,
|
|
1781
|
+
): Promise<void> {
|
|
1782
|
+
const { missingRel, missingAbs, staleRel } = gap;
|
|
1783
|
+
if (missingAbs.length === 0 && staleRel.length === 0) return;
|
|
1784
|
+
|
|
1785
|
+
// Now that we know there's work to do, hand off to better-sqlite3 (needed
|
|
1786
|
+
// for the INSERT path below).
|
|
1787
|
+
if (ctx.nativeFirstProxy) {
|
|
1788
|
+
closeNativeDb(ctx, 'pre-parity-backfill');
|
|
1789
|
+
ctx.db = openDb(ctx.dbPath);
|
|
1790
|
+
ctx.nativeFirstProxy = false;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const dbConn = ctx.db as unknown as BetterSqlite3Database;
|
|
1794
|
+
|
|
1795
|
+
// Purge WASM-only files that were deleted from disk (#1073). Rust's
|
|
1796
|
+
// detect_removed_files skips them and the insert path below never visits
|
|
1797
|
+
// them, so without this their rows would persist across rebuilds until the
|
|
1798
|
+
// next full rebuild reset the DB.
|
|
1799
|
+
if (staleRel.length > 0) {
|
|
1800
|
+
purgeStaleWasmOnlyFiles(dbConn, staleRel);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
if (missingAbs.length === 0) return;
|
|
1804
|
+
|
|
1805
|
+
// Parse all missing files via WASM first so we can distinguish real native
|
|
1806
|
+
// extractor failures (WASM finds symbols but native didn't) from files the
|
|
1807
|
+
// Rust engine legitimately skipped (gitignored artifacts, empty declaration
|
|
1808
|
+
// files, etc. where WASM also produces 0 symbols). Both categories are
|
|
1809
|
+
// backfilled — only the former triggers a WARN (#1566).
|
|
1810
|
+
const wasmResults = await parseFilesWasmForBackfill(missingAbs, ctx.rootDir);
|
|
1811
|
+
|
|
1812
|
+
// Build two sets from wasmResults:
|
|
1813
|
+
// wasmParsedFiles — rel-paths present in wasmResults (WASM succeeded, even 0 symbols)
|
|
1814
|
+
// wasmFoundSymbols — subset where WASM found ≥1 symbol
|
|
1815
|
+
// Files absent from wasmParsedFiles were skipped by WASM entirely (extension
|
|
1816
|
+
// not in _extToLang, wasmExtractSymbols returned null, or a read error).
|
|
1817
|
+
// Those files do NOT end up in the batchInsertNodes loop below.
|
|
1818
|
+
const wasmParsedFiles = new Set<string>();
|
|
1819
|
+
const wasmFoundSymbols = new Set<string>();
|
|
1820
|
+
for (const [relPath, symbols] of wasmResults) {
|
|
1821
|
+
wasmParsedFiles.add(relPath);
|
|
1822
|
+
if ((symbols.definitions?.length ?? 0) > 0 || (symbols.exports?.length ?? 0) > 0) {
|
|
1823
|
+
wasmFoundSymbols.add(relPath);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// Classify drops so users see per-extension reasons instead of just a count
|
|
1828
|
+
// (#1011). `unsupported-by-native` is a legitimate parser limit (no Rust
|
|
1829
|
+
// extractor); `native-extractor-failure` indicates a real native bug since
|
|
1830
|
+
// the language IS supported by the addon yet WASM found symbols the native
|
|
1831
|
+
// engine should have extracted. Files where both engines produce 0 symbols
|
|
1832
|
+
// are legitimately empty (e.g. gitignored napi-generated declaration stubs)
|
|
1833
|
+
// and logged at debug level only.
|
|
1834
|
+
classifyAndLogDroppedFiles(missingRel, wasmParsedFiles, wasmFoundSymbols);
|
|
1835
|
+
|
|
1836
|
+
insertBackfilledNodes(dbConn, wasmResults);
|
|
1837
|
+
backfillFileHashes(dbConn, missingRel, missingAbs);
|
|
1418
1838
|
|
|
1419
1839
|
// Free WASM parse trees from the inline backfill path (#1058).
|
|
1420
1840
|
// `parseFilesWasmInline` sets `symbols._tree` (a live web-tree-sitter Tree
|
|
@@ -1424,23 +1844,15 @@ async function backfillNativeDroppedFiles(
|
|
|
1424
1844
|
// sees them. Without this, trees leak WASM memory until process exit —
|
|
1425
1845
|
// bounded per run but cumulative across in-process integration tests.
|
|
1426
1846
|
// Mirrors the cleanup discipline established for #931.
|
|
1427
|
-
|
|
1428
|
-
const tree = (symbols as { _tree?: { delete?: () => void } })._tree;
|
|
1429
|
-
if (tree && typeof tree.delete === 'function') {
|
|
1430
|
-
try {
|
|
1431
|
-
tree.delete();
|
|
1432
|
-
} catch {
|
|
1433
|
-
/* ignore cleanup errors */
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
(symbols as { _tree?: unknown; _langId?: unknown })._tree = undefined;
|
|
1437
|
-
(symbols as { _tree?: unknown; _langId?: unknown })._langId = undefined;
|
|
1438
|
-
}
|
|
1847
|
+
cleanupThisDispatchWasmTrees(wasmResults);
|
|
1439
1848
|
}
|
|
1440
1849
|
|
|
1441
1850
|
/**
|
|
1442
1851
|
* Backfill the `technique` column on `calls` edges written by the native Rust
|
|
1443
|
-
* orchestrator, which does not write the column itself.
|
|
1852
|
+
* orchestrator, which does not write the column itself. Also lifts any
|
|
1853
|
+
* resolved ts-native edge whose confidence is below TS_NATIVE_CONFIDENCE_FLOOR
|
|
1854
|
+
* to that floor value so that the name-lookup quality of the native resolver is
|
|
1855
|
+
* reflected in the call-confidence metric.
|
|
1444
1856
|
*
|
|
1445
1857
|
* For full builds, all `calls` edges in the DB are new so a global UPDATE is
|
|
1446
1858
|
* correct. For incremental builds, only changed-file source nodes are updated
|
|
@@ -1461,6 +1873,12 @@ function backfillEdgeTechniquesAfterNativeOrchestrator(
|
|
|
1461
1873
|
db.prepare(
|
|
1462
1874
|
"UPDATE edges SET technique = 'ts-native' WHERE kind = 'calls' AND technique IS NULL",
|
|
1463
1875
|
).run();
|
|
1876
|
+
// Lift resolved ts-native edges below the confidence floor.
|
|
1877
|
+
db.prepare(
|
|
1878
|
+
`UPDATE edges SET confidence = ?
|
|
1879
|
+
WHERE kind = 'calls' AND technique = 'ts-native'
|
|
1880
|
+
AND confidence > 0 AND confidence < ?`,
|
|
1881
|
+
).run(TS_NATIVE_CONFIDENCE_FLOOR, TS_NATIVE_CONFIDENCE_FLOOR);
|
|
1464
1882
|
return;
|
|
1465
1883
|
}
|
|
1466
1884
|
// Incremental: scope to source nodes whose file is one of the changed files.
|
|
@@ -1477,11 +1895,235 @@ function backfillEdgeTechniquesAfterNativeOrchestrator(
|
|
|
1477
1895
|
SELECT id FROM nodes WHERE file IN (${placeholders})
|
|
1478
1896
|
)`,
|
|
1479
1897
|
).run(...chunk);
|
|
1898
|
+
// Lift resolved ts-native edges below the confidence floor for this chunk.
|
|
1899
|
+
db.prepare(
|
|
1900
|
+
`UPDATE edges SET confidence = ?
|
|
1901
|
+
WHERE kind = 'calls' AND technique = 'ts-native'
|
|
1902
|
+
AND confidence > 0 AND confidence < ?
|
|
1903
|
+
AND source_id IN (
|
|
1904
|
+
SELECT id FROM nodes WHERE file IN (${placeholders})
|
|
1905
|
+
)`,
|
|
1906
|
+
).run(TS_NATIVE_CONFIDENCE_FLOOR, TS_NATIVE_CONFIDENCE_FLOOR, ...chunk);
|
|
1480
1907
|
}
|
|
1481
1908
|
});
|
|
1482
1909
|
tx();
|
|
1483
1910
|
}
|
|
1484
1911
|
|
|
1912
|
+
// ── tryNativeOrchestrator helpers ────────────────────────────────────────────
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Open NativeDatabase on demand — deferred from setupPipeline to skip the
|
|
1916
|
+
* ~60ms cost on no-op/early-exit builds.
|
|
1917
|
+
*
|
|
1918
|
+
* Closes the better-sqlite3 connection first to avoid dual-connection WAL
|
|
1919
|
+
* corruption. On setup failure, falls back to reopening better-sqlite3 and
|
|
1920
|
+
* leaves ctx.nativeDb undefined so the caller falls through to the JS pipeline.
|
|
1921
|
+
*/
|
|
1922
|
+
function openNativeDatabase(ctx: PipelineContext): void {
|
|
1923
|
+
if (ctx.nativeDb || !ctx.nativeAvailable) return;
|
|
1924
|
+
const native = loadNative();
|
|
1925
|
+
if (!native?.NativeDatabase) return;
|
|
1926
|
+
try {
|
|
1927
|
+
// Close better-sqlite3 before opening rusqlite to avoid WAL conflicts.
|
|
1928
|
+
// Uses raw close() instead of closeDb() intentionally — the advisory lock
|
|
1929
|
+
// is kept and transferred to the NativeDbProxy below, not released here.
|
|
1930
|
+
ctx.db.close();
|
|
1931
|
+
acquireAdvisoryLock(ctx.dbPath);
|
|
1932
|
+
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
1933
|
+
ctx.nativeDb.initSchema();
|
|
1934
|
+
// Replace ctx.db with a NativeDbProxy so post-native JS fallback
|
|
1935
|
+
// (structure, analysis) can use it without reopening better-sqlite3.
|
|
1936
|
+
const proxy = new NativeDbProxy(ctx.nativeDb);
|
|
1937
|
+
proxy.__lockPath = `${ctx.dbPath}.lock`;
|
|
1938
|
+
ctx.db = proxy as unknown as typeof ctx.db;
|
|
1939
|
+
ctx.nativeFirstProxy = true;
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
|
|
1942
|
+
try {
|
|
1943
|
+
ctx.nativeDb?.close();
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
debug(`tryNativeOrchestrator: close failed during fallback: ${toErrorMessage(e)}`);
|
|
1946
|
+
}
|
|
1947
|
+
ctx.nativeDb = undefined;
|
|
1948
|
+
ctx.nativeFirstProxy = false; // defensive: reset in case future refactors move the assignment above throwing lines
|
|
1949
|
+
releaseAdvisoryLock(`${ctx.dbPath}.lock`);
|
|
1950
|
+
// Reopen better-sqlite3 for JS pipeline fallback
|
|
1951
|
+
ctx.db = openDb(ctx.dbPath);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* Coordinate all post-native edge-writing post-passes, role re-classification,
|
|
1957
|
+
* and technique backfill. Returns timing data for the build result.
|
|
1958
|
+
*
|
|
1959
|
+
* Post-passes run before structure/analysis so role classification sees the
|
|
1960
|
+
* complete graph including CHA + this/super dispatch edges.
|
|
1961
|
+
*/
|
|
1962
|
+
async function runPostNativePasses(
|
|
1963
|
+
ctx: PipelineContext,
|
|
1964
|
+
result: NativeOrchestratorResult,
|
|
1965
|
+
): Promise<PostPassTimings & { backfillHappened: boolean }> {
|
|
1966
|
+
// Engine parity: the native orchestrator silently drops files whose
|
|
1967
|
+
// Rust extractor/grammar is missing or fails (e.g. HCL, Scala, Swift on
|
|
1968
|
+
// stale native binaries). WASM handles those — backfill via WASM so both
|
|
1969
|
+
// engines process the same file set (#967).
|
|
1970
|
+
//
|
|
1971
|
+
// Detect the gap once (fs walk + 2 DB queries) and use it for both gating
|
|
1972
|
+
// and the backfill itself. On quiet incrementals we still pay the walk so
|
|
1973
|
+
// we can detect brand-new files in dropped-language extensions — a gap that
|
|
1974
|
+
// the orchestrator's `detect_removed_files` filter (#1070) leaves open
|
|
1975
|
+
// (#1083, #1091). The pre-check is cheap because the expensive part (WASM
|
|
1976
|
+
// re-parse of the missing set) is gated below.
|
|
1977
|
+
const gapDetectStart = performance.now();
|
|
1978
|
+
const gap = detectDroppedLanguageGap(ctx);
|
|
1979
|
+
const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;
|
|
1980
|
+
if (backfillHappened) {
|
|
1981
|
+
await backfillNativeDroppedFiles(ctx, gap);
|
|
1982
|
+
}
|
|
1983
|
+
const gapDetectMs = performance.now() - gapDetectStart;
|
|
1984
|
+
|
|
1985
|
+
// Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
|
|
1986
|
+
// whose raw receiver info the Rust pipeline does not persist to DB.
|
|
1987
|
+
// Runs BEFORE the CHA expansion pass so that super.method() → Parent.method edges
|
|
1988
|
+
// (technique='cha') are in the DB when runPostNativeCha expands them to sibling
|
|
1989
|
+
// class overrides (e.g. PostMixin.m → B.m when PostMixin and B both extend A).
|
|
1990
|
+
const {
|
|
1991
|
+
elapsedMs: thisDispatchMs,
|
|
1992
|
+
targetIds: thisDispatchTargetIds,
|
|
1993
|
+
affectedFiles: thisDispatchAffectedFiles,
|
|
1994
|
+
} = await runPostNativeThisDispatch(
|
|
1995
|
+
ctx.db as unknown as BetterSqlite3Database,
|
|
1996
|
+
ctx.rootDir,
|
|
1997
|
+
result.changedFiles,
|
|
1998
|
+
!!result.isFullBuild,
|
|
1999
|
+
);
|
|
2000
|
+
|
|
2001
|
+
// Phase 8.6: expand CHA call edges (interface dispatch → concrete implementations).
|
|
2002
|
+
// Returns the affected files so role re-classification below can be scoped to
|
|
2003
|
+
// the nodes whose fan-in/out actually changed.
|
|
2004
|
+
//
|
|
2005
|
+
// Runs AFTER this/super dispatch so super.method() edges are already in the DB.
|
|
2006
|
+
// The 'cha-expanded' technique tag on this pass's own output prevents re-expansion
|
|
2007
|
+
// of those edges in subsequent incremental builds, while 'cha'-tagged edges from
|
|
2008
|
+
// this/super dispatch remain eligible for expansion here.
|
|
2009
|
+
//
|
|
2010
|
+
// Function-as-object-property methods (`fn.method = function() {}`) are extracted
|
|
2011
|
+
// natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
|
|
2012
|
+
// no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
|
|
2013
|
+
const chaStart = performance.now();
|
|
2014
|
+
const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(
|
|
2015
|
+
ctx.db as unknown as BetterSqlite3Database,
|
|
2016
|
+
// null = full build (scan all call→method edges); array = incremental (gate queries decide scope)
|
|
2017
|
+
result.isFullBuild ? null : (result.changedFiles ?? null),
|
|
2018
|
+
);
|
|
2019
|
+
const chaMs = performance.now() - chaStart;
|
|
2020
|
+
|
|
2021
|
+
// Role re-classification after the Rust orchestrator build.
|
|
2022
|
+
//
|
|
2023
|
+
// Two reasons to re-classify:
|
|
2024
|
+
//
|
|
2025
|
+
// 1. Post-pass edges (CHA, this-dispatch): the Rust orchestrator classifies
|
|
2026
|
+
// roles before these passes add edges, so fan-in/out for their endpoints
|
|
2027
|
+
// is stale. On incremental builds, scope to the affected files for speed.
|
|
2028
|
+
//
|
|
2029
|
+
// 2. hasActiveFileSiblings parity: the Rust classifier does not implement the
|
|
2030
|
+
// JS hasActiveFileSiblings heuristic. That heuristic promotes functions with
|
|
2031
|
+
// fan_in=0 but fan_out>0 to 'leaf' when their file has other connected
|
|
2032
|
+
// callables — preventing false dead-unresolved classifications for functions
|
|
2033
|
+
// like `main` or `square` that call others but are never called themselves.
|
|
2034
|
+
// On full builds, always run a full JS re-classification so the Rust roles
|
|
2035
|
+
// are replaced by the canonical JS classifier output (#1659).
|
|
2036
|
+
//
|
|
2037
|
+
// Strategy:
|
|
2038
|
+
// - Full build: always run full JS classifyNodeRoles(db, null).
|
|
2039
|
+
// - Incremental build with post-pass edges: run scoped re-classification
|
|
2040
|
+
// for the affected files (same as before). The full-build pass already
|
|
2041
|
+
// produced correct JS roles for all unchanged files on the previous build.
|
|
2042
|
+
// - Incremental build with no post-pass edges: skip re-classification
|
|
2043
|
+
// (Rust roles on unchanged files are not stale, and the heuristic gap
|
|
2044
|
+
// was corrected on the last full build).
|
|
2045
|
+
let reclassifyMs = 0;
|
|
2046
|
+
const needsFullReclassify = !!result.isFullBuild;
|
|
2047
|
+
const needsScopedReclassify =
|
|
2048
|
+
!needsFullReclassify && (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0);
|
|
2049
|
+
if (needsFullReclassify || needsScopedReclassify) {
|
|
2050
|
+
let scopedFiles: string[] | null = null;
|
|
2051
|
+
if (needsScopedReclassify) {
|
|
2052
|
+
const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
|
|
2053
|
+
// When edges were inserted but all their endpoint nodes have null `file`
|
|
2054
|
+
// columns (rare but possible), affectedFiles stays empty even though
|
|
2055
|
+
// fan-in/out changed. Fall back to full-graph re-classification in that
|
|
2056
|
+
// case — scoped classification with an empty set would be a no-op, leaving
|
|
2057
|
+
// roles stale for those nodes.
|
|
2058
|
+
scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
|
|
2059
|
+
}
|
|
2060
|
+
const reclassifyStart = performance.now();
|
|
2061
|
+
try {
|
|
2062
|
+
const { classifyNodeRoles } = (await import('../../../../features/structure.js')) as {
|
|
2063
|
+
classifyNodeRoles: (
|
|
2064
|
+
db: BetterSqlite3Database,
|
|
2065
|
+
changedFiles?: string[] | null,
|
|
2066
|
+
) => Record<string, number>;
|
|
2067
|
+
};
|
|
2068
|
+
classifyNodeRoles(ctx.db as unknown as BetterSqlite3Database, scopedFiles);
|
|
2069
|
+
debug(
|
|
2070
|
+
scopedFiles
|
|
2071
|
+
? `Post-pass role re-classification complete (${scopedFiles.length} file(s))`
|
|
2072
|
+
: 'Post-pass role re-classification complete (full graph)',
|
|
2073
|
+
);
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
|
|
2076
|
+
}
|
|
2077
|
+
reclassifyMs = performance.now() - reclassifyStart;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// Backfill the `technique` column on `calls` edges written by the Rust
|
|
2081
|
+
// orchestrator, which does not write the column. Runs after all edge-writing
|
|
2082
|
+
// phases (including the WASM dropped-language backfill, CHA post-pass, and
|
|
2083
|
+
// this/super dispatch) so every new edge in this build cycle gets a label.
|
|
2084
|
+
const techniqueBackfillStart = performance.now();
|
|
2085
|
+
backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
|
|
2086
|
+
const techniqueBackfillMs = performance.now() - techniqueBackfillStart;
|
|
2087
|
+
|
|
2088
|
+
// Re-count nodes/edges now that all edge-writing post-passes have run: the
|
|
2089
|
+
// Rust orchestrator captured its counts before the JS post-passes added
|
|
2090
|
+
// edges, so both its summary and build_meta under-report (#1452).
|
|
2091
|
+
//
|
|
2092
|
+
// Fast path: skip the COUNT(*) scan when no post-pass wrote any edges.
|
|
2093
|
+
// COUNT(*) on large tables (50K+ edges) is non-trivial, especially via the
|
|
2094
|
+
// NativeDbProxy napi-rs round-trip. When all post-passes were no-ops, the
|
|
2095
|
+
// Rust orchestrator's counts are still accurate — no re-count needed.
|
|
2096
|
+
let finalNodeCount = result.nodeCount ?? 0;
|
|
2097
|
+
let finalEdgeCount = result.edgeCount ?? 0;
|
|
2098
|
+
const postPassWroteData = backfillHappened || chaEdgeCount > 0 || thisDispatchTargetIds.size > 0;
|
|
2099
|
+
if (postPassWroteData) {
|
|
2100
|
+
try {
|
|
2101
|
+
const counts = (ctx.db as unknown as BetterSqlite3Database)
|
|
2102
|
+
.prepare('SELECT (SELECT COUNT(*) FROM nodes) AS n, (SELECT COUNT(*) FROM edges) AS e')
|
|
2103
|
+
.get() as { n: number; e: number };
|
|
2104
|
+
if (counts.n !== finalNodeCount || counts.e !== finalEdgeCount) {
|
|
2105
|
+
finalNodeCount = counts.n;
|
|
2106
|
+
finalEdgeCount = counts.e;
|
|
2107
|
+
setBuildMeta(ctx.db, { node_count: finalNodeCount, edge_count: finalEdgeCount });
|
|
2108
|
+
}
|
|
2109
|
+
} catch (err) {
|
|
2110
|
+
debug(`Post-pass node/edge re-count failed: ${toErrorMessage(err)}`);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
info(
|
|
2114
|
+
`Native build orchestrator completed: ${finalNodeCount} nodes, ${finalEdgeCount} edges, ${result.fileCount ?? 0} files`,
|
|
2115
|
+
);
|
|
2116
|
+
|
|
2117
|
+
return {
|
|
2118
|
+
gapDetectMs,
|
|
2119
|
+
chaMs,
|
|
2120
|
+
thisDispatchMs,
|
|
2121
|
+
reclassifyMs,
|
|
2122
|
+
techniqueBackfillMs,
|
|
2123
|
+
backfillHappened,
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
|
|
1485
2127
|
/**
|
|
1486
2128
|
* Try the native build orchestrator.
|
|
1487
2129
|
*
|
|
@@ -1505,50 +2147,44 @@ export async function tryNativeOrchestrator(
|
|
|
1505
2147
|
return undefined;
|
|
1506
2148
|
}
|
|
1507
2149
|
|
|
1508
|
-
|
|
1509
|
-
// ~60ms cost on no-op/early-exit builds. Close the better-sqlite3 connection
|
|
1510
|
-
// first to avoid dual-connection WAL corruption.
|
|
1511
|
-
if (!ctx.nativeDb && ctx.nativeAvailable) {
|
|
1512
|
-
const native = loadNative();
|
|
1513
|
-
if (native?.NativeDatabase) {
|
|
1514
|
-
try {
|
|
1515
|
-
// Close better-sqlite3 before opening rusqlite to avoid WAL conflicts.
|
|
1516
|
-
// Uses raw close() instead of closeDb() intentionally — the advisory lock
|
|
1517
|
-
// is kept and transferred to the NativeDbProxy below, not released here.
|
|
1518
|
-
ctx.db.close();
|
|
1519
|
-
acquireAdvisoryLock(ctx.dbPath);
|
|
1520
|
-
ctx.nativeDb = native.NativeDatabase.openReadWrite(ctx.dbPath);
|
|
1521
|
-
ctx.nativeDb.initSchema();
|
|
1522
|
-
// Replace ctx.db with a NativeDbProxy so post-native JS fallback
|
|
1523
|
-
// (structure, analysis) can use it without reopening better-sqlite3.
|
|
1524
|
-
const proxy = new NativeDbProxy(ctx.nativeDb);
|
|
1525
|
-
proxy.__lockPath = `${ctx.dbPath}.lock`;
|
|
1526
|
-
ctx.db = proxy as unknown as typeof ctx.db;
|
|
1527
|
-
ctx.nativeFirstProxy = true;
|
|
1528
|
-
} catch (err) {
|
|
1529
|
-
warn(`NativeDatabase setup failed, falling back to JS: ${toErrorMessage(err)}`);
|
|
1530
|
-
try {
|
|
1531
|
-
ctx.nativeDb?.close();
|
|
1532
|
-
} catch (e) {
|
|
1533
|
-
debug(`tryNativeOrchestrator: close failed during fallback: ${toErrorMessage(e)}`);
|
|
1534
|
-
}
|
|
1535
|
-
ctx.nativeDb = undefined;
|
|
1536
|
-
ctx.nativeFirstProxy = false; // defensive: reset in case future refactors move the assignment above throwing lines
|
|
1537
|
-
releaseAdvisoryLock(`${ctx.dbPath}.lock`);
|
|
1538
|
-
// Reopen better-sqlite3 for JS pipeline fallback
|
|
1539
|
-
ctx.db = openDb(ctx.dbPath);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
2150
|
+
openNativeDatabase(ctx);
|
|
1543
2151
|
|
|
1544
2152
|
if (!ctx.nativeDb?.buildGraph) return undefined;
|
|
1545
2153
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
)
|
|
2154
|
+
// The previous full build's clear_all_graph_data() sets PRAGMA foreign_keys = ON
|
|
2155
|
+
// on the native connection. Older native binaries (< v3.14) do not delete
|
|
2156
|
+
// dataflow_vertices / dataflow_summary / call_edge_id rows before purging
|
|
2157
|
+
// nodes/edges during incremental builds, so FK enforcement causes the purge
|
|
2158
|
+
// statements to fail silently — leaving stale nodes and edges that then get
|
|
2159
|
+
// duplicated when the barrel-candidate re-parse re-inserts them (issue #1644).
|
|
2160
|
+
// Disabling FK before buildGraph() lets the purge succeed; FK is restored in
|
|
2161
|
+
// a finally block so post-passes (gap-repair, structure patch) retain FK protection
|
|
2162
|
+
// even if buildGraph() throws.
|
|
2163
|
+
try {
|
|
2164
|
+
ctx.nativeDb.exec('PRAGMA foreign_keys = OFF');
|
|
2165
|
+
} catch {
|
|
2166
|
+
// exec may not exist on very old addon versions — safe to ignore
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
let resultJson: string;
|
|
2170
|
+
try {
|
|
2171
|
+
resultJson = ctx.nativeDb.buildGraph(
|
|
2172
|
+
ctx.rootDir,
|
|
2173
|
+
JSON.stringify(ctx.config),
|
|
2174
|
+
JSON.stringify(ctx.aliases),
|
|
2175
|
+
JSON.stringify(ctx.opts),
|
|
2176
|
+
);
|
|
2177
|
+
} finally {
|
|
2178
|
+
// Restore FK enforcement so any subsequent writes to this connection
|
|
2179
|
+
// (gap-repair, structure patch) retain FK protection — even if buildGraph()
|
|
2180
|
+
// throws.
|
|
2181
|
+
try {
|
|
2182
|
+
ctx.nativeDb.exec('PRAGMA foreign_keys = ON');
|
|
2183
|
+
} catch {
|
|
2184
|
+
// safe to ignore on very old addon versions
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
1552
2188
|
const result = JSON.parse(resultJson) as NativeOrchestratorResult;
|
|
1553
2189
|
|
|
1554
2190
|
if (result.earlyExit) {
|
|
@@ -1646,136 +2282,7 @@ export async function tryNativeOrchestrator(
|
|
|
1646
2282
|
}
|
|
1647
2283
|
}
|
|
1648
2284
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
// Engine parity: the native orchestrator silently drops files whose
|
|
1652
|
-
// Rust extractor/grammar is missing or fails (e.g. HCL, Scala, Swift on
|
|
1653
|
-
// stale native binaries). WASM handles those — backfill via WASM so both
|
|
1654
|
-
// engines process the same file set (#967).
|
|
1655
|
-
//
|
|
1656
|
-
// Detect the gap once (fs walk + 2 DB queries) and use it for both gating
|
|
1657
|
-
// and the backfill itself. On quiet incrementals we still pay the walk so
|
|
1658
|
-
// we can detect brand-new files in dropped-language extensions — a gap that
|
|
1659
|
-
// the orchestrator's `detect_removed_files` filter (#1070) leaves open
|
|
1660
|
-
// (#1083, #1091). The pre-check is cheap because the expensive part (WASM
|
|
1661
|
-
// re-parse of the missing set) is gated below.
|
|
1662
|
-
const gapDetectStart = performance.now();
|
|
1663
|
-
const gap = detectDroppedLanguageGap(ctx);
|
|
1664
|
-
const backfillHappened = gap.missingAbs.length > 0 || gap.staleRel.length > 0;
|
|
1665
|
-
if (backfillHappened) {
|
|
1666
|
-
await backfillNativeDroppedFiles(ctx, gap);
|
|
1667
|
-
}
|
|
1668
|
-
const gapDetectMs = performance.now() - gapDetectStart;
|
|
1669
|
-
|
|
1670
|
-
// Phase 8.5: this/super dispatch — hybrid WASM re-parse to resolve call sites
|
|
1671
|
-
// whose raw receiver info the Rust pipeline does not persist to DB.
|
|
1672
|
-
// Runs BEFORE the CHA expansion pass so that super.method() → Parent.method edges
|
|
1673
|
-
// (technique='cha') are in the DB when runPostNativeCha expands them to sibling
|
|
1674
|
-
// class overrides (e.g. PostMixin.m → B.m when PostMixin and B both extend A).
|
|
1675
|
-
const {
|
|
1676
|
-
elapsedMs: thisDispatchMs,
|
|
1677
|
-
targetIds: thisDispatchTargetIds,
|
|
1678
|
-
affectedFiles: thisDispatchAffectedFiles,
|
|
1679
|
-
} = await runPostNativeThisDispatch(
|
|
1680
|
-
ctx.db as unknown as BetterSqlite3Database,
|
|
1681
|
-
ctx.rootDir,
|
|
1682
|
-
result.changedFiles,
|
|
1683
|
-
!!result.isFullBuild,
|
|
1684
|
-
);
|
|
1685
|
-
|
|
1686
|
-
// Phase 8.6: expand CHA call edges (interface dispatch → concrete implementations).
|
|
1687
|
-
// Returns the affected files so role re-classification below can be scoped to
|
|
1688
|
-
// the nodes whose fan-in/out actually changed.
|
|
1689
|
-
//
|
|
1690
|
-
// Runs AFTER this/super dispatch so super.method() edges are already in the DB.
|
|
1691
|
-
// The 'cha-expanded' technique tag on this pass's own output prevents re-expansion
|
|
1692
|
-
// of those edges in subsequent incremental builds, while 'cha'-tagged edges from
|
|
1693
|
-
// this/super dispatch remain eligible for expansion here.
|
|
1694
|
-
//
|
|
1695
|
-
// Function-as-object-property methods (`fn.method = function() {}`) are extracted
|
|
1696
|
-
// natively by the Rust engine (#1432) and resolved in-build by its edge builder, so
|
|
1697
|
-
// no WASM re-parse post-pass is needed for them. `Foo.prototype.bar = fn` likewise.
|
|
1698
|
-
const chaStart = performance.now();
|
|
1699
|
-
const { newEdgeCount: chaEdgeCount, affectedFiles: chaAffectedFiles } = runPostNativeCha(
|
|
1700
|
-
ctx.db as unknown as BetterSqlite3Database,
|
|
1701
|
-
// null = full build (scan all call→method edges); array = incremental (gate queries decide scope)
|
|
1702
|
-
result.isFullBuild ? null : (result.changedFiles ?? null),
|
|
1703
|
-
);
|
|
1704
|
-
const chaMs = performance.now() - chaStart;
|
|
1705
|
-
|
|
1706
|
-
// Role re-classification after JS edge-writing post-passes.
|
|
1707
|
-
// The Rust orchestrator classifies roles before these post-passes (CHA,
|
|
1708
|
-
// this-dispatch) add edges, so roles for the edge endpoints are stale.
|
|
1709
|
-
// Scoped to the files containing those endpoints: a new edge only changes
|
|
1710
|
-
// fan-in/out for its own source and target nodes, so re-classifying their
|
|
1711
|
-
// files restores correctness without re-running the classifier over the
|
|
1712
|
-
// whole graph (which cost ~130ms per build on codegraph itself and was a
|
|
1713
|
-
// major part of the v3.12.0 native full-build benchmark regression).
|
|
1714
|
-
let reclassifyMs = 0;
|
|
1715
|
-
if (chaEdgeCount > 0 || thisDispatchTargetIds.size > 0) {
|
|
1716
|
-
const affectedFiles = [...new Set([...chaAffectedFiles, ...thisDispatchAffectedFiles])];
|
|
1717
|
-
// When edges were inserted but all their endpoint nodes have null `file`
|
|
1718
|
-
// columns (rare but possible), affectedFiles stays empty even though
|
|
1719
|
-
// fan-in/out changed. Fall back to full-graph re-classification in that
|
|
1720
|
-
// case — scoped classification with an empty set would be a no-op, leaving
|
|
1721
|
-
// roles stale for those nodes.
|
|
1722
|
-
const scopedFiles = affectedFiles.length > 0 ? affectedFiles : null;
|
|
1723
|
-
const reclassifyStart = performance.now();
|
|
1724
|
-
try {
|
|
1725
|
-
const { classifyNodeRoles } = (await import('../../../../features/structure.js')) as {
|
|
1726
|
-
classifyNodeRoles: (
|
|
1727
|
-
db: BetterSqlite3Database,
|
|
1728
|
-
changedFiles?: string[] | null,
|
|
1729
|
-
) => Record<string, number>;
|
|
1730
|
-
};
|
|
1731
|
-
classifyNodeRoles(ctx.db as unknown as BetterSqlite3Database, scopedFiles);
|
|
1732
|
-
debug(
|
|
1733
|
-
scopedFiles
|
|
1734
|
-
? `Post-pass role re-classification complete (${scopedFiles.length} file(s))`
|
|
1735
|
-
: 'Post-pass role re-classification complete (full graph — null-file endpoints)',
|
|
1736
|
-
);
|
|
1737
|
-
} catch (err) {
|
|
1738
|
-
debug(`Post-pass role re-classification failed: ${toErrorMessage(err)}`);
|
|
1739
|
-
}
|
|
1740
|
-
reclassifyMs = performance.now() - reclassifyStart;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
// Backfill the `technique` column on `calls` edges written by the Rust
|
|
1744
|
-
// orchestrator, which does not write the column. Runs after all edge-writing
|
|
1745
|
-
// phases (including the WASM dropped-language backfill, CHA post-pass, and
|
|
1746
|
-
// this/super dispatch) so every new edge in this build cycle gets a label.
|
|
1747
|
-
const techniqueBackfillStart = performance.now();
|
|
1748
|
-
backfillEdgeTechniquesAfterNativeOrchestrator(ctx.db, !!result.isFullBuild, result.changedFiles);
|
|
1749
|
-
const techniqueBackfillMs = performance.now() - techniqueBackfillStart;
|
|
1750
|
-
|
|
1751
|
-
// Re-count nodes/edges now that all edge-writing post-passes have run: the
|
|
1752
|
-
// Rust orchestrator captured its counts before the JS post-passes added
|
|
1753
|
-
// edges, so both its summary and build_meta under-report (#1452).
|
|
1754
|
-
//
|
|
1755
|
-
// Fast path: skip the COUNT(*) scan when no post-pass wrote any edges.
|
|
1756
|
-
// COUNT(*) on large tables (50K+ edges) is non-trivial, especially via the
|
|
1757
|
-
// NativeDbProxy napi-rs round-trip. When all post-passes were no-ops, the
|
|
1758
|
-
// Rust orchestrator's counts are still accurate — no re-count needed.
|
|
1759
|
-
let finalNodeCount = result.nodeCount ?? 0;
|
|
1760
|
-
let finalEdgeCount = result.edgeCount ?? 0;
|
|
1761
|
-
const postPassWroteData = backfillHappened || chaEdgeCount > 0 || thisDispatchTargetIds.size > 0;
|
|
1762
|
-
if (postPassWroteData) {
|
|
1763
|
-
try {
|
|
1764
|
-
const counts = (ctx.db as unknown as BetterSqlite3Database)
|
|
1765
|
-
.prepare('SELECT (SELECT COUNT(*) FROM nodes) AS n, (SELECT COUNT(*) FROM edges) AS e')
|
|
1766
|
-
.get() as { n: number; e: number };
|
|
1767
|
-
if (counts.n !== finalNodeCount || counts.e !== finalEdgeCount) {
|
|
1768
|
-
finalNodeCount = counts.n;
|
|
1769
|
-
finalEdgeCount = counts.e;
|
|
1770
|
-
setBuildMeta(ctx.db, { node_count: finalNodeCount, edge_count: finalEdgeCount });
|
|
1771
|
-
}
|
|
1772
|
-
} catch (err) {
|
|
1773
|
-
debug(`Post-pass node/edge re-count failed: ${toErrorMessage(err)}`);
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
info(
|
|
1777
|
-
`Native build orchestrator completed: ${finalNodeCount} nodes, ${finalEdgeCount} edges, ${result.fileCount ?? 0} files`,
|
|
1778
|
-
);
|
|
2285
|
+
const postPassTimings = await runPostNativePasses(ctx, result);
|
|
1779
2286
|
|
|
1780
2287
|
// ── Structure and analysis fallback (run after edge-writing so roles see full graph) ──
|
|
1781
2288
|
// Reconstruct fileSymbols once for both structure and analysis to avoid two
|
|
@@ -1798,12 +2305,17 @@ export async function tryNativeOrchestrator(
|
|
|
1798
2305
|
}
|
|
1799
2306
|
}
|
|
1800
2307
|
|
|
2308
|
+
// P6: Vertex extraction for the analysisComplete=true path.
|
|
2309
|
+
// When needsAnalysisFallback=false (the normal native case), runPostNativeAnalysis
|
|
2310
|
+
// was skipped, so buildDataflowEdges never ran and dataflow_vertices were never
|
|
2311
|
+
// populated. Re-run the Rust dataflow visitor per file (fast — no re-parse) to
|
|
2312
|
+
// get the DataflowResult, then build vertices and inter-procedural edges.
|
|
2313
|
+
// Languages where Rust has no dataflow rules are silently skipped; a WASM
|
|
2314
|
+
// fallback for those is tracked in issue #1614.
|
|
2315
|
+
if (ctx.opts.dataflow !== false && !needsAnalysisFallback) {
|
|
2316
|
+
await runDataflowVertexPass(ctx, result.changedFiles);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
1801
2319
|
closeDbPair({ db: ctx.db, nativeDb: ctx.nativeDb });
|
|
1802
|
-
return formatNativeTimingResult(p, structurePatchMs, analysisTiming,
|
|
1803
|
-
gapDetectMs,
|
|
1804
|
-
chaMs,
|
|
1805
|
-
thisDispatchMs,
|
|
1806
|
-
reclassifyMs,
|
|
1807
|
-
techniqueBackfillMs,
|
|
1808
|
-
});
|
|
2320
|
+
return formatNativeTimingResult(p, structurePatchMs, analysisTiming, postPassTimings);
|
|
1809
2321
|
}
|