@quolu/lattice 0.69.5 → 0.71.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.ja.md +5 -1
- package/package.json +1 -1
- package/sensor/dist/bin/lattice-sensor.js +746 -323
- package/sensor/dist/bin/viewer-gate.d.ts +17 -0
- package/sensor/dist/bin/viewer-gate.d.ts.map +1 -0
- package/sensor/dist/bin/viewer-gate.js +31 -0
- package/sensor/dist/bin/viewer-gate.js.map +1 -0
- package/sensor/dist/context/formatter.d.ts +7 -1
- package/sensor/dist/context/formatter.d.ts.map +1 -1
- package/sensor/dist/context/formatter.js +12 -6
- package/sensor/dist/context/formatter.js.map +1 -1
- package/sensor/dist/context/index.d.ts +12 -0
- package/sensor/dist/context/index.d.ts.map +1 -1
- package/sensor/dist/context/index.js +103 -16
- package/sensor/dist/context/index.js.map +1 -1
- package/sensor/dist/db/index.d.ts +23 -2
- package/sensor/dist/db/index.d.ts.map +1 -1
- package/sensor/dist/db/index.js +109 -31
- package/sensor/dist/db/index.js.map +1 -1
- package/sensor/dist/db/migrations.d.ts +1 -1
- package/sensor/dist/db/migrations.d.ts.map +1 -1
- package/sensor/dist/db/migrations.js +68 -1
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +583 -6
- package/sensor/dist/db/queries.d.ts.map +1 -1
- package/sensor/dist/db/queries.js +1425 -57
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/schema.sql +27 -4
- package/sensor/dist/db/sqlite-adapter.d.ts +2 -0
- package/sensor/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/sensor/dist/db/sqlite-adapter.js +56 -7
- package/sensor/dist/db/sqlite-adapter.js.map +1 -1
- package/sensor/dist/db/synthesis-stage.d.ts +12 -0
- package/sensor/dist/db/synthesis-stage.d.ts.map +1 -0
- package/sensor/dist/db/synthesis-stage.js +93 -0
- package/sensor/dist/db/synthesis-stage.js.map +1 -0
- package/sensor/dist/db/wal-valve.d.ts +32 -12
- package/sensor/dist/db/wal-valve.d.ts.map +1 -1
- package/sensor/dist/db/wal-valve.js +65 -22
- package/sensor/dist/db/wal-valve.js.map +1 -1
- package/sensor/dist/db/wsl-shared-index.d.ts +53 -0
- package/sensor/dist/db/wsl-shared-index.d.ts.map +1 -0
- package/sensor/dist/db/wsl-shared-index.js +121 -0
- package/sensor/dist/db/wsl-shared-index.js.map +1 -0
- package/sensor/dist/directory.d.ts +126 -2
- package/sensor/dist/directory.d.ts.map +1 -1
- package/sensor/dist/directory.js +285 -16
- package/sensor/dist/directory.js.map +1 -1
- package/sensor/dist/errors.d.ts +14 -0
- package/sensor/dist/errors.d.ts.map +1 -1
- package/sensor/dist/errors.js +16 -1
- package/sensor/dist/errors.js.map +1 -1
- package/sensor/dist/extraction/cfml-extractor.d.ts +1 -0
- package/sensor/dist/extraction/cfml-extractor.d.ts.map +1 -1
- package/sensor/dist/extraction/cfml-extractor.js +15 -3
- package/sensor/dist/extraction/cfml-extractor.js.map +1 -1
- package/sensor/dist/extraction/dfm-extractor.d.ts.map +1 -1
- package/sensor/dist/extraction/dfm-extractor.js +14 -10
- package/sensor/dist/extraction/dfm-extractor.js.map +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts.map +1 -1
- package/sensor/dist/extraction/extraction-version.js +3 -1
- package/sensor/dist/extraction/extraction-version.js.map +1 -1
- package/sensor/dist/extraction/function-ref.d.ts +6 -4
- package/sensor/dist/extraction/function-ref.d.ts.map +1 -1
- package/sensor/dist/extraction/function-ref.js +35 -14
- package/sensor/dist/extraction/function-ref.js.map +1 -1
- package/sensor/dist/extraction/generated-detection.d.ts +48 -12
- package/sensor/dist/extraction/generated-detection.d.ts.map +1 -1
- package/sensor/dist/extraction/generated-detection.js +176 -11
- package/sensor/dist/extraction/generated-detection.js.map +1 -1
- package/sensor/dist/extraction/grammars.d.ts +38 -0
- package/sensor/dist/extraction/grammars.d.ts.map +1 -1
- package/sensor/dist/extraction/grammars.js +115 -3
- package/sensor/dist/extraction/grammars.js.map +1 -1
- package/sensor/dist/extraction/index.d.ts +218 -14
- package/sensor/dist/extraction/index.d.ts.map +1 -1
- package/sensor/dist/extraction/index.js +887 -175
- package/sensor/dist/extraction/index.js.map +1 -1
- package/sensor/dist/extraction/languages/c-cpp.d.ts +58 -0
- package/sensor/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/c-cpp.js +310 -19
- package/sensor/dist/extraction/languages/c-cpp.js.map +1 -1
- package/sensor/dist/extraction/languages/dart.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/dart.js +8 -3
- package/sensor/dist/extraction/languages/dart.js.map +1 -1
- package/sensor/dist/extraction/languages/erlang.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/erlang.js +58 -22
- package/sensor/dist/extraction/languages/erlang.js.map +1 -1
- package/sensor/dist/extraction/languages/javascript.js +1 -1
- package/sensor/dist/extraction/languages/javascript.js.map +1 -1
- package/sensor/dist/extraction/languages/kotlin.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/kotlin.js +169 -36
- package/sensor/dist/extraction/languages/kotlin.js.map +1 -1
- package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/lua.js +4 -1
- package/sensor/dist/extraction/languages/lua.js.map +1 -1
- package/sensor/dist/extraction/languages/objc.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/objc.js +4 -0
- package/sensor/dist/extraction/languages/objc.js.map +1 -1
- package/sensor/dist/extraction/languages/python.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/python.js +71 -0
- package/sensor/dist/extraction/languages/python.js.map +1 -1
- package/sensor/dist/extraction/languages/rust.d.ts +20 -0
- package/sensor/dist/extraction/languages/rust.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/rust.js +50 -20
- package/sensor/dist/extraction/languages/rust.js.map +1 -1
- package/sensor/dist/extraction/languages/scala.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/scala.js +18 -3
- package/sensor/dist/extraction/languages/scala.js.map +1 -1
- package/sensor/dist/extraction/languages/swift.d.ts +15 -0
- package/sensor/dist/extraction/languages/swift.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/swift.js +50 -0
- package/sensor/dist/extraction/languages/swift.js.map +1 -1
- package/sensor/dist/extraction/languages/typescript.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/typescript.js +9 -2
- package/sensor/dist/extraction/languages/typescript.js.map +1 -1
- package/sensor/dist/extraction/liquid-extractor.d.ts +18 -0
- package/sensor/dist/extraction/liquid-extractor.d.ts.map +1 -1
- package/sensor/dist/extraction/liquid-extractor.js +98 -22
- package/sensor/dist/extraction/liquid-extractor.js.map +1 -1
- package/sensor/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/sensor/dist/extraction/mybatis-extractor.js +6 -4
- package/sensor/dist/extraction/mybatis-extractor.js.map +1 -1
- package/sensor/dist/extraction/parse-pool.d.ts +7 -0
- package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
- package/sensor/dist/extraction/parse-pool.js +20 -1
- package/sensor/dist/extraction/parse-pool.js.map +1 -1
- package/sensor/dist/extraction/syntax-tokens.d.ts +103 -0
- package/sensor/dist/extraction/syntax-tokens.d.ts.map +1 -0
- package/sensor/dist/extraction/syntax-tokens.js +381 -0
- package/sensor/dist/extraction/syntax-tokens.js.map +1 -0
- package/sensor/dist/extraction/tree-sitter-helpers.d.ts +5 -0
- package/sensor/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter-helpers.js +17 -0
- package/sensor/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/sensor/dist/extraction/tree-sitter-types.d.ts +31 -3
- package/sensor/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.d.ts +101 -18
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +928 -134
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/sensor/dist/file-limits.d.ts +40 -0
- package/sensor/dist/file-limits.d.ts.map +1 -0
- package/sensor/dist/file-limits.js +147 -0
- package/sensor/dist/file-limits.js.map +1 -0
- package/sensor/dist/graph/branch-guards.d.ts +265 -0
- package/sensor/dist/graph/branch-guards.d.ts.map +1 -0
- package/sensor/dist/graph/branch-guards.js +2325 -0
- package/sensor/dist/graph/branch-guards.js.map +1 -0
- package/sensor/dist/graph/dead-code.d.ts +251 -0
- package/sensor/dist/graph/dead-code.d.ts.map +1 -0
- package/sensor/dist/graph/dead-code.js +776 -0
- package/sensor/dist/graph/dead-code.js.map +1 -0
- package/sensor/dist/graph/dynamic-boundary-report.d.ts +121 -0
- package/sensor/dist/graph/dynamic-boundary-report.d.ts.map +1 -0
- package/sensor/dist/graph/dynamic-boundary-report.js +283 -0
- package/sensor/dist/graph/dynamic-boundary-report.js.map +1 -0
- package/sensor/dist/graph/index.d.ts +4 -0
- package/sensor/dist/graph/index.d.ts.map +1 -1
- package/sensor/dist/graph/index.js +16 -1
- package/sensor/dist/graph/index.js.map +1 -1
- package/sensor/dist/graph/named-symbol-flow.d.ts +125 -0
- package/sensor/dist/graph/named-symbol-flow.d.ts.map +1 -0
- package/sensor/dist/graph/named-symbol-flow.js +552 -0
- package/sensor/dist/graph/named-symbol-flow.js.map +1 -0
- package/sensor/dist/graph/queries.d.ts.map +1 -1
- package/sensor/dist/graph/queries.js +2 -0
- package/sensor/dist/graph/queries.js.map +1 -1
- package/sensor/dist/graph/symbol-lookup.d.ts +83 -0
- package/sensor/dist/graph/symbol-lookup.d.ts.map +1 -0
- package/sensor/dist/graph/symbol-lookup.js +181 -0
- package/sensor/dist/graph/symbol-lookup.js.map +1 -0
- package/sensor/dist/graph/traversal.d.ts +9 -0
- package/sensor/dist/graph/traversal.d.ts.map +1 -1
- package/sensor/dist/graph/traversal.js +62 -36
- package/sensor/dist/graph/traversal.js.map +1 -1
- package/sensor/dist/graph/type-hierarchy.d.ts +155 -0
- package/sensor/dist/graph/type-hierarchy.d.ts.map +1 -0
- package/sensor/dist/graph/type-hierarchy.js +382 -0
- package/sensor/dist/graph/type-hierarchy.js.map +1 -0
- package/sensor/dist/index.d.ts +272 -10
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +510 -34
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/mcp/answer-freshness.d.ts +10 -0
- package/sensor/dist/mcp/answer-freshness.d.ts.map +1 -0
- package/sensor/dist/mcp/answer-freshness.js +68 -0
- package/sensor/dist/mcp/answer-freshness.js.map +1 -0
- package/sensor/dist/mcp/daemon-manager.d.ts +1 -1
- package/sensor/dist/mcp/daemon-manager.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-manager.js +17 -1
- package/sensor/dist/mcp/daemon-manager.js.map +1 -1
- package/sensor/dist/mcp/daemon-paths.d.ts +8 -0
- package/sensor/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-paths.js +65 -2
- package/sensor/dist/mcp/daemon-paths.js.map +1 -1
- package/sensor/dist/mcp/daemon-registry.d.ts +17 -4
- package/sensor/dist/mcp/daemon-registry.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-registry.js +133 -23
- package/sensor/dist/mcp/daemon-registry.js.map +1 -1
- package/sensor/dist/mcp/daemon.d.ts +26 -13
- package/sensor/dist/mcp/daemon.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon.js +126 -32
- package/sensor/dist/mcp/daemon.js.map +1 -1
- package/sensor/dist/mcp/dynamic-boundaries.d.ts +1 -10
- package/sensor/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/sensor/dist/mcp/dynamic-boundaries.js +46 -42
- package/sensor/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/sensor/dist/mcp/engine.d.ts +31 -9
- package/sensor/dist/mcp/engine.d.ts.map +1 -1
- package/sensor/dist/mcp/engine.js +190 -79
- package/sensor/dist/mcp/engine.js.map +1 -1
- package/sensor/dist/mcp/explore-dedup.d.ts +140 -0
- package/sensor/dist/mcp/explore-dedup.d.ts.map +1 -0
- package/sensor/dist/mcp/explore-dedup.js +239 -0
- package/sensor/dist/mcp/explore-dedup.js.map +1 -0
- package/sensor/dist/mcp/explore-diagnostics.d.ts +289 -0
- package/sensor/dist/mcp/explore-diagnostics.d.ts.map +1 -0
- package/sensor/dist/mcp/explore-diagnostics.js +558 -0
- package/sensor/dist/mcp/explore-diagnostics.js.map +1 -0
- package/sensor/dist/mcp/explore-session-state.d.ts +217 -0
- package/sensor/dist/mcp/explore-session-state.d.ts.map +1 -0
- package/sensor/dist/mcp/explore-session-state.js +322 -0
- package/sensor/dist/mcp/explore-session-state.js.map +1 -0
- package/sensor/dist/mcp/index-freshness-worker.d.ts +2 -0
- package/sensor/dist/mcp/index-freshness-worker.d.ts.map +1 -0
- package/sensor/dist/mcp/index-freshness-worker.js +30 -0
- package/sensor/dist/mcp/index-freshness-worker.js.map +1 -0
- package/sensor/dist/mcp/index-freshness.d.ts +7 -0
- package/sensor/dist/mcp/index-freshness.d.ts.map +1 -0
- package/sensor/dist/mcp/index-freshness.js +94 -0
- package/sensor/dist/mcp/index-freshness.js.map +1 -0
- package/sensor/dist/mcp/index.d.ts +3 -1
- package/sensor/dist/mcp/index.d.ts.map +1 -1
- package/sensor/dist/mcp/index.js +109 -12
- package/sensor/dist/mcp/index.js.map +1 -1
- package/sensor/dist/mcp/liveness-watchdog.d.ts +4 -1
- package/sensor/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/sensor/dist/mcp/liveness-watchdog.js +11 -2
- package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
- package/sensor/dist/mcp/project-lifecycle.d.ts +9 -0
- package/sensor/dist/mcp/project-lifecycle.d.ts.map +1 -0
- package/sensor/dist/mcp/project-lifecycle.js +182 -0
- package/sensor/dist/mcp/project-lifecycle.js.map +1 -0
- package/sensor/dist/mcp/proxy.d.ts.map +1 -1
- package/sensor/dist/mcp/proxy.js +15 -1
- package/sensor/dist/mcp/proxy.js.map +1 -1
- package/sensor/dist/mcp/query-pool.d.ts +2 -2
- package/sensor/dist/mcp/query-pool.d.ts.map +1 -1
- package/sensor/dist/mcp/query-pool.js +9 -7
- package/sensor/dist/mcp/query-pool.js.map +1 -1
- package/sensor/dist/mcp/query-worker.js +2 -2
- package/sensor/dist/mcp/query-worker.js.map +1 -1
- package/sensor/dist/mcp/server-instructions.d.ts +2 -2
- package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
- package/sensor/dist/mcp/server-instructions.js +13 -6
- package/sensor/dist/mcp/server-instructions.js.map +1 -1
- package/sensor/dist/mcp/session.d.ts +16 -0
- package/sensor/dist/mcp/session.d.ts.map +1 -1
- package/sensor/dist/mcp/session.js +36 -13
- package/sensor/dist/mcp/session.js.map +1 -1
- package/sensor/dist/mcp/startup-handshake.d.ts +3 -1
- package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
- package/sensor/dist/mcp/startup-handshake.js +10 -2
- package/sensor/dist/mcp/startup-handshake.js.map +1 -1
- package/sensor/dist/mcp/tools.d.ts +496 -38
- package/sensor/dist/mcp/tools.d.ts.map +1 -1
- package/sensor/dist/mcp/tools.js +4021 -772
- package/sensor/dist/mcp/tools.js.map +1 -1
- package/sensor/dist/mcp/writer-lock.d.ts +61 -0
- package/sensor/dist/mcp/writer-lock.d.ts.map +1 -0
- package/sensor/dist/mcp/writer-lock.js +260 -0
- package/sensor/dist/mcp/writer-lock.js.map +1 -0
- package/sensor/dist/project-config.d.ts +23 -0
- package/sensor/dist/project-config.d.ts.map +1 -1
- package/sensor/dist/project-config.js +39 -2
- package/sensor/dist/project-config.js.map +1 -1
- package/sensor/dist/resolution/alias-binding.d.ts +56 -0
- package/sensor/dist/resolution/alias-binding.d.ts.map +1 -0
- package/sensor/dist/resolution/alias-binding.js +127 -0
- package/sensor/dist/resolution/alias-binding.js.map +1 -0
- package/sensor/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/sensor/dist/resolution/c-fnptr-synthesizer.js +21 -19
- package/sensor/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/sensor/dist/resolution/callback-synthesizer.d.ts +23 -1
- package/sensor/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/sensor/dist/resolution/callback-synthesizer.js +356 -155
- package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
- package/sensor/dist/resolution/cpp-constructor.d.ts +4 -0
- package/sensor/dist/resolution/cpp-constructor.d.ts.map +1 -0
- package/sensor/dist/resolution/cpp-constructor.js +123 -0
- package/sensor/dist/resolution/cpp-constructor.js.map +1 -0
- package/sensor/dist/resolution/cpp-macro-visibility.d.ts +12 -0
- package/sensor/dist/resolution/cpp-macro-visibility.d.ts.map +1 -0
- package/sensor/dist/resolution/cpp-macro-visibility.js +342 -0
- package/sensor/dist/resolution/cpp-macro-visibility.js.map +1 -0
- package/sensor/dist/resolution/expo-router-synthesizer.d.ts +32 -0
- package/sensor/dist/resolution/expo-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/expo-router-synthesizer.js +189 -0
- package/sensor/dist/resolution/expo-router-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/frameworks/csharp.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/csharp.js +96 -2
- package/sensor/dist/resolution/frameworks/csharp.js.map +1 -1
- package/sensor/dist/resolution/frameworks/expo-router.d.ts +189 -0
- package/sensor/dist/resolution/frameworks/expo-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/expo-router.js +785 -0
- package/sensor/dist/resolution/frameworks/expo-router.js.map +1 -0
- package/sensor/dist/resolution/frameworks/express.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/express.js +251 -16
- package/sensor/dist/resolution/frameworks/express.js.map +1 -1
- package/sensor/dist/resolution/frameworks/index.d.ts +5 -0
- package/sensor/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/index.js +29 -1
- package/sensor/dist/resolution/frameworks/index.js.map +1 -1
- package/sensor/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/java.js +79 -47
- package/sensor/dist/resolution/frameworks/java.js.map +1 -1
- package/sensor/dist/resolution/frameworks/laravel.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/laravel.js +294 -4
- package/sensor/dist/resolution/frameworks/laravel.js.map +1 -1
- package/sensor/dist/resolution/frameworks/nestjs.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/nestjs.js +6 -14
- package/sensor/dist/resolution/frameworks/nestjs.js.map +1 -1
- package/sensor/dist/resolution/frameworks/nextjs.d.ts +76 -0
- package/sensor/dist/resolution/frameworks/nextjs.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/nextjs.js +328 -0
- package/sensor/dist/resolution/frameworks/nextjs.js.map +1 -0
- package/sensor/dist/resolution/frameworks/object-literal.d.ts +32 -0
- package/sensor/dist/resolution/frameworks/object-literal.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/object-literal.js +139 -0
- package/sensor/dist/resolution/frameworks/object-literal.js.map +1 -0
- package/sensor/dist/resolution/frameworks/package-deps.d.ts +14 -0
- package/sensor/dist/resolution/frameworks/package-deps.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/package-deps.js +78 -0
- package/sensor/dist/resolution/frameworks/package-deps.js.map +1 -0
- package/sensor/dist/resolution/frameworks/python.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/python.js +166 -0
- package/sensor/dist/resolution/frameworks/python.js.map +1 -1
- package/sensor/dist/resolution/frameworks/react-native.d.ts +26 -0
- package/sensor/dist/resolution/frameworks/react-native.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/react-native.js +153 -14
- package/sensor/dist/resolution/frameworks/react-native.js.map +1 -1
- package/sensor/dist/resolution/frameworks/react-router.d.ts +46 -0
- package/sensor/dist/resolution/frameworks/react-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/react-router.js +187 -0
- package/sensor/dist/resolution/frameworks/react-router.js.map +1 -0
- package/sensor/dist/resolution/frameworks/react.d.ts +2 -1
- package/sensor/dist/resolution/frameworks/react.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/react.js +188 -139
- package/sensor/dist/resolution/frameworks/react.js.map +1 -1
- package/sensor/dist/resolution/frameworks/svelte.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/svelte.js +6 -1
- package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
- package/sensor/dist/resolution/frameworks/sveltekit-router.d.ts +36 -0
- package/sensor/dist/resolution/frameworks/sveltekit-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/sveltekit-router.js +156 -0
- package/sensor/dist/resolution/frameworks/sveltekit-router.js.map +1 -0
- package/sensor/dist/resolution/frameworks/swift.d.ts.map +1 -1
- package/sensor/dist/resolution/frameworks/swift.js +140 -99
- package/sensor/dist/resolution/frameworks/swift.js.map +1 -1
- package/sensor/dist/resolution/frameworks/tanstack-router.d.ts +84 -0
- package/sensor/dist/resolution/frameworks/tanstack-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/tanstack-router.js +437 -0
- package/sensor/dist/resolution/frameworks/tanstack-router.js.map +1 -0
- package/sensor/dist/resolution/frameworks/vue-router.d.ts +82 -0
- package/sensor/dist/resolution/frameworks/vue-router.d.ts.map +1 -0
- package/sensor/dist/resolution/frameworks/vue-router.js +377 -0
- package/sensor/dist/resolution/frameworks/vue-router.js.map +1 -0
- package/sensor/dist/resolution/import-resolver.d.ts +38 -0
- package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
- package/sensor/dist/resolution/import-resolver.js +394 -28
- package/sensor/dist/resolution/import-resolver.js.map +1 -1
- package/sensor/dist/resolution/index.d.ts +89 -25
- package/sensor/dist/resolution/index.d.ts.map +1 -1
- package/sensor/dist/resolution/index.js +573 -148
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/js-builtins.d.ts +13 -0
- package/sensor/dist/resolution/js-builtins.d.ts.map +1 -0
- package/sensor/dist/resolution/js-builtins.js +46 -0
- package/sensor/dist/resolution/js-builtins.js.map +1 -0
- package/sensor/dist/resolution/name-matcher.d.ts +99 -22
- package/sensor/dist/resolution/name-matcher.d.ts.map +1 -1
- package/sensor/dist/resolution/name-matcher.js +2220 -86
- package/sensor/dist/resolution/name-matcher.js.map +1 -1
- package/sensor/dist/resolution/next-router-synthesizer.d.ts +27 -0
- package/sensor/dist/resolution/next-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/next-router-synthesizer.js +113 -0
- package/sensor/dist/resolution/next-router-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/path-aliases.d.ts +6 -4
- package/sensor/dist/resolution/path-aliases.d.ts.map +1 -1
- package/sensor/dist/resolution/path-aliases.js +125 -17
- package/sensor/dist/resolution/path-aliases.js.map +1 -1
- package/sensor/dist/resolution/react-router-synthesizer.d.ts +33 -0
- package/sensor/dist/resolution/react-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/react-router-synthesizer.js +126 -0
- package/sensor/dist/resolution/react-router-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/resolver-pool.d.ts +2 -0
- package/sensor/dist/resolution/resolver-pool.d.ts.map +1 -1
- package/sensor/dist/resolution/resolver-pool.js +4 -0
- package/sensor/dist/resolution/resolver-pool.js.map +1 -1
- package/sensor/dist/resolution/strip-comments.d.ts +6 -0
- package/sensor/dist/resolution/strip-comments.d.ts.map +1 -1
- package/sensor/dist/resolution/strip-comments.js +58 -0
- package/sensor/dist/resolution/strip-comments.js.map +1 -1
- package/sensor/dist/resolution/sveltekit-synthesizer.d.ts +58 -0
- package/sensor/dist/resolution/sveltekit-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/sveltekit-synthesizer.js +173 -0
- package/sensor/dist/resolution/sveltekit-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/swift-type-visibility.d.ts +52 -0
- package/sensor/dist/resolution/swift-type-visibility.d.ts.map +1 -0
- package/sensor/dist/resolution/swift-type-visibility.js +328 -0
- package/sensor/dist/resolution/swift-type-visibility.js.map +1 -0
- package/sensor/dist/resolution/synth-utils.d.ts +26 -0
- package/sensor/dist/resolution/synth-utils.d.ts.map +1 -0
- package/sensor/dist/resolution/synth-utils.js +75 -0
- package/sensor/dist/resolution/synth-utils.js.map +1 -0
- package/sensor/dist/resolution/tanstack-router-synthesizer.d.ts +31 -0
- package/sensor/dist/resolution/tanstack-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/tanstack-router-synthesizer.js +114 -0
- package/sensor/dist/resolution/tanstack-router-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/tier-synthesizer.d.ts +54 -0
- package/sensor/dist/resolution/tier-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/tier-synthesizer.js +1294 -0
- package/sensor/dist/resolution/tier-synthesizer.js.map +1 -0
- package/sensor/dist/resolution/types.d.ts +78 -2
- package/sensor/dist/resolution/types.d.ts.map +1 -1
- package/sensor/dist/resolution/types.js +64 -0
- package/sensor/dist/resolution/types.js.map +1 -1
- package/sensor/dist/resolution/vue-router-synthesizer.d.ts +30 -0
- package/sensor/dist/resolution/vue-router-synthesizer.d.ts.map +1 -0
- package/sensor/dist/resolution/vue-router-synthesizer.js +115 -0
- package/sensor/dist/resolution/vue-router-synthesizer.js.map +1 -0
- package/sensor/dist/search/identifier-segments.d.ts +10 -0
- package/sensor/dist/search/identifier-segments.d.ts.map +1 -1
- package/sensor/dist/search/identifier-segments.js +29 -0
- package/sensor/dist/search/identifier-segments.js.map +1 -1
- package/sensor/dist/search/query-paths.d.ts +106 -0
- package/sensor/dist/search/query-paths.d.ts.map +1 -0
- package/sensor/dist/search/query-paths.js +513 -0
- package/sensor/dist/search/query-paths.js.map +1 -0
- package/sensor/dist/search/query-utils.d.ts +22 -2
- package/sensor/dist/search/query-utils.d.ts.map +1 -1
- package/sensor/dist/search/query-utils.js +56 -16
- package/sensor/dist/search/query-utils.js.map +1 -1
- package/sensor/dist/sync/watch-policy.d.ts +14 -0
- package/sensor/dist/sync/watch-policy.d.ts.map +1 -1
- package/sensor/dist/sync/watch-policy.js +19 -0
- package/sensor/dist/sync/watch-policy.js.map +1 -1
- package/sensor/dist/sync/watcher.d.ts +74 -35
- package/sensor/dist/sync/watcher.d.ts.map +1 -1
- package/sensor/dist/sync/watcher.js +281 -99
- package/sensor/dist/sync/watcher.js.map +1 -1
- package/sensor/dist/telemetry/index.d.ts +4 -2
- package/sensor/dist/telemetry/index.d.ts.map +1 -1
- package/sensor/dist/telemetry/index.js +92 -41
- package/sensor/dist/telemetry/index.js.map +1 -1
- package/sensor/dist/types.d.ts +18 -2
- package/sensor/dist/types.d.ts.map +1 -1
- package/sensor/dist/types.js +2 -0
- package/sensor/dist/types.js.map +1 -1
- package/sensor/dist/ui-server/api/deadcode.d.ts +84 -0
- package/sensor/dist/ui-server/api/deadcode.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/deadcode.js +136 -0
- package/sensor/dist/ui-server/api/deadcode.js.map +1 -0
- package/sensor/dist/ui-server/api/effects.d.ts +94 -0
- package/sensor/dist/ui-server/api/effects.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/effects.js +527 -0
- package/sensor/dist/ui-server/api/effects.js.map +1 -0
- package/sensor/dist/ui-server/api/entrypoints.d.ts +92 -0
- package/sensor/dist/ui-server/api/entrypoints.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/entrypoints.js +361 -0
- package/sensor/dist/ui-server/api/entrypoints.js.map +1 -0
- package/sensor/dist/ui-server/api/events.d.ts +183 -0
- package/sensor/dist/ui-server/api/events.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/events.js +430 -0
- package/sensor/dist/ui-server/api/events.js.map +1 -0
- package/sensor/dist/ui-server/api/file.d.ts +66 -0
- package/sensor/dist/ui-server/api/file.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/file.js +235 -0
- package/sensor/dist/ui-server/api/file.js.map +1 -0
- package/sensor/dist/ui-server/api/filecode.d.ts +126 -0
- package/sensor/dist/ui-server/api/filecode.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/filecode.js +205 -0
- package/sensor/dist/ui-server/api/filecode.js.map +1 -0
- package/sensor/dist/ui-server/api/flow.d.ts +226 -0
- package/sensor/dist/ui-server/api/flow.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/flow.js +612 -0
- package/sensor/dist/ui-server/api/flow.js.map +1 -0
- package/sensor/dist/ui-server/api/hierarchy.d.ts +81 -0
- package/sensor/dist/ui-server/api/hierarchy.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/hierarchy.js +92 -0
- package/sensor/dist/ui-server/api/hierarchy.js.map +1 -0
- package/sensor/dist/ui-server/api/index.d.ts +90 -0
- package/sensor/dist/ui-server/api/index.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/index.js +332 -0
- package/sensor/dist/ui-server/api/index.js.map +1 -0
- package/sensor/dist/ui-server/api/map.d.ts +256 -0
- package/sensor/dist/ui-server/api/map.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/map.js +760 -0
- package/sensor/dist/ui-server/api/map.js.map +1 -0
- package/sensor/dist/ui-server/api/node.d.ts +81 -0
- package/sensor/dist/ui-server/api/node.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/node.js +388 -0
- package/sensor/dist/ui-server/api/node.js.map +1 -0
- package/sensor/dist/ui-server/api/nodes.d.ts +25 -0
- package/sensor/dist/ui-server/api/nodes.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/nodes.js +45 -0
- package/sensor/dist/ui-server/api/nodes.js.map +1 -0
- package/sensor/dist/ui-server/api/program.d.ts +139 -0
- package/sensor/dist/ui-server/api/program.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/program.js +315 -0
- package/sensor/dist/ui-server/api/program.js.map +1 -0
- package/sensor/dist/ui-server/api/respond.d.ts +76 -0
- package/sensor/dist/ui-server/api/respond.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/respond.js +186 -0
- package/sensor/dist/ui-server/api/respond.js.map +1 -0
- package/sensor/dist/ui-server/api/route-roots.d.ts +39 -0
- package/sensor/dist/ui-server/api/route-roots.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/route-roots.js +96 -0
- package/sensor/dist/ui-server/api/route-roots.js.map +1 -0
- package/sensor/dist/ui-server/api/routes.d.ts +59 -0
- package/sensor/dist/ui-server/api/routes.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/routes.js +108 -0
- package/sensor/dist/ui-server/api/routes.js.map +1 -0
- package/sensor/dist/ui-server/api/screens.d.ts +117 -0
- package/sensor/dist/ui-server/api/screens.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/screens.js +611 -0
- package/sensor/dist/ui-server/api/screens.js.map +1 -0
- package/sensor/dist/ui-server/api/search.d.ts +29 -0
- package/sensor/dist/ui-server/api/search.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/search.js +232 -0
- package/sensor/dist/ui-server/api/search.js.map +1 -0
- package/sensor/dist/ui-server/api/session.d.ts +52 -0
- package/sensor/dist/ui-server/api/session.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/session.js +196 -0
- package/sensor/dist/ui-server/api/session.js.map +1 -0
- package/sensor/dist/ui-server/api/source.d.ts +200 -0
- package/sensor/dist/ui-server/api/source.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/source.js +422 -0
- package/sensor/dist/ui-server/api/source.js.map +1 -0
- package/sensor/dist/ui-server/api/stats.d.ts +27 -0
- package/sensor/dist/ui-server/api/stats.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/stats.js +164 -0
- package/sensor/dist/ui-server/api/stats.js.map +1 -0
- package/sensor/dist/ui-server/api/steps.d.ts +274 -0
- package/sensor/dist/ui-server/api/steps.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/steps.js +1544 -0
- package/sensor/dist/ui-server/api/steps.js.map +1 -0
- package/sensor/dist/ui-server/api/trail-store.d.ts +142 -0
- package/sensor/dist/ui-server/api/trail-store.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/trail-store.js +325 -0
- package/sensor/dist/ui-server/api/trail-store.js.map +1 -0
- package/sensor/dist/ui-server/api/trails.d.ts +163 -0
- package/sensor/dist/ui-server/api/trails.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/trails.js +369 -0
- package/sensor/dist/ui-server/api/trails.js.map +1 -0
- package/sensor/dist/ui-server/api/when.d.ts +133 -0
- package/sensor/dist/ui-server/api/when.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/when.js +275 -0
- package/sensor/dist/ui-server/api/when.js.map +1 -0
- package/sensor/dist/ui-server/api/wire.d.ts +179 -0
- package/sensor/dist/ui-server/api/wire.d.ts.map +1 -0
- package/sensor/dist/ui-server/api/wire.js +231 -0
- package/sensor/dist/ui-server/api/wire.js.map +1 -0
- package/sensor/dist/ui-server/assets.d.ts +40 -0
- package/sensor/dist/ui-server/assets.d.ts.map +1 -0
- package/sensor/dist/ui-server/assets.js +110 -0
- package/sensor/dist/ui-server/assets.js.map +1 -0
- package/sensor/dist/ui-server/constants.d.ts +32 -0
- package/sensor/dist/ui-server/constants.d.ts.map +1 -0
- package/sensor/dist/ui-server/constants.js +35 -0
- package/sensor/dist/ui-server/constants.js.map +1 -0
- package/sensor/dist/ui-server/highlight/index.d.ts +113 -0
- package/sensor/dist/ui-server/highlight/index.d.ts.map +1 -0
- package/sensor/dist/ui-server/highlight/index.js +307 -0
- package/sensor/dist/ui-server/highlight/index.js.map +1 -0
- package/sensor/dist/ui-server/highlight/languages.d.ts +36 -0
- package/sensor/dist/ui-server/highlight/languages.d.ts.map +1 -0
- package/sensor/dist/ui-server/highlight/languages.js +50 -0
- package/sensor/dist/ui-server/highlight/languages.js.map +1 -0
- package/sensor/dist/ui-server/index.d.ts +92 -0
- package/sensor/dist/ui-server/index.d.ts.map +1 -0
- package/sensor/dist/ui-server/index.js +399 -0
- package/sensor/dist/ui-server/index.js.map +1 -0
- package/sensor/dist/ui-server/open-browser.d.ts +31 -0
- package/sensor/dist/ui-server/open-browser.d.ts.map +1 -0
- package/sensor/dist/ui-server/open-browser.js +79 -0
- package/sensor/dist/ui-server/open-browser.js.map +1 -0
- package/sensor/dist/ui-server/security.d.ts +127 -0
- package/sensor/dist/ui-server/security.d.ts.map +1 -0
- package/sensor/dist/ui-server/security.js +332 -0
- package/sensor/dist/ui-server/security.js.map +1 -0
- package/sensor/dist/ui-server/static.d.ts +45 -0
- package/sensor/dist/ui-server/static.d.ts.map +1 -0
- package/sensor/dist/ui-server/static.js +165 -0
- package/sensor/dist/ui-server/static.js.map +1 -0
- package/sensor/dist/utils.d.ts +14 -2
- package/sensor/dist/utils.d.ts.map +1 -1
- package/sensor/dist/utils.js +25 -10
- package/sensor/dist/utils.js.map +1 -1
- package/src/cli-help.mjs +4 -2
- package/src/sensor-adapter.mjs +5 -1
- package/src/todo-cli.mjs +88 -11
|
@@ -5,20 +5,26 @@
|
|
|
5
5
|
* Prepared statements for CRUD operations on the knowledge graph.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.QueryBuilder = void 0;
|
|
8
|
+
exports.QueryBuilder = exports.DEPRIORITIZED_NAME_BONUS_SCALE = void 0;
|
|
9
9
|
const utils_1 = require("../utils");
|
|
10
10
|
const query_utils_1 = require("../search/query-utils");
|
|
11
11
|
const query_parser_1 = require("../search/query-parser");
|
|
12
12
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
13
13
|
const identifier_segments_1 = require("../search/identifier-segments");
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* Files that should not be candidates for "dominant file" detection: test/spec
|
|
16
|
+
* files and tool-generated files. Generated files (`*.pb.go`, `*.pulsar.go`,
|
|
17
|
+
* mock outputs, …) often have huge in-file edge counts that dwarf the real
|
|
18
|
+
* source — etcd's `rpc.pb.go` has 4× the in-file edges of `server.go`.
|
|
19
|
+
*
|
|
20
|
+
* Path patterns plus, when the caller passes the indexed set, files whose
|
|
21
|
+
* HEADER declares them generated — a `payroll.go` full of generated CRUD has
|
|
22
|
+
* exactly the same edge-density problem as `rpc.pb.go` and nothing in its name
|
|
23
|
+
* to catch it (#1500).
|
|
20
24
|
*/
|
|
21
|
-
function isLowValueFile(filePath) {
|
|
25
|
+
function isLowValueFile(filePath, generated) {
|
|
26
|
+
if (generated?.has(filePath))
|
|
27
|
+
return true;
|
|
22
28
|
const lp = filePath.toLowerCase();
|
|
23
29
|
return (/(?:^|\/)(tests?|__tests?__|spec)\//.test(lp) ||
|
|
24
30
|
/_test\.go$/.test(lp) ||
|
|
@@ -34,6 +40,39 @@ function isLowValueFile(filePath) {
|
|
|
34
40
|
(0, generated_detection_1.isGeneratedFile)(filePath));
|
|
35
41
|
}
|
|
36
42
|
const SQLITE_PARAM_CHUNK_SIZE = 500;
|
|
43
|
+
/**
|
|
44
|
+
* A SQL predicate: is the node aliased `alias` a member an INTERFACE declares?
|
|
45
|
+
*
|
|
46
|
+
* `method_signature` / `property_signature` enter the graph as `method` /
|
|
47
|
+
* `property` nodes hung off their interface by a `contains` edge (#1638). They
|
|
48
|
+
* have no body and originate no behaviour, so for a structural judgement about
|
|
49
|
+
* a FILE they are the interface restated, not an extra thing the file declares.
|
|
50
|
+
* See {@link QueryBuilder.getAmbientDeclarationPathsAmong}, the one caller, for
|
|
51
|
+
* why treating them as opaque would break that rule in three places at once.
|
|
52
|
+
*
|
|
53
|
+
* Seeks `idx_edges_target_kind`, so it costs a key lookup per row rather than a
|
|
54
|
+
* join over the whole edge table.
|
|
55
|
+
*/
|
|
56
|
+
const IS_INTERFACE_MEMBER = (alias) => `EXISTS (
|
|
57
|
+
SELECT 1 FROM edges ce JOIN nodes owner ON owner.id = ce.source
|
|
58
|
+
WHERE ce.target = ${alias}.id AND ce.kind = 'contains' AND owner.kind = 'interface'
|
|
59
|
+
)`;
|
|
60
|
+
/**
|
|
61
|
+
* How much of the exact-name bonus a `deprioritize`d path keeps (#982). Damped
|
|
62
|
+
* rather than zeroed: a query that genuinely targets that tree must still rank
|
|
63
|
+
* it, the same "discount, don't erase" rule the path penalty follows.
|
|
64
|
+
*
|
|
65
|
+
* Derived rather than picked. `nameMatchBonus`'s prefix arm tops out below
|
|
66
|
+
* `10 + 30 = 40`, and a de-prioritized node also takes the -15 path penalty, so
|
|
67
|
+
* `80 * SCALE - 15 > 40` is what stops a damped WHOLE-QUERY exact match from
|
|
68
|
+
* losing to a mere prefix match. 0.75 clears it (45). Measured on a 62k-node
|
|
69
|
+
* django index: at 0.25 that invariant breaks in practice — `child`, `parent`
|
|
70
|
+
* and `method` lose rank 1 to `children`, `all_parents` and `method_decorator`
|
|
71
|
+
* — while crowd-out removal is almost flat between 0.75 and 0.5 (39 vs 40 of 88
|
|
72
|
+
* peripheral top-10 slots cleared), so a deeper discount buys little and costs
|
|
73
|
+
* the invariant. Pinned by a test.
|
|
74
|
+
*/
|
|
75
|
+
exports.DEPRIORITIZED_NAME_BONUS_SCALE = 0.75;
|
|
37
76
|
/**
|
|
38
77
|
* `edges.resolved_by` strategy names that resolve a name WITHOUT verifying
|
|
39
78
|
* that any import/require/include path actually connects the two files —
|
|
@@ -79,8 +118,11 @@ const UNCORROBORATED_FILTER_LANGUAGES_SQL_LIST = [...UNCORROBORATED_FILTER_LANGU
|
|
|
79
118
|
* refs against newly-added node names.
|
|
80
119
|
*/
|
|
81
120
|
function referenceNameTail(referenceName) {
|
|
82
|
-
|
|
83
|
-
|
|
121
|
+
// Erlang refs carry a written arity (`f/1`, `mod::fn/2` — #1610); the tail a
|
|
122
|
+
// new symbol's plain name could match is the arity-less function name.
|
|
123
|
+
const base = referenceName.replace(/\/\d{1,3}$/, '') || referenceName;
|
|
124
|
+
const idx = Math.max(base.lastIndexOf('.'), base.lastIndexOf(':'));
|
|
125
|
+
return idx >= 0 ? base.slice(idx + 1) : base;
|
|
84
126
|
}
|
|
85
127
|
/**
|
|
86
128
|
* Convert database row to Node object
|
|
@@ -164,6 +206,7 @@ function rowToFileRecord(row) {
|
|
|
164
206
|
nodeCount: row.node_count,
|
|
165
207
|
extractionVersion: row.extraction_version ?? 0,
|
|
166
208
|
errors: row.errors ? (0, utils_1.safeJsonParse)(row.errors, undefined) : undefined,
|
|
209
|
+
generated: row.generated === 1,
|
|
167
210
|
};
|
|
168
211
|
}
|
|
169
212
|
/**
|
|
@@ -176,9 +219,16 @@ class QueryBuilder {
|
|
|
176
219
|
// whole project, not a symbol, so it carries no discriminative signal (#720).
|
|
177
220
|
// Set once by the LatticeSensor instance; empty by default (no down-weighting).
|
|
178
221
|
projectNameTokens = new Set();
|
|
222
|
+
isDeprioritizedPath;
|
|
223
|
+
// FTS5 availability flag — detected once at construction time (#1532)
|
|
224
|
+
_fts5Available;
|
|
179
225
|
// Node cache for frequently accessed nodes (LRU-style, max 1000 entries)
|
|
180
226
|
nodeCache = new Map();
|
|
181
227
|
maxCacheSize = 1000;
|
|
228
|
+
// getDominantFile()'s answer, tagged with the database change stamp it was
|
|
229
|
+
// computed under (see getChangeStamp). Query-independent, so one value
|
|
230
|
+
// serves every explore until the database changes (#1864).
|
|
231
|
+
dominantFileMemo;
|
|
182
232
|
// Prepared statements (lazily initialized)
|
|
183
233
|
stmts = {};
|
|
184
234
|
// Names whose segments were already written this session — skips re-splitting
|
|
@@ -195,6 +245,11 @@ class QueryBuilder {
|
|
|
195
245
|
// (and therefore resolution's insertion-order disambiguation) is identical
|
|
196
246
|
// to the one-row-per-run path.
|
|
197
247
|
batchStmts = new Map();
|
|
248
|
+
// Kind-filtered edge reads build their SQL per call (a variable IN list),
|
|
249
|
+
// but from a handful of kind sets: prepare each shape once. Supertype walks
|
|
250
|
+
// and the member-lookup passes issue them per node, and preparing cost
|
|
251
|
+
// about a third of the read.
|
|
252
|
+
edgeKindStmts = new Map();
|
|
198
253
|
static BATCH_SIZES = [128, 32, 8, 1];
|
|
199
254
|
/**
|
|
200
255
|
* Run `rows` through a multi-row `INSERT` built as `head + (tuple,)*n`,
|
|
@@ -230,6 +285,14 @@ class QueryBuilder {
|
|
|
230
285
|
}
|
|
231
286
|
constructor(db) {
|
|
232
287
|
this.db = db;
|
|
288
|
+
// Detect FTS5 availability once (#1532)
|
|
289
|
+
try {
|
|
290
|
+
db.prepare("SELECT * FROM nodes_fts LIMIT 0").get();
|
|
291
|
+
this._fts5Available = true;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
this._fts5Available = false;
|
|
295
|
+
}
|
|
233
296
|
}
|
|
234
297
|
/**
|
|
235
298
|
* Swap the underlying connection in place. Used by pool workers'
|
|
@@ -247,6 +310,21 @@ class QueryBuilder {
|
|
|
247
310
|
this.db = db;
|
|
248
311
|
this.stmts = {};
|
|
249
312
|
this.batchStmts.clear();
|
|
313
|
+
this.edgeKindStmts.clear();
|
|
314
|
+
// The change stamp is per connection, and fresh connections to two
|
|
315
|
+
// different databases report the same one — the memo goes with the old
|
|
316
|
+
// connection, or a worker following a rebuilt index keeps its answer (#1864).
|
|
317
|
+
this.dominantFileMemo = undefined;
|
|
318
|
+
}
|
|
319
|
+
edgeKindStmt(sql) {
|
|
320
|
+
let stmt = this.edgeKindStmts.get(sql);
|
|
321
|
+
if (!stmt) {
|
|
322
|
+
if (this.edgeKindStmts.size >= 64)
|
|
323
|
+
this.edgeKindStmts.delete(this.edgeKindStmts.keys().next().value);
|
|
324
|
+
stmt = this.db.prepare(sql);
|
|
325
|
+
this.edgeKindStmts.set(sql, stmt);
|
|
326
|
+
}
|
|
327
|
+
return stmt;
|
|
250
328
|
}
|
|
251
329
|
/** Set the normalized project-name tokens used to down-weight non-discriminative
|
|
252
330
|
* query words in path scoring (#720). Called once when the project opens. */
|
|
@@ -257,6 +335,19 @@ class QueryBuilder {
|
|
|
257
335
|
getProjectNameTokens() {
|
|
258
336
|
return this.projectNameTokens;
|
|
259
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Set the predicate that marks a path as de-prioritized by the project's
|
|
340
|
+
* `lattice-sensor.json` `deprioritize` patterns (#982). Ranking-only: those paths
|
|
341
|
+
* stay indexed and findable, they just stop outranking first-party code.
|
|
342
|
+
* Called once when the project opens; undefined disables the lever.
|
|
343
|
+
*/
|
|
344
|
+
setDeprioritizedPathMatcher(matcher) {
|
|
345
|
+
this.isDeprioritizedPath = matcher;
|
|
346
|
+
}
|
|
347
|
+
/** The `deprioritize` predicate (#982), so other rankers apply the same lever. */
|
|
348
|
+
getDeprioritizedPathMatcher() {
|
|
349
|
+
return this.isDeprioritizedPath;
|
|
350
|
+
}
|
|
260
351
|
// ===========================================================================
|
|
261
352
|
// Node Operations
|
|
262
353
|
// ===========================================================================
|
|
@@ -718,10 +809,16 @@ class QueryBuilder {
|
|
|
718
809
|
const uniqueIds = [...new Set(ids)];
|
|
719
810
|
for (let i = 0; i < uniqueIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
720
811
|
const chunk = uniqueIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
812
|
+
// Every edge insert checks its endpoints here, a chunk at a time: the
|
|
813
|
+
// full-size statement is prepared once, the final partial chunk ad hoc.
|
|
814
|
+
let stmt;
|
|
815
|
+
if (chunk.length === SQLITE_PARAM_CHUNK_SIZE) {
|
|
816
|
+
stmt = this.stmts.existingNodeIdsFull ??= this.db.prepare(`SELECT id FROM nodes WHERE id IN (${new Array(SQLITE_PARAM_CHUNK_SIZE).fill('?').join(',')})`);
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
stmt = this.db.prepare(`SELECT id FROM nodes WHERE id IN (${chunk.map(() => '?').join(',')})`);
|
|
820
|
+
}
|
|
821
|
+
const rows = stmt.all(...chunk);
|
|
725
822
|
for (const row of rows) {
|
|
726
823
|
out.add(row.id);
|
|
727
824
|
}
|
|
@@ -747,16 +844,58 @@ class QueryBuilder {
|
|
|
747
844
|
clearCache() {
|
|
748
845
|
this.nodeCache.clear();
|
|
749
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Whether any node in `filePath` is exported — `getNodesByFile(f).some((n) =>
|
|
849
|
+
* n.isExported)` as one indexed probe, without decoding the file's nodes.
|
|
850
|
+
*/
|
|
851
|
+
fileHasExportedNode(filePath) {
|
|
852
|
+
if (!this.stmts.fileHasExportedNode) {
|
|
853
|
+
this.stmts.fileHasExportedNode = this.db.prepare('SELECT 1 FROM nodes WHERE file_path = ? AND is_exported = 1 LIMIT 1');
|
|
854
|
+
}
|
|
855
|
+
return this.stmts.fileHasExportedNode.get(filePath) !== undefined;
|
|
856
|
+
}
|
|
857
|
+
/** The exported nodes of a file, in {@link getNodesByFile} order, decoding only those rows. */
|
|
858
|
+
getExportedNodesByFile(filePath) {
|
|
859
|
+
if (!this.stmts.getExportedNodesByFile) {
|
|
860
|
+
this.stmts.getExportedNodesByFile = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? AND is_exported = 1 ORDER BY start_line, id');
|
|
861
|
+
}
|
|
862
|
+
return this.stmts.getExportedNodesByFile.all(filePath).map(rowToNode);
|
|
863
|
+
}
|
|
864
|
+
/** The nodes of a file with one name, in {@link getNodesByFile} order, decoding only those rows. */
|
|
865
|
+
getNodesByFileAndName(filePath, name) {
|
|
866
|
+
if (!this.stmts.getNodesByFileAndName) {
|
|
867
|
+
this.stmts.getNodesByFileAndName = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? AND name = ? ORDER BY start_line, id');
|
|
868
|
+
}
|
|
869
|
+
return this.stmts.getNodesByFileAndName.all(filePath, name).map(rowToNode);
|
|
870
|
+
}
|
|
750
871
|
/**
|
|
751
872
|
* Get all nodes in a file
|
|
752
873
|
*/
|
|
753
874
|
getNodesByFile(filePath) {
|
|
754
875
|
if (!this.stmts.getNodesByFile) {
|
|
755
|
-
this.stmts.getNodesByFile = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line');
|
|
876
|
+
this.stmts.getNodesByFile = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line, id');
|
|
756
877
|
}
|
|
757
878
|
const rows = this.stmts.getNodesByFile.all(filePath);
|
|
758
879
|
return rows.map(rowToNode);
|
|
759
880
|
}
|
|
881
|
+
/**
|
|
882
|
+
* Get all nodes in several files at once — one chunked `IN` query rather than
|
|
883
|
+
* one {@link getNodesByFile} per file (#1975).
|
|
884
|
+
*/
|
|
885
|
+
getNodesByFiles(filePaths) {
|
|
886
|
+
const unique = [...new Set(filePaths)];
|
|
887
|
+
const out = [];
|
|
888
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
889
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
890
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
891
|
+
const rows = this.db
|
|
892
|
+
.prepare(`SELECT * FROM nodes WHERE file_path IN (${placeholders})`)
|
|
893
|
+
.all(...chunk);
|
|
894
|
+
for (const row of rows)
|
|
895
|
+
out.push(rowToNode(row));
|
|
896
|
+
}
|
|
897
|
+
return out;
|
|
898
|
+
}
|
|
760
899
|
/**
|
|
761
900
|
* Find the file that holds the densest concentration of the project's
|
|
762
901
|
* internal call graph — the "core" file. Used by context-builder to
|
|
@@ -774,8 +913,46 @@ class QueryBuilder {
|
|
|
774
913
|
* Excludes test/spec files from candidacy via path-pattern. The agent's
|
|
775
914
|
* typical question is "how does X work", not "how is X tested", so
|
|
776
915
|
* boosting a test file's directory would be a misfire.
|
|
916
|
+
*
|
|
917
|
+
* The answer depends only on the graph, never on the query, and the
|
|
918
|
+
* aggregation behind it scans every edge — seconds on a large index, paid
|
|
919
|
+
* by every generic explore (#1864). So it is memoized per database change
|
|
920
|
+
* stamp: recomputed only after something wrote to the database.
|
|
777
921
|
*/
|
|
778
922
|
getDominantFile() {
|
|
923
|
+
// total_changes() counts writes that are later rolled back, so a result
|
|
924
|
+
// read inside a transaction could outlive a ROLLBACK under an unchanged
|
|
925
|
+
// stamp. Never keep one; `undefined` (a runtime without the getter) is
|
|
926
|
+
// treated the same way.
|
|
927
|
+
if (this.db.inTransaction !== false) {
|
|
928
|
+
this.dominantFileMemo = undefined;
|
|
929
|
+
return this.computeDominantFile();
|
|
930
|
+
}
|
|
931
|
+
const stamp = this.getChangeStamp();
|
|
932
|
+
if (this.dominantFileMemo?.stamp === stamp)
|
|
933
|
+
return this.dominantFileMemo.value;
|
|
934
|
+
const value = this.computeDominantFile();
|
|
935
|
+
this.dominantFileMemo = { stamp, value };
|
|
936
|
+
return value;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* A value that differs whenever the database content may have changed
|
|
940
|
+
* since the last call, whoever changed it: `total_changes()` counts the
|
|
941
|
+
* rows this connection inserted, updated or deleted, and
|
|
942
|
+
* `PRAGMA data_version` moves when any OTHER connection — another process's
|
|
943
|
+
* sync, a CLI `lattice sensor index` beside a running MCP server — commits. Both
|
|
944
|
+
* are O(1), so no write path has to remember to invalidate anything.
|
|
945
|
+
* Coarse on purpose: any write, not just one to nodes/edges, forces a
|
|
946
|
+
* recompute, which only costs time, never a stale answer.
|
|
947
|
+
*/
|
|
948
|
+
getChangeStamp() {
|
|
949
|
+
if (!this.stmts.getChangeStamp) {
|
|
950
|
+
this.stmts.getChangeStamp = this.db.prepare('SELECT total_changes() AS changes, (SELECT data_version FROM pragma_data_version) AS version');
|
|
951
|
+
}
|
|
952
|
+
const row = this.stmts.getChangeStamp.get();
|
|
953
|
+
return `${row.changes}:${row.version}`;
|
|
954
|
+
}
|
|
955
|
+
computeDominantFile() {
|
|
779
956
|
if (!this.stmts.getDominantFile) {
|
|
780
957
|
// Pull top 20 candidates; we then filter out test/generated files
|
|
781
958
|
// in code (regex-grade matching that SQL LIKE can't express). The
|
|
@@ -796,7 +973,8 @@ class QueryBuilder {
|
|
|
796
973
|
`);
|
|
797
974
|
}
|
|
798
975
|
const rows = this.stmts.getDominantFile.all();
|
|
799
|
-
const
|
|
976
|
+
const generated = this.getGeneratedPathsAmong(rows.map(r => r.file_path));
|
|
977
|
+
const filtered = rows.filter(r => !isLowValueFile(r.file_path, generated));
|
|
800
978
|
if (filtered.length === 0 || filtered[0].edge_count < 20)
|
|
801
979
|
return null;
|
|
802
980
|
return {
|
|
@@ -829,7 +1007,8 @@ class QueryBuilder {
|
|
|
829
1007
|
`);
|
|
830
1008
|
}
|
|
831
1009
|
const rows = this.stmts.getTopRouteFile.all();
|
|
832
|
-
const
|
|
1010
|
+
const generated = this.getGeneratedPathsAmong(rows.map(r => r.file_path));
|
|
1011
|
+
const filtered = rows.filter(r => !isLowValueFile(r.file_path, generated));
|
|
833
1012
|
if (filtered.length === 0)
|
|
834
1013
|
return null;
|
|
835
1014
|
const totalRoutes = filtered.reduce((sum, r) => sum + r.cnt, 0);
|
|
@@ -859,6 +1038,9 @@ class QueryBuilder {
|
|
|
859
1038
|
this.stmts.getRoutingManifest = this.db.prepare(`
|
|
860
1039
|
SELECT
|
|
861
1040
|
r.name AS url,
|
|
1041
|
+
r.id AS route_id,
|
|
1042
|
+
r.file_path AS route_file,
|
|
1043
|
+
r.start_line AS route_line,
|
|
862
1044
|
h.name AS handler,
|
|
863
1045
|
h.file_path AS handler_file,
|
|
864
1046
|
h.start_line AS handler_line,
|
|
@@ -868,14 +1050,15 @@ class QueryBuilder {
|
|
|
868
1050
|
JOIN nodes h ON e.target = h.id
|
|
869
1051
|
WHERE r.kind = 'route'
|
|
870
1052
|
AND e.kind IN ('references', 'calls')
|
|
871
|
-
AND h.kind IN ('function', 'method', 'class')
|
|
1053
|
+
AND h.kind IN ('function', 'method', 'class', 'constant', 'variable')
|
|
872
1054
|
ORDER BY r.file_path, r.start_line
|
|
873
1055
|
LIMIT ?
|
|
874
1056
|
`);
|
|
875
1057
|
}
|
|
876
1058
|
const rows = this.stmts.getRoutingManifest.all(limit);
|
|
877
1059
|
// Drop test/generated handlers — same hygiene as elsewhere.
|
|
878
|
-
const
|
|
1060
|
+
const generated = this.getGeneratedPathsAmong(rows.map(r => r.handler_file));
|
|
1061
|
+
const filtered = rows.filter(r => !isLowValueFile(r.handler_file, generated));
|
|
879
1062
|
if (filtered.length < 3)
|
|
880
1063
|
return null;
|
|
881
1064
|
// Identify the file holding the most handlers (the "primary handler file").
|
|
@@ -898,6 +1081,9 @@ class QueryBuilder {
|
|
|
898
1081
|
handlerFile: r.handler_file,
|
|
899
1082
|
handlerLine: r.handler_line,
|
|
900
1083
|
handlerKind: r.handler_kind,
|
|
1084
|
+
routeId: r.route_id,
|
|
1085
|
+
routeFile: r.route_file,
|
|
1086
|
+
routeLine: r.route_line,
|
|
901
1087
|
})),
|
|
902
1088
|
topHandlerFile,
|
|
903
1089
|
topHandlerFileCount,
|
|
@@ -909,7 +1095,7 @@ class QueryBuilder {
|
|
|
909
1095
|
*/
|
|
910
1096
|
getNodesByKind(kind) {
|
|
911
1097
|
if (!this.stmts.getNodesByKind) {
|
|
912
|
-
this.stmts.getNodesByKind = this.db.prepare('SELECT * FROM nodes WHERE kind = ?');
|
|
1098
|
+
this.stmts.getNodesByKind = this.db.prepare('SELECT * FROM nodes WHERE kind = ? ORDER BY file_path, start_line, id');
|
|
913
1099
|
}
|
|
914
1100
|
const rows = this.stmts.getNodesByKind.all(kind);
|
|
915
1101
|
return rows.map(rowToNode);
|
|
@@ -924,11 +1110,29 @@ class QueryBuilder {
|
|
|
924
1110
|
*iterateNodesByKind(kind) {
|
|
925
1111
|
// Fresh statement per call (not a cached one): an iterator holds an open
|
|
926
1112
|
// cursor, so a shared statement would conflict across overlapping scans.
|
|
927
|
-
|
|
1113
|
+
// Synthesis uses first/last-match precedence and caps: insertion order
|
|
1114
|
+
// changes on sync. idx_nodes_kind streams this canonical order without
|
|
1115
|
+
// materializing/sorting all of a large project's methods in memory.
|
|
1116
|
+
const stmt = this.db.prepare('SELECT * FROM nodes WHERE kind = ? ORDER BY file_path, start_line, id');
|
|
928
1117
|
for (const row of stmt.iterate(kind)) {
|
|
929
1118
|
yield rowToNode(row);
|
|
930
1119
|
}
|
|
931
1120
|
}
|
|
1121
|
+
/**
|
|
1122
|
+
* iterateNodesByKind narrowed to some languages, in the same canonical
|
|
1123
|
+
* order — the ORDER BY is total (`id` is unique), so this yields exactly the
|
|
1124
|
+
* nodes a caller filtering iterateNodesByKind by language would keep, in the
|
|
1125
|
+
* same sequence. A Go pass on a TypeScript monorepo otherwise materialized
|
|
1126
|
+
* every method in the project to find a couple of Go ones.
|
|
1127
|
+
*/
|
|
1128
|
+
*iterateNodesByKindIn(kind, languages) {
|
|
1129
|
+
if (languages.length === 0)
|
|
1130
|
+
return;
|
|
1131
|
+
const stmt = this.db.prepare(`SELECT * FROM nodes WHERE kind = ? AND language IN (${languages.map(() => '?').join(', ')}) ORDER BY file_path, start_line, id`);
|
|
1132
|
+
for (const row of stmt.iterate(kind, ...languages)) {
|
|
1133
|
+
yield rowToNode(row);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
932
1136
|
/**
|
|
933
1137
|
* Get all nodes in the database
|
|
934
1138
|
*/
|
|
@@ -948,7 +1152,7 @@ class QueryBuilder {
|
|
|
948
1152
|
*iterateNodesByLanguageWithDecorator(language, decorator) {
|
|
949
1153
|
// Fresh statement per call — an iterator holds an open cursor (see
|
|
950
1154
|
// iterateNodesByKind).
|
|
951
|
-
const stmt = this.db.prepare("SELECT * FROM nodes WHERE language = ? AND decorators LIKE '%' || ? || '%'");
|
|
1155
|
+
const stmt = this.db.prepare("SELECT * FROM nodes WHERE language = ? AND decorators LIKE '%' || ? || '%' ORDER BY file_path, start_line, id");
|
|
952
1156
|
for (const row of stmt.iterate(language, `"${decorator}"`)) {
|
|
953
1157
|
yield rowToNode(row);
|
|
954
1158
|
}
|
|
@@ -964,11 +1168,26 @@ class QueryBuilder {
|
|
|
964
1168
|
return new Set(rows.map((r) => r.language));
|
|
965
1169
|
}
|
|
966
1170
|
/**
|
|
967
|
-
* Get nodes by exact name match (uses idx_nodes_name index)
|
|
1171
|
+
* Get nodes by exact name match (uses idx_nodes_name index).
|
|
1172
|
+
*
|
|
1173
|
+
* This is resolution's candidate list, and the ORDER BY is load-bearing for
|
|
1174
|
+
* index correctness, not cosmetic (CG-33). When a reference names a symbol
|
|
1175
|
+
* that several files define and nothing disambiguates them, resolution binds
|
|
1176
|
+
* to the first candidate — so without an ORDER BY the winner was decided by
|
|
1177
|
+
* rowid, i.e. by the order files happened to be WRITTEN. A full index writes
|
|
1178
|
+
* them in scan order; an incremental sync appends each file as it changes, so
|
|
1179
|
+
* the same tree resolved to different edges depending on how the index was
|
|
1180
|
+
* built, and a long-lived synced index drifted away from a rebuild of itself
|
|
1181
|
+
* (measured at 4.3% of distinct edges, mostly `calls`).
|
|
1182
|
+
*
|
|
1183
|
+
* `(file_path, start_line)` is a property of the CODE, so both paths now pick
|
|
1184
|
+
* the same candidate. The sort is paid once per distinct name per resolution
|
|
1185
|
+
* run — ReferenceResolver memoizes this in its nameCache — and the population
|
|
1186
|
+
* is capped by AMBIGUOUS_NAME_CEILING (#999).
|
|
968
1187
|
*/
|
|
969
1188
|
getNodesByName(name) {
|
|
970
1189
|
if (!this.stmts.getNodesByName) {
|
|
971
|
-
this.stmts.getNodesByName = this.db.prepare('SELECT * FROM nodes WHERE name = ?');
|
|
1190
|
+
this.stmts.getNodesByName = this.db.prepare('SELECT * FROM nodes WHERE name = ? ORDER BY file_path, start_line, id');
|
|
972
1191
|
}
|
|
973
1192
|
const rows = this.stmts.getNodesByName.all(name);
|
|
974
1193
|
return rows.map(rowToNode);
|
|
@@ -984,24 +1203,43 @@ class QueryBuilder {
|
|
|
984
1203
|
const rows = this.stmts.getNodesByNamePrefix.all(prefix, prefix + '', limit);
|
|
985
1204
|
return rows.map(rowToNode);
|
|
986
1205
|
}
|
|
1206
|
+
/** File nodes whose basename starts with `prefix`, without a result cap. */
|
|
1207
|
+
getFileNodesByNamePrefix(prefix) {
|
|
1208
|
+
if (!this.stmts.getFileNodesByNamePrefix) {
|
|
1209
|
+
this.stmts.getFileNodesByNamePrefix = this.db.prepare("SELECT * FROM nodes WHERE kind = 'file' AND name >= ? AND name < ? ORDER BY name");
|
|
1210
|
+
}
|
|
1211
|
+
const rows = this.stmts.getFileNodesByNamePrefix.all(prefix, prefix + '');
|
|
1212
|
+
return rows.map(rowToNode);
|
|
1213
|
+
}
|
|
987
1214
|
/**
|
|
988
1215
|
* Get nodes by exact qualified name match (uses idx_nodes_qualified_name index)
|
|
989
1216
|
*/
|
|
990
1217
|
getNodesByQualifiedNameExact(qualifiedName) {
|
|
991
1218
|
if (!this.stmts.getNodesByQualifiedNameExact) {
|
|
992
|
-
this.stmts.getNodesByQualifiedNameExact = this.db.prepare('SELECT * FROM nodes WHERE qualified_name = ?');
|
|
1219
|
+
this.stmts.getNodesByQualifiedNameExact = this.db.prepare('SELECT * FROM nodes WHERE qualified_name = ? ORDER BY file_path, start_line, id');
|
|
993
1220
|
}
|
|
994
1221
|
const rows = this.stmts.getNodesByQualifiedNameExact.all(qualifiedName);
|
|
995
1222
|
return rows.map(rowToNode);
|
|
996
1223
|
}
|
|
997
1224
|
/**
|
|
998
|
-
* Get nodes by
|
|
1225
|
+
* Get nodes by name, case-insensitively (seeks the idx_nodes_lower_name
|
|
1226
|
+
* expression index).
|
|
1227
|
+
*
|
|
1228
|
+
* The parameter is lowered in SQL rather than trusted to arrive lowered, so
|
|
1229
|
+
* the lookup means the same thing whatever casing a caller hands it. Written
|
|
1230
|
+
* as a bare `lower(name) = ?` it silently returned nothing for any input
|
|
1231
|
+
* carrying an uppercase letter, and — because SQLite's `lower()` folds ASCII
|
|
1232
|
+
* only while JavaScript's `.toLowerCase()` folds Unicode — a caller that
|
|
1233
|
+
* pre-lowered in JavaScript could not match a non-ASCII name at all.
|
|
1234
|
+
*
|
|
1235
|
+
* Note this hardens the query, not its one caller: `matchFuzzy` still lowers
|
|
1236
|
+
* in JavaScript before calling, so the non-ASCII gap remains open there.
|
|
999
1237
|
*/
|
|
1000
|
-
getNodesByLowerName(
|
|
1238
|
+
getNodesByLowerName(name) {
|
|
1001
1239
|
if (!this.stmts.getNodesByLowerName) {
|
|
1002
|
-
this.stmts.getNodesByLowerName = this.db.prepare('SELECT * FROM nodes WHERE lower(name) = ?');
|
|
1240
|
+
this.stmts.getNodesByLowerName = this.db.prepare('SELECT * FROM nodes WHERE lower(name) = lower(?)');
|
|
1003
1241
|
}
|
|
1004
|
-
const rows = this.stmts.getNodesByLowerName.all(
|
|
1242
|
+
const rows = this.stmts.getNodesByLowerName.all(name);
|
|
1005
1243
|
return rows.map(rowToNode);
|
|
1006
1244
|
}
|
|
1007
1245
|
/**
|
|
@@ -1034,9 +1272,9 @@ class QueryBuilder {
|
|
|
1034
1272
|
const text = parsed.text;
|
|
1035
1273
|
const kinds = mergedKinds;
|
|
1036
1274
|
const languages = mergedLanguages;
|
|
1037
|
-
// First try FTS5 with prefix matching
|
|
1275
|
+
// First try FTS5 with prefix matching (skip if FTS5 not available, #1532)
|
|
1038
1276
|
let results = text
|
|
1039
|
-
? this.searchNodesFTS(text, { kinds, languages, limit, offset })
|
|
1277
|
+
? (this._fts5Available !== false ? this.searchNodesFTS(text, { kinds, languages, limit, offset }) : [])
|
|
1040
1278
|
// Over-fetch by 5× when running filter-only (no text). The
|
|
1041
1279
|
// post-scoring path: + name: filters can be very selective, so
|
|
1042
1280
|
// a smaller multiplier risks returning fewer than `limit`
|
|
@@ -1059,12 +1297,25 @@ class QueryBuilder {
|
|
|
1059
1297
|
// pushing them past the FTS fetch limit before post-hoc scoring can help.
|
|
1060
1298
|
// Use the max BM25 score as the base so the nameMatchBonus (exact=30 vs
|
|
1061
1299
|
// prefix=20) actually differentiates them after rescoring.
|
|
1300
|
+
//
|
|
1301
|
+
// Whole-name equality MUST be written as `lower(name) = lower(?)` so it
|
|
1302
|
+
// seeks `idx_nodes_lower_name`. The equivalent `name = ? COLLATE NOCASE`
|
|
1303
|
+
// matches no index — `idx_nodes_name` is BINARY-collated and the expression
|
|
1304
|
+
// index only matches the same expression — and degrades to a full table
|
|
1305
|
+
// scan. The `LIMIT 20` does not rescue it: SQLite can only stop early once
|
|
1306
|
+
// it has produced 20 rows, and this runs once per query term, most of which
|
|
1307
|
+
// name nothing in the corpus. Measured per term on an unmatched term:
|
|
1308
|
+
// 0.08ms on gin (2.5k nodes), 0.39ms on excalidraw (11k), 2.4ms on django
|
|
1309
|
+
// (62k) — and growing with the corpus, where the seek is flat at ~0.002ms.
|
|
1310
|
+
// Lowering the parameter in SQL rather than in JS is deliberate: SQLite's
|
|
1311
|
+
// `lower()` and NOCASE both fold ASCII only, while JS `.toLowerCase()`
|
|
1312
|
+
// folds Unicode, which would silently stop matching non-ASCII names.
|
|
1062
1313
|
if (results.length > 0 && query) {
|
|
1063
1314
|
const existingIds = new Set(results.map(r => r.node.id));
|
|
1064
1315
|
const maxFtsScore = Math.max(...results.map(r => r.score));
|
|
1065
1316
|
const terms = query.split(/\s+/).filter(t => t.length >= 2);
|
|
1066
1317
|
for (const term of terms) {
|
|
1067
|
-
let sql = 'SELECT * FROM nodes WHERE name = ?
|
|
1318
|
+
let sql = 'SELECT * FROM nodes WHERE lower(name) = lower(?)';
|
|
1068
1319
|
const params = [term];
|
|
1069
1320
|
if (kinds && kinds.length > 0) {
|
|
1070
1321
|
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
@@ -1087,13 +1338,24 @@ class QueryBuilder {
|
|
|
1087
1338
|
// Apply multi-signal scoring
|
|
1088
1339
|
if (results.length > 0 && (text || query)) {
|
|
1089
1340
|
const scoringQuery = text || query;
|
|
1090
|
-
results = results.map(r =>
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1341
|
+
results = results.map(r => {
|
|
1342
|
+
// A path the project de-prioritized is saying its symbol NAMES are not
|
|
1343
|
+
// the answer, so the exact-name bonus has to be damped too. The -15 path
|
|
1344
|
+
// penalty alone cannot do it: the bonus is additive and larger (measured
|
|
1345
|
+
// on #982's repro, a `usage()` helper sat at 74.8 vs 51.2 for the top
|
|
1346
|
+
// product symbol — -15 lands at 59.8, still ahead). Damped, not zeroed,
|
|
1347
|
+
// so the tree stays findable when it genuinely is what you asked for.
|
|
1348
|
+
// Evaluated once and reused: the predicate stats the config file.
|
|
1349
|
+
const deprioritized = this.isDeprioritizedPath?.(r.node.filePath) ?? false;
|
|
1350
|
+
const nameBonus = (0, query_utils_1.nameMatchBonus)(r.node.name, scoringQuery);
|
|
1351
|
+
return {
|
|
1352
|
+
...r,
|
|
1353
|
+
score: r.score
|
|
1354
|
+
+ (0, query_utils_1.kindBonus)(r.node.kind)
|
|
1355
|
+
+ (0, query_utils_1.scorePathRelevance)(r.node.filePath, scoringQuery, this.projectNameTokens, deprioritized)
|
|
1356
|
+
+ (deprioritized ? Math.round(nameBonus * exports.DEPRIORITIZED_NAME_BONUS_SCALE) : nameBonus),
|
|
1357
|
+
};
|
|
1358
|
+
});
|
|
1097
1359
|
results.sort((a, b) => b.score - a.score);
|
|
1098
1360
|
// Trim to requested limit after rescoring
|
|
1099
1361
|
if (results.length > limit) {
|
|
@@ -1204,6 +1466,46 @@ class QueryBuilder {
|
|
|
1204
1466
|
}
|
|
1205
1467
|
return results;
|
|
1206
1468
|
}
|
|
1469
|
+
/** Bounded miss diagnostics, independent of relevance filters and ranking. */
|
|
1470
|
+
getExploreMissDiagnostics(query) {
|
|
1471
|
+
const words = [...new Set((query.match(/[\p{L}\p{N}]+/gu) ?? []).map(w => w.toLowerCase()))];
|
|
1472
|
+
const checked = words.filter(w => w.length <= 64).slice(0, 16);
|
|
1473
|
+
const limited = checked.length !== words.length;
|
|
1474
|
+
if (checked.length === 0)
|
|
1475
|
+
return { matched: [], unmatched: [], candidates: [], limited };
|
|
1476
|
+
// EXISTS uses FTS postings and the segment primary key, never source scans.
|
|
1477
|
+
// Vocab rows can outlive deleted definitions, so verify them against nodes.
|
|
1478
|
+
const rows = this.db.prepare(`
|
|
1479
|
+
WITH words(word, pattern) AS (VALUES ${checked.map(() => '(?, ?)').join(', ')})
|
|
1480
|
+
SELECT word, (
|
|
1481
|
+
EXISTS (SELECT 1 FROM nodes_fts WHERE nodes_fts MATCH pattern)
|
|
1482
|
+
OR EXISTS (
|
|
1483
|
+
SELECT 1 FROM name_segment_vocab v WHERE v.segment = word
|
|
1484
|
+
AND EXISTS (SELECT 1 FROM nodes n WHERE n.name = v.name AND n.kind NOT IN ('file', 'import'))
|
|
1485
|
+
)
|
|
1486
|
+
) AS matched FROM words
|
|
1487
|
+
`).all(...checked.flatMap(w => [w, `{name qualified_name signature docstring} : "${w}"*`]));
|
|
1488
|
+
const names = this.db.prepare(`
|
|
1489
|
+
SELECT name FROM (
|
|
1490
|
+
SELECT v.name FROM name_segment_vocab v
|
|
1491
|
+
WHERE v.segment IN (${checked.map(() => '?').join(', ')})
|
|
1492
|
+
AND EXISTS (SELECT 1 FROM nodes n WHERE n.name = v.name AND n.kind NOT IN ('file', 'import'))
|
|
1493
|
+
LIMIT 12
|
|
1494
|
+
)
|
|
1495
|
+
UNION ALL
|
|
1496
|
+
SELECT name FROM (
|
|
1497
|
+
SELECT n.name FROM nodes_fts JOIN nodes n ON n.rowid = nodes_fts.rowid
|
|
1498
|
+
WHERE nodes_fts MATCH ? AND n.kind NOT IN ('file', 'import')
|
|
1499
|
+
LIMIT 12
|
|
1500
|
+
)
|
|
1501
|
+
`).all(...checked, `name : (${checked.map(w => `"${w}"*`).join(' OR ')})`);
|
|
1502
|
+
return {
|
|
1503
|
+
matched: rows.filter(r => r.matched).map(r => r.word),
|
|
1504
|
+
unmatched: rows.filter(r => !r.matched).map(r => r.word),
|
|
1505
|
+
candidates: [...new Set(names.map(r => r.name))].slice(0, 12),
|
|
1506
|
+
limited,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1207
1509
|
/**
|
|
1208
1510
|
* FTS5 search with prefix matching
|
|
1209
1511
|
*/
|
|
@@ -1332,9 +1634,16 @@ class QueryBuilder {
|
|
|
1332
1634
|
// Pass 1: Find which files contain distinctive (rare) symbols from the query.
|
|
1333
1635
|
// Pass 2: Query each name, boosting results that co-locate with distinctive symbols.
|
|
1334
1636
|
// Pass 1: Find files containing each queried name, identify distinctive names
|
|
1637
|
+
//
|
|
1638
|
+
// Both passes spell whole-name equality as `lower(name) = lower(?)` so they
|
|
1639
|
+
// seek `idx_nodes_lower_name` — see the note in `searchNodes` for why the
|
|
1640
|
+
// `name = ? COLLATE NOCASE` form full-scans instead. This path is the one
|
|
1641
|
+
// that hurts most: it runs both passes for every symbol extracted from the
|
|
1642
|
+
// query, and extraction is generous, so most of those names are absent from
|
|
1643
|
+
// the corpus and never reach either LIMIT.
|
|
1335
1644
|
const nameToFiles = new Map();
|
|
1336
1645
|
for (const name of names) {
|
|
1337
|
-
let sql = 'SELECT DISTINCT file_path FROM nodes WHERE name
|
|
1646
|
+
let sql = 'SELECT DISTINCT file_path FROM nodes WHERE lower(name) = lower(?)';
|
|
1338
1647
|
const params = [name];
|
|
1339
1648
|
if (kinds && kinds.length > 0) {
|
|
1340
1649
|
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
@@ -1360,7 +1669,7 @@ class QueryBuilder {
|
|
|
1360
1669
|
let sql = `
|
|
1361
1670
|
SELECT nodes.*, 1.0 as score
|
|
1362
1671
|
FROM nodes
|
|
1363
|
-
WHERE name
|
|
1672
|
+
WHERE lower(name) = lower(?)
|
|
1364
1673
|
`;
|
|
1365
1674
|
const params = [name];
|
|
1366
1675
|
if (kinds && kinds.length > 0) {
|
|
@@ -1434,6 +1743,30 @@ class QueryBuilder {
|
|
|
1434
1743
|
// ===========================================================================
|
|
1435
1744
|
// Edge Operations
|
|
1436
1745
|
// ===========================================================================
|
|
1746
|
+
/** Must run before file replacement/deletion cascades the endpoint edges. */
|
|
1747
|
+
hasSynthesizedEdgesTouchingFile(filePath) {
|
|
1748
|
+
const owned = "CASE WHEN json_valid(e.metadata) THEN json_extract(e.metadata, '$.synthesizedBy') END IS NOT NULL";
|
|
1749
|
+
for (const endpoint of ['source', 'target']) {
|
|
1750
|
+
if (this.db.prepare(`SELECT 1 FROM nodes n JOIN edges e ON e.${endpoint} = n.id
|
|
1751
|
+
WHERE n.file_path = ? AND ${owned} LIMIT 1`).get(filePath))
|
|
1752
|
+
return true;
|
|
1753
|
+
}
|
|
1754
|
+
// Wiring often lives in a third file, with neither endpoint in it.
|
|
1755
|
+
return !!this.db.prepare(`SELECT 1 FROM edges e WHERE ${owned}
|
|
1756
|
+
AND CASE WHEN json_valid(e.metadata) THEN json_extract(e.metadata, '$.registeredAt') END >= ?
|
|
1757
|
+
AND CASE WHEN json_valid(e.metadata) THEN json_extract(e.metadata, '$.registeredAt') END < ? LIMIT 1`).get(`${filePath}:`, `${filePath};`);
|
|
1758
|
+
}
|
|
1759
|
+
wasSynthesisInput(filePath) {
|
|
1760
|
+
return !!this.db.prepare('SELECT 1 FROM synthesis_inputs WHERE file_path = ?').get(filePath);
|
|
1761
|
+
}
|
|
1762
|
+
replaceSynthesisInputs(files) {
|
|
1763
|
+
this.db.transaction(() => {
|
|
1764
|
+
this.db.exec('DELETE FROM synthesis_inputs');
|
|
1765
|
+
const insert = this.db.prepare('INSERT INTO synthesis_inputs(file_path) VALUES (?)');
|
|
1766
|
+
for (const file of files)
|
|
1767
|
+
insert.run(file);
|
|
1768
|
+
})();
|
|
1769
|
+
}
|
|
1437
1770
|
/**
|
|
1438
1771
|
* Insert a new edge
|
|
1439
1772
|
*/
|
|
@@ -1499,7 +1832,8 @@ class QueryBuilder {
|
|
|
1499
1832
|
this.stmts.deleteEdgesBySource.run(sourceId);
|
|
1500
1833
|
}
|
|
1501
1834
|
/**
|
|
1502
|
-
* Get outgoing edges from a node
|
|
1835
|
+
* Get outgoing edges from a node. Preserve the source/kind index order
|
|
1836
|
+
* (calls before imports/references), then break ties deterministically.
|
|
1503
1837
|
*/
|
|
1504
1838
|
getOutgoingEdges(sourceId, kinds, provenance) {
|
|
1505
1839
|
if ((kinds && kinds.length > 0) || provenance) {
|
|
@@ -1513,30 +1847,649 @@ class QueryBuilder {
|
|
|
1513
1847
|
sql += ' AND provenance = ?';
|
|
1514
1848
|
params.push(provenance);
|
|
1515
1849
|
}
|
|
1516
|
-
|
|
1850
|
+
sql += ' ORDER BY kind, target, line, col';
|
|
1851
|
+
const rows = this.edgeKindStmt(sql).all(...params);
|
|
1517
1852
|
return rows.map(rowToEdge);
|
|
1518
1853
|
}
|
|
1519
1854
|
if (!this.stmts.getEdgesBySource) {
|
|
1520
|
-
this.stmts.getEdgesBySource = this.db.prepare('SELECT * FROM edges WHERE source = ?');
|
|
1855
|
+
this.stmts.getEdgesBySource = this.db.prepare('SELECT * FROM edges WHERE source = ? ORDER BY kind, target, line, col');
|
|
1521
1856
|
}
|
|
1522
1857
|
const rows = this.stmts.getEdgesBySource.all(sourceId);
|
|
1523
1858
|
return rows.map(rowToEdge);
|
|
1524
1859
|
}
|
|
1525
1860
|
/**
|
|
1526
|
-
* Get incoming edges to a node
|
|
1861
|
+
* Get incoming edges to a node. Kind must precede opaque source IDs:
|
|
1862
|
+
* file IDs sort before function IDs, so source-first ordering lets imports
|
|
1863
|
+
* displace actual calls in capped caller lists. Keep deterministic ties
|
|
1864
|
+
* without changing the target/kind index's established kind precedence.
|
|
1527
1865
|
*/
|
|
1528
1866
|
getIncomingEdges(targetId, kinds) {
|
|
1529
1867
|
if (kinds && kinds.length > 0) {
|
|
1530
|
-
const sql = `SELECT * FROM edges WHERE target = ? AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1531
|
-
const rows = this.
|
|
1868
|
+
const sql = `SELECT * FROM edges WHERE target = ? AND kind IN (${kinds.map(() => '?').join(',')}) ORDER BY kind, source, line, col`;
|
|
1869
|
+
const rows = this.edgeKindStmt(sql).all(targetId, ...kinds);
|
|
1532
1870
|
return rows.map(rowToEdge);
|
|
1533
1871
|
}
|
|
1534
1872
|
if (!this.stmts.getEdgesByTarget) {
|
|
1535
|
-
this.stmts.getEdgesByTarget = this.db.prepare('SELECT * FROM edges WHERE target = ?');
|
|
1873
|
+
this.stmts.getEdgesByTarget = this.db.prepare('SELECT * FROM edges WHERE target = ? ORDER BY kind, source, line, col');
|
|
1536
1874
|
}
|
|
1537
1875
|
const rows = this.stmts.getEdgesByTarget.all(targetId);
|
|
1538
1876
|
return rows.map(rowToEdge);
|
|
1539
1877
|
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Outgoing edges for MANY source nodes in one query.
|
|
1880
|
+
*
|
|
1881
|
+
* The batch form of {@link getOutgoingEdges}. Building a nested outline needs
|
|
1882
|
+
* the `contains` edges of every container in a file at once; doing that one
|
|
1883
|
+
* source at a time is a query per symbol on files that have hundreds.
|
|
1884
|
+
*/
|
|
1885
|
+
getOutgoingEdgesFrom(sourceIds, kinds) {
|
|
1886
|
+
if (sourceIds.length === 0)
|
|
1887
|
+
return [];
|
|
1888
|
+
const unique = [...new Set(sourceIds)];
|
|
1889
|
+
const out = [];
|
|
1890
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
1891
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
1892
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
1893
|
+
let sql = `SELECT * FROM edges WHERE source IN (${placeholders})`;
|
|
1894
|
+
const params = [...chunk];
|
|
1895
|
+
if (kinds && kinds.length > 0) {
|
|
1896
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1897
|
+
params.push(...kinds);
|
|
1898
|
+
}
|
|
1899
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1900
|
+
for (const row of rows)
|
|
1901
|
+
out.push(rowToEdge(row));
|
|
1902
|
+
}
|
|
1903
|
+
return out;
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Fan-in (total incoming edge count) for MANY nodes in one query.
|
|
1907
|
+
*
|
|
1908
|
+
* The per-node alternative — `getIncomingEdges(id).length` — is an indexed
|
|
1909
|
+
* lookup each, but a symbol screen rendering a couple of hundred callees
|
|
1910
|
+
* would issue a couple of hundred of them. Ids with no incoming edges are
|
|
1911
|
+
* absent from the map rather than present as 0, so callers can tell "no
|
|
1912
|
+
* edges" from "not asked about".
|
|
1913
|
+
*/
|
|
1914
|
+
countIncomingEdges(ids) {
|
|
1915
|
+
const out = new Map();
|
|
1916
|
+
if (ids.length === 0)
|
|
1917
|
+
return out;
|
|
1918
|
+
const unique = [...new Set(ids)];
|
|
1919
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
1920
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
1921
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
1922
|
+
const rows = this.db
|
|
1923
|
+
.prepare(
|
|
1924
|
+
// A test's request onto a route (tier-synthesizer's `test-request`)
|
|
1925
|
+
// is not a production caller: forty tests hitting one endpoint must
|
|
1926
|
+
// not make it a hub the Steps walk refuses to enter.
|
|
1927
|
+
`SELECT target, COUNT(*) AS count FROM edges WHERE target IN (${placeholders})
|
|
1928
|
+
AND (metadata IS NULL OR metadata NOT LIKE '%"synthesizedBy":"test-request"%') GROUP BY target`)
|
|
1929
|
+
.all(...chunk);
|
|
1930
|
+
for (const row of rows)
|
|
1931
|
+
out.set(row.target, row.count);
|
|
1932
|
+
}
|
|
1933
|
+
return out;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Incoming edges for MANY target nodes in one query — the mirror of
|
|
1937
|
+
* {@link getOutgoingEdgesFrom}. Needed wherever a whole file's inbound edges
|
|
1938
|
+
* are wanted at once ("which files import anything in this one?").
|
|
1939
|
+
*/
|
|
1940
|
+
getIncomingEdgesTo(targetIds, kinds) {
|
|
1941
|
+
if (targetIds.length === 0)
|
|
1942
|
+
return [];
|
|
1943
|
+
const unique = [...new Set(targetIds)];
|
|
1944
|
+
const out = [];
|
|
1945
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
1946
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
1947
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
1948
|
+
let sql = `SELECT * FROM edges WHERE target IN (${placeholders})`;
|
|
1949
|
+
const params = [...chunk];
|
|
1950
|
+
if (kinds && kinds.length > 0) {
|
|
1951
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1952
|
+
params.push(...kinds);
|
|
1953
|
+
}
|
|
1954
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1955
|
+
for (const row of rows)
|
|
1956
|
+
out.push(rowToEdge(row));
|
|
1957
|
+
}
|
|
1958
|
+
return out;
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Fan-out (total outgoing edge count) for MANY nodes in one query — the
|
|
1962
|
+
* mirror of {@link countIncomingEdges}. Ids with no outgoing edges are absent
|
|
1963
|
+
* from the map rather than present as 0.
|
|
1964
|
+
*/
|
|
1965
|
+
countOutgoingEdges(ids) {
|
|
1966
|
+
const out = new Map();
|
|
1967
|
+
if (ids.length === 0)
|
|
1968
|
+
return out;
|
|
1969
|
+
const unique = [...new Set(ids)];
|
|
1970
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
1971
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
1972
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
1973
|
+
const rows = this.db
|
|
1974
|
+
.prepare(`SELECT source, COUNT(*) AS count FROM edges WHERE source IN (${placeholders}) GROUP BY source`)
|
|
1975
|
+
.all(...chunk);
|
|
1976
|
+
for (const row of rows)
|
|
1977
|
+
out.set(row.source, row.count);
|
|
1978
|
+
}
|
|
1979
|
+
return out;
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Symbols nothing in the index points at — the candidate set behind the dead
|
|
1983
|
+
* code list (`src/graph/dead-code.ts`).
|
|
1984
|
+
*
|
|
1985
|
+
* "Points at" is every edge kind EXCEPT `contains`: a class containing a
|
|
1986
|
+
* method is structure, not use, and counting it would make every member look
|
|
1987
|
+
* reached by its own container. A self-edge is excluded for the same reason
|
|
1988
|
+
* a recursive function is not its own caller.
|
|
1989
|
+
*
|
|
1990
|
+
* One scan, one index probe per candidate. `NOT EXISTS` over
|
|
1991
|
+
* `idx_edges_target_kind` is what keeps it that way — the alternative
|
|
1992
|
+
* (`LEFT JOIN edges … GROUP BY`) builds a row per edge for the whole table
|
|
1993
|
+
* before discarding all but the empty groups. Ordered by position so the
|
|
1994
|
+
* answer is stable across runs and groups by file without a second sort.
|
|
1995
|
+
*
|
|
1996
|
+
* The result is deliberately NOT called dead code: an unreferenced symbol is
|
|
1997
|
+
* a symbol with no STATIC reference, and the caller applies the exclusions
|
|
1998
|
+
* (tests, generated files, overrides, unresolved names) that turn the
|
|
1999
|
+
* candidate set into a claim worth making.
|
|
2000
|
+
*/
|
|
2001
|
+
getUnreferencedNodes(kinds, limit) {
|
|
2002
|
+
if (kinds.length === 0 || limit <= 0)
|
|
2003
|
+
return [];
|
|
2004
|
+
const placeholders = kinds.map(() => '?').join(',');
|
|
2005
|
+
const rows = this.db
|
|
2006
|
+
.prepare(`SELECT n.*, COALESCE(f.generated, 0) AS file_generated
|
|
2007
|
+
FROM nodes n
|
|
2008
|
+
LEFT JOIN files f ON f.path = n.file_path
|
|
2009
|
+
WHERE n.kind IN (${placeholders})
|
|
2010
|
+
AND NOT EXISTS (
|
|
2011
|
+
SELECT 1 FROM edges e
|
|
2012
|
+
WHERE e.target = n.id
|
|
2013
|
+
AND e.kind != 'contains'
|
|
2014
|
+
AND e.source != n.id
|
|
2015
|
+
)
|
|
2016
|
+
ORDER BY n.file_path, n.start_line, n.name
|
|
2017
|
+
LIMIT ?`)
|
|
2018
|
+
.all(...kinds, limit);
|
|
2019
|
+
return rows.map((row) => ({ node: rowToNode(row), generated: row.file_generated === 1 }));
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Which of `names` the index holds an UNRESOLVED reference to.
|
|
2023
|
+
*
|
|
2024
|
+
* The point is honesty about our own blind spots. A `failed` row in
|
|
2025
|
+
* `unresolved_refs` records that some file referenced a name and the resolver
|
|
2026
|
+
* could not decide what it meant — so a symbol with that name cannot be
|
|
2027
|
+
* called unreferenced, whatever the edge table says. It is deliberately
|
|
2028
|
+
* matched loosely, on the reference name AND on its tail (`util.greet` →
|
|
2029
|
+
* `greet`), because the question being asked is "could this name be the one
|
|
2030
|
+
* we failed to follow", and a maybe has to count as a yes.
|
|
2031
|
+
*
|
|
2032
|
+
* Bounded-lookup like {@link getGeneratedPathsAmong}: the caller holds a
|
|
2033
|
+
* candidate list, so this is a chunked probe over `idx_unresolved_name`, not
|
|
2034
|
+
* a scan of the table.
|
|
2035
|
+
*/
|
|
2036
|
+
getUnresolvedNamesAmong(names) {
|
|
2037
|
+
const unique = [...new Set(names)].filter((name) => name.length > 0);
|
|
2038
|
+
const found = new Set();
|
|
2039
|
+
if (unique.length === 0)
|
|
2040
|
+
return found;
|
|
2041
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2042
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2043
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2044
|
+
const rows = this.db
|
|
2045
|
+
.prepare(`SELECT DISTINCT reference_name AS name FROM unresolved_refs
|
|
2046
|
+
WHERE reference_name IN (${placeholders})
|
|
2047
|
+
UNION
|
|
2048
|
+
SELECT DISTINCT name_tail AS name FROM unresolved_refs
|
|
2049
|
+
WHERE name_tail IN (${placeholders})`)
|
|
2050
|
+
.all(...chunk, ...chunk);
|
|
2051
|
+
for (const row of rows)
|
|
2052
|
+
found.add(row.name);
|
|
2053
|
+
}
|
|
2054
|
+
return found;
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Which of `nodeIds` extend or implement a type the resolver could not follow.
|
|
2058
|
+
* An ancestor outside the index (`React.Component`, `stream.Transform`, a
|
|
2059
|
+
* framework interface) leaves no edge, only this `extends` / `implements`
|
|
2060
|
+
* row, so it is the one record that such an ancestor exists (#1973).
|
|
2061
|
+
* Chunked probe over `idx_unresolved_from_node`.
|
|
2062
|
+
*/
|
|
2063
|
+
getUnresolvedSupertypeSourcesAmong(nodeIds) {
|
|
2064
|
+
const unique = [...new Set(nodeIds)];
|
|
2065
|
+
const found = new Set();
|
|
2066
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2067
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2068
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2069
|
+
const rows = this.db
|
|
2070
|
+
.prepare(`SELECT DISTINCT from_node_id AS id FROM unresolved_refs
|
|
2071
|
+
WHERE from_node_id IN (${placeholders})
|
|
2072
|
+
AND reference_kind IN ('extends', 'implements')`)
|
|
2073
|
+
.all(...chunk);
|
|
2074
|
+
for (const row of rows)
|
|
2075
|
+
found.add(row.id);
|
|
2076
|
+
}
|
|
2077
|
+
return found;
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Which of `names` are carried by MORE THAN ONE symbol, at least one of which
|
|
2081
|
+
* something points at.
|
|
2082
|
+
*
|
|
2083
|
+
* The false positive this exists to kill: `LatticeSensor.getTopRouteFile` calls
|
|
2084
|
+
* `this.queries.getTopRouteFile()`, and the resolver — which prefers a
|
|
2085
|
+
* same-name definition in the call site's own file — attaches that edge to
|
|
2086
|
+
* the *calling* method. One of the two ends up with a self-edge and the other
|
|
2087
|
+
* with nothing at all, and neither is unreferenced. From the edge table the
|
|
2088
|
+
* mis-resolution and a genuinely unused twin are the same picture, so the
|
|
2089
|
+
* claim is not made about either.
|
|
2090
|
+
*
|
|
2091
|
+
* Both halves of the condition are load-bearing. **More than one symbol**:
|
|
2092
|
+
* a uniquely-named function that only calls itself is genuinely dead, and
|
|
2093
|
+
* excluding every recursive function would gut the list. **Self-edges
|
|
2094
|
+
* counted**: the self-edge IS the fingerprint of the mis-resolution above, so
|
|
2095
|
+
* it has to count as evidence that this name resolves somewhere.
|
|
2096
|
+
*
|
|
2097
|
+
* Chunked probe over `idx_nodes_name`, bounded by the caller's candidate list.
|
|
2098
|
+
*/
|
|
2099
|
+
getAmbiguousReferencedNames(names) {
|
|
2100
|
+
const unique = [...new Set(names)].filter((name) => name.length > 0);
|
|
2101
|
+
const found = new Set();
|
|
2102
|
+
if (unique.length === 0)
|
|
2103
|
+
return found;
|
|
2104
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2105
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2106
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2107
|
+
const rows = this.db
|
|
2108
|
+
.prepare(`SELECT name FROM (
|
|
2109
|
+
SELECT n.name AS name,
|
|
2110
|
+
EXISTS (
|
|
2111
|
+
SELECT 1 FROM edges e
|
|
2112
|
+
WHERE e.target = n.id AND e.kind != 'contains'
|
|
2113
|
+
) AS referenced
|
|
2114
|
+
FROM nodes n
|
|
2115
|
+
WHERE n.name IN (${placeholders})
|
|
2116
|
+
)
|
|
2117
|
+
GROUP BY name
|
|
2118
|
+
HAVING COUNT(*) > 1 AND SUM(referenced) > 0`)
|
|
2119
|
+
.all(...chunk);
|
|
2120
|
+
for (const row of rows)
|
|
2121
|
+
found.add(row.name);
|
|
2122
|
+
}
|
|
2123
|
+
return found;
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Which of the given languages the index records an EXPORT marker for.
|
|
2127
|
+
*
|
|
2128
|
+
* A self-measurement, and the honest basis for a whole class of exclusion.
|
|
2129
|
+
* The dead code report's strongest filter is "exported symbols may be reached
|
|
2130
|
+
* from outside this repository" — and that filter silently does nothing for a
|
|
2131
|
+
* language whose exports are not recorded, either because the extractor does
|
|
2132
|
+
* not record them (Rust `pub`) or because the language has no such concept at
|
|
2133
|
+
* all (Python, C, Ruby: the header or the module IS the surface). Rather than
|
|
2134
|
+
* carry a table of which is which, ask the index: if nothing in this language
|
|
2135
|
+
* is marked exported, the filter did not run, and no claim about outside
|
|
2136
|
+
* reachability can be made for it.
|
|
2137
|
+
*
|
|
2138
|
+
* `idx_nodes_language` covers the grouping; the caller passes the handful of
|
|
2139
|
+
* languages its candidates are actually in.
|
|
2140
|
+
*/
|
|
2141
|
+
getLanguagesWithExports(languages) {
|
|
2142
|
+
const unique = [...new Set(languages)].filter((language) => language.length > 0);
|
|
2143
|
+
const found = new Set();
|
|
2144
|
+
if (unique.length === 0)
|
|
2145
|
+
return found;
|
|
2146
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2147
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2148
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2149
|
+
const rows = this.db
|
|
2150
|
+
.prepare(`SELECT language, MAX(is_exported) AS any_exported
|
|
2151
|
+
FROM nodes
|
|
2152
|
+
WHERE language IN (${placeholders})
|
|
2153
|
+
GROUP BY language`)
|
|
2154
|
+
.all(...chunk);
|
|
2155
|
+
for (const row of rows)
|
|
2156
|
+
if (row.any_exported === 1)
|
|
2157
|
+
found.add(row.language);
|
|
2158
|
+
}
|
|
2159
|
+
return found;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* The nodes with the most DISTINCT dependents, most first.
|
|
2163
|
+
*
|
|
2164
|
+
* "Distinct" is the difference that matters: a helper called forty times from
|
|
2165
|
+
* one function has a fan-in of 40 but exactly one dependent. This counts the
|
|
2166
|
+
* second thing — the number a reader means by "N callers" — so the top of
|
|
2167
|
+
* this list is the set of symbols a change actually radiates furthest from.
|
|
2168
|
+
*
|
|
2169
|
+
* `contains` is excluded because it is structure, not dependency: counting it
|
|
2170
|
+
* would rank every file and class above the code they hold.
|
|
2171
|
+
*/
|
|
2172
|
+
getTopDependedOn(limit) {
|
|
2173
|
+
if (limit <= 0)
|
|
2174
|
+
return [];
|
|
2175
|
+
const rows = this.db
|
|
2176
|
+
.prepare(`SELECT target AS nodeId, COUNT(DISTINCT source) AS dependents
|
|
2177
|
+
FROM edges
|
|
2178
|
+
WHERE kind != 'contains' AND source != target
|
|
2179
|
+
GROUP BY target
|
|
2180
|
+
ORDER BY dependents DESC
|
|
2181
|
+
LIMIT ?`)
|
|
2182
|
+
.all(limit);
|
|
2183
|
+
return rows;
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* The graph's executable roots — files that RUN something at module level,
|
|
2187
|
+
* ranked by how much of the project they set in motion.
|
|
2188
|
+
*
|
|
2189
|
+
* The engine records a statement at the top level of a file as an edge from
|
|
2190
|
+
* the *file* node, so `src/bin/lattice-sensor.ts` calling `program.parse()` at
|
|
2191
|
+
* module scope is a `calls` edge out of a `file`. That set is what makes the
|
|
2192
|
+
* roots of a dependency graph visible: a library module holds definitions and
|
|
2193
|
+
* runs nothing until someone imports it, while a CLI, a worker entry or a
|
|
2194
|
+
* build script does its work on the way down the file. `instantiates` counts
|
|
2195
|
+
* the same way — `new Server(...)` at module scope is the same act.
|
|
2196
|
+
*
|
|
2197
|
+
* A call made while initializing a module-level `variable` / `constant` —
|
|
2198
|
+
* `const service = new Service()`, `app = FastAPI()` — is attributed to the
|
|
2199
|
+
* declared name (#693), not to the file, so the file's own edges alone would
|
|
2200
|
+
* miss most of what a real entry point runs. Those names are the file's
|
|
2201
|
+
* top-level code too, so `tops` counts them alongside the file node.
|
|
2202
|
+
*
|
|
2203
|
+
* Ranking multiplies the two things an entry point does: it runs (calls), and
|
|
2204
|
+
* it wires the project together (distinct other files its symbols reach). One
|
|
2205
|
+
* alone is misleading — a registration table makes hundreds of module-level
|
|
2206
|
+
* calls into itself, and a barrel file imports everything and runs nothing.
|
|
2207
|
+
* The product puts the file that does both at the top.
|
|
2208
|
+
*/
|
|
2209
|
+
getTopCallingFiles(limit) {
|
|
2210
|
+
if (limit <= 0)
|
|
2211
|
+
return [];
|
|
2212
|
+
return this.db
|
|
2213
|
+
.prepare(`WITH tops AS (
|
|
2214
|
+
SELECT n.id AS file_id, n.id AS src
|
|
2215
|
+
FROM nodes n
|
|
2216
|
+
WHERE n.kind = 'file'
|
|
2217
|
+
UNION ALL
|
|
2218
|
+
SELECT c.source AS file_id, c.target AS src
|
|
2219
|
+
FROM edges c
|
|
2220
|
+
JOIN nodes f ON f.id = c.source
|
|
2221
|
+
JOIN nodes v ON v.id = c.target
|
|
2222
|
+
WHERE c.kind = 'contains'
|
|
2223
|
+
AND f.kind = 'file'
|
|
2224
|
+
AND v.kind IN ('variable', 'constant')
|
|
2225
|
+
),
|
|
2226
|
+
runs AS (
|
|
2227
|
+
SELECT t.file_id AS id, COUNT(*) AS calls
|
|
2228
|
+
FROM tops t
|
|
2229
|
+
JOIN edges e ON e.source = t.src
|
|
2230
|
+
WHERE e.kind IN ('calls', 'instantiates')
|
|
2231
|
+
GROUP BY t.file_id
|
|
2232
|
+
),
|
|
2233
|
+
cand AS (
|
|
2234
|
+
SELECT r.id AS id, n.file_path AS fp, r.calls AS calls
|
|
2235
|
+
FROM runs r JOIN nodes n ON n.id = r.id
|
|
2236
|
+
),
|
|
2237
|
+
wires AS (
|
|
2238
|
+
SELECT sn.file_path AS fp, COUNT(DISTINCT tn.file_path) AS reaches
|
|
2239
|
+
FROM edges e
|
|
2240
|
+
JOIN nodes sn ON sn.id = e.source
|
|
2241
|
+
JOIN nodes tn ON tn.id = e.target
|
|
2242
|
+
WHERE e.kind != 'contains'
|
|
2243
|
+
AND sn.file_path <> tn.file_path
|
|
2244
|
+
AND sn.file_path IN (SELECT fp FROM cand)
|
|
2245
|
+
GROUP BY sn.file_path
|
|
2246
|
+
)
|
|
2247
|
+
SELECT c.id AS nodeId,
|
|
2248
|
+
c.fp AS filePath,
|
|
2249
|
+
c.calls AS calls,
|
|
2250
|
+
COALESCE(w.reaches, 0) AS reaches,
|
|
2251
|
+
c.calls * (1 + COALESCE(w.reaches, 0)) AS score
|
|
2252
|
+
FROM cand c LEFT JOIN wires w ON w.fp = c.fp
|
|
2253
|
+
ORDER BY score DESC, calls DESC, filePath
|
|
2254
|
+
LIMIT ?`)
|
|
2255
|
+
.all(limit);
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* How many OTHER files depend on each of the given files.
|
|
2259
|
+
*
|
|
2260
|
+
* Counted through the symbols, not the file nodes: an `imports` edge points
|
|
2261
|
+
* at the imported symbol, so a file node almost never receives one and
|
|
2262
|
+
* counting edges into it would report every file as depended on by nobody.
|
|
2263
|
+
* Same-file edges are excluded, which is what makes zero mean "nothing else
|
|
2264
|
+
* in the index reaches into this file" — the honest reading of a root.
|
|
2265
|
+
*/
|
|
2266
|
+
getFileDependentCounts(filePaths) {
|
|
2267
|
+
if (filePaths.length === 0)
|
|
2268
|
+
return [];
|
|
2269
|
+
return this.db
|
|
2270
|
+
.prepare(`SELECT tn.file_path AS filePath, COUNT(DISTINCT sn.file_path) AS dependents
|
|
2271
|
+
FROM edges e
|
|
2272
|
+
JOIN nodes tn ON tn.id = e.target
|
|
2273
|
+
JOIN nodes sn ON sn.id = e.source
|
|
2274
|
+
WHERE e.kind != 'contains'
|
|
2275
|
+
AND tn.file_path IN (SELECT value FROM json_each(?))
|
|
2276
|
+
AND sn.file_path <> tn.file_path
|
|
2277
|
+
GROUP BY tn.file_path`)
|
|
2278
|
+
.all(JSON.stringify(filePaths));
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* How far each of the given files reaches OUT: distinct other files its
|
|
2282
|
+
* symbols touch, and how many references that is.
|
|
2283
|
+
*
|
|
2284
|
+
* The mirror of {@link getFileDependentCounts}, and the same reasoning about
|
|
2285
|
+
* `contains` and same-file edges applies. It is driven from `nodes` rather
|
|
2286
|
+
* than from `edges` so the work is proportional to the files asked about —
|
|
2287
|
+
* the entry-points endpoint asks it about every test file in the index, and
|
|
2288
|
+
* an edge-first plan would scan the whole table to answer a question about a
|
|
2289
|
+
* tenth of it.
|
|
2290
|
+
*/
|
|
2291
|
+
getFileReachCounts(filePaths) {
|
|
2292
|
+
if (filePaths.length === 0)
|
|
2293
|
+
return [];
|
|
2294
|
+
return this.db
|
|
2295
|
+
.prepare(`SELECT sn.file_path AS filePath,
|
|
2296
|
+
COUNT(DISTINCT tn.file_path) AS reaches,
|
|
2297
|
+
COUNT(*) AS refs
|
|
2298
|
+
FROM nodes sn
|
|
2299
|
+
JOIN edges e ON e.source = sn.id
|
|
2300
|
+
JOIN nodes tn ON tn.id = e.target
|
|
2301
|
+
WHERE sn.file_path IN (SELECT value FROM json_each(?))
|
|
2302
|
+
AND e.kind != 'contains'
|
|
2303
|
+
AND tn.file_path <> sn.file_path
|
|
2304
|
+
GROUP BY sn.file_path`)
|
|
2305
|
+
.all(JSON.stringify(filePaths));
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* The `file` nodes for the given paths, in one query.
|
|
2309
|
+
*
|
|
2310
|
+
* A file's own node is what makes a file row navigable, and looking it up
|
|
2311
|
+
* with {@link getNodesInFile} means materialising every symbol in the file to
|
|
2312
|
+
* throw all but one away.
|
|
2313
|
+
*/
|
|
2314
|
+
getFileNodes(filePaths) {
|
|
2315
|
+
if (filePaths.length === 0)
|
|
2316
|
+
return [];
|
|
2317
|
+
const rows = this.db
|
|
2318
|
+
.prepare(`SELECT * FROM nodes
|
|
2319
|
+
WHERE kind = 'file'
|
|
2320
|
+
AND file_path IN (SELECT value FROM json_each(?))`)
|
|
2321
|
+
.all(JSON.stringify(filePaths));
|
|
2322
|
+
return rows.map(rowToNode);
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Roll the whole edge table up to module granularity in one pass.
|
|
2326
|
+
*
|
|
2327
|
+
* The caller decides what a module IS — it hands in a file → module
|
|
2328
|
+
* assignment and gets back the cross-module traffic. That split is
|
|
2329
|
+
* deliberate: naming modules is a *policy* (top-level directories, a façade
|
|
2330
|
+
* file kept separate, a monorepo root) that belongs where the reader lives,
|
|
2331
|
+
* while grouping a million edges by it is *mechanics* that must happen in
|
|
2332
|
+
* SQLite. Doing the fold in JavaScript instead means materialising every
|
|
2333
|
+
* cross-file edge in memory; doing the naming in SQL means a tower of
|
|
2334
|
+
* `instr`/`substr` no one can read.
|
|
2335
|
+
*
|
|
2336
|
+
* The assignment lands in a TEMP table with a primary key, so the join is
|
|
2337
|
+
* indexed and the result set is bounded by modules², not by edges. Temp
|
|
2338
|
+
* tables live in SQLite's own temp database, so this stays valid against a
|
|
2339
|
+
* read-only main.
|
|
2340
|
+
*
|
|
2341
|
+
* Two result sets, because they need two different groupings over the same
|
|
2342
|
+
* join: `links` counts edges per (module, module, kind), and `pairs` names
|
|
2343
|
+
* the busiest symbol pairs behind each link (the map's tooltip). `pairs` is
|
|
2344
|
+
* ranked and cut inside SQLite — the un-cut grouping is the one thing here
|
|
2345
|
+
* that scales with distinct symbol names rather than with modules. Pairs are
|
|
2346
|
+
* ranked by `declared` before raw count, so a link's tooltip names the
|
|
2347
|
+
* symbols the source actually points at rather than whichever `has`/`get`
|
|
2348
|
+
* happened to name-match most often.
|
|
2349
|
+
*
|
|
2350
|
+
* `declared` is the subset of a link's edges that came from something the
|
|
2351
|
+
* source *writes down*: an import, a qualified name, an inheritance clause,
|
|
2352
|
+
* or a call through a typed receiver. It exists because bare name matching
|
|
2353
|
+
* (`resolvedBy: 'exact-match'`) is what invents cross-module links out of
|
|
2354
|
+
* common method names — `run`, `push`, `finish` — and a map that lets those
|
|
2355
|
+
* decide the layering puts the storage layer above the CLI.
|
|
2356
|
+
*/
|
|
2357
|
+
aggregateModuleGraph(assignments, options) {
|
|
2358
|
+
if (assignments.length === 0 || options.kinds.length === 0)
|
|
2359
|
+
return { links: [], pairs: [] };
|
|
2360
|
+
const CONFIDENCE = `COALESCE(json_extract(e.metadata, '$.confidence'), 1)`;
|
|
2361
|
+
const DECLARED = `(json_extract(e.metadata, '$.resolvedBy') IN ('import', 'qualified-name')
|
|
2362
|
+
OR e.kind IN ('extends', 'implements')
|
|
2363
|
+
OR (json_extract(e.metadata, '$.resolvedBy') = 'instance-method'
|
|
2364
|
+
AND ${CONFIDENCE} >= 0.9))`;
|
|
2365
|
+
this.db.exec('DROP TABLE IF EXISTS temp.cg_module_map');
|
|
2366
|
+
this.db.exec('CREATE TEMP TABLE cg_module_map (path TEXT PRIMARY KEY, mod TEXT NOT NULL)');
|
|
2367
|
+
try {
|
|
2368
|
+
const insert = this.db.prepare('INSERT OR REPLACE INTO cg_module_map (path, mod) VALUES (?, ?)');
|
|
2369
|
+
this.db.exec('BEGIN');
|
|
2370
|
+
try {
|
|
2371
|
+
for (const row of assignments)
|
|
2372
|
+
insert.run(row.filePath, row.module);
|
|
2373
|
+
this.db.exec('COMMIT');
|
|
2374
|
+
}
|
|
2375
|
+
catch (err) {
|
|
2376
|
+
this.db.exec('ROLLBACK');
|
|
2377
|
+
throw err;
|
|
2378
|
+
}
|
|
2379
|
+
// ONE pass over the edge table. Grouping by the symbol names as well as
|
|
2380
|
+
// the modules costs nothing extra in scan time — the join is what is
|
|
2381
|
+
// expensive — and it buys both results from a single scan. Measured on
|
|
2382
|
+
// this index inflated to 1.6M edges: 1.66s for this query against 3.0s
|
|
2383
|
+
// for the module-level and name-level queries run separately, which is
|
|
2384
|
+
// the difference between meeting and missing the map's cold budget on a
|
|
2385
|
+
// ten-thousand-file repository.
|
|
2386
|
+
const rows = this.db
|
|
2387
|
+
.prepare(`SELECT ms.mod AS source, mt.mod AS target, e.kind AS kind,
|
|
2388
|
+
sn.name AS "from", tn.name AS "to",
|
|
2389
|
+
SUM(CASE WHEN ${CONFIDENCE} >= ? THEN 1 ELSE 0 END) AS count,
|
|
2390
|
+
SUM(CASE WHEN ${CONFIDENCE} >= ? AND ${DECLARED} THEN 1 ELSE 0 END) AS declared,
|
|
2391
|
+
SUM(CASE WHEN ${CONFIDENCE} < ? THEN 1 ELSE 0 END) AS uncertain
|
|
2392
|
+
FROM edges e
|
|
2393
|
+
JOIN nodes sn ON sn.id = e.source
|
|
2394
|
+
JOIN nodes tn ON tn.id = e.target
|
|
2395
|
+
JOIN cg_module_map ms ON ms.path = sn.file_path
|
|
2396
|
+
JOIN cg_module_map mt ON mt.path = tn.file_path
|
|
2397
|
+
WHERE e.kind IN (SELECT value FROM json_each(?))
|
|
2398
|
+
AND ms.mod <> mt.mod
|
|
2399
|
+
GROUP BY ms.mod, mt.mod, e.kind, sn.name, tn.name`)
|
|
2400
|
+
.all(options.minConfidence, options.minConfidence, options.minConfidence, JSON.stringify(options.kinds));
|
|
2401
|
+
return foldModuleRows(rows, options);
|
|
2402
|
+
}
|
|
2403
|
+
finally {
|
|
2404
|
+
this.db.exec('DROP TABLE IF EXISTS temp.cg_module_map');
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Every ordered pair of files where one reaches into the other, once each.
|
|
2409
|
+
*
|
|
2410
|
+
* The input a cycle finder wants: file-level circular dependencies are the
|
|
2411
|
+
* strongly connected components of this graph. One query instead of the
|
|
2412
|
+
* dependency lookup per file that {@link GraphQueryManager.findCircularDependencies}
|
|
2413
|
+
* does — which matters because a cycle report is only interesting on a large
|
|
2414
|
+
* repo, and that is exactly where a query per file stops being affordable.
|
|
2415
|
+
*
|
|
2416
|
+
* `contains` is excluded (a file "contains" its own symbols, which is not a
|
|
2417
|
+
* dependency), and so are same-file edges and low-confidence name matches:
|
|
2418
|
+
* a cycle conjured by a common method name is a false alarm a reader cannot
|
|
2419
|
+
* check.
|
|
2420
|
+
*/
|
|
2421
|
+
getCrossFileDependencyPairs(minConfidence) {
|
|
2422
|
+
return this.db
|
|
2423
|
+
.prepare(`SELECT DISTINCT sn.file_path AS source, tn.file_path AS target
|
|
2424
|
+
FROM edges e
|
|
2425
|
+
JOIN nodes sn ON sn.id = e.source
|
|
2426
|
+
JOIN nodes tn ON tn.id = e.target
|
|
2427
|
+
WHERE e.kind <> 'contains'
|
|
2428
|
+
AND sn.file_path <> tn.file_path
|
|
2429
|
+
AND COALESCE(json_extract(e.metadata, '$.confidence'), 1) >= ?`)
|
|
2430
|
+
.all(minConfidence);
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Every unresolved reference recorded in one FILE, ordered by line.
|
|
2434
|
+
*
|
|
2435
|
+
* The per-symbol form above answers "what does this body reach that the
|
|
2436
|
+
* index does not hold". A whole-file reader asks the same question of every
|
|
2437
|
+
* line at once, and asking it one symbol at a time is a query per symbol —
|
|
2438
|
+
* 153 of them on this repo's largest file. `unresolved_refs.file_path` is
|
|
2439
|
+
* indexed, so this is one lookup whatever the file holds.
|
|
2440
|
+
*
|
|
2441
|
+
* `limit` bounds the answer rather than the work: the caller draws a marker
|
|
2442
|
+
* per row, and a generated file with fifty thousand of them would ship
|
|
2443
|
+
* megabytes to say something a count already says. Rows come back in line
|
|
2444
|
+
* order, so a cap trims the END of the file, which is at least legible.
|
|
2445
|
+
*/
|
|
2446
|
+
getUnresolvedReferencesInFile(filePath, limit = 5000) {
|
|
2447
|
+
if (!this.stmts.getUnresolvedInFile) {
|
|
2448
|
+
this.stmts.getUnresolvedInFile = this.db.prepare('SELECT * FROM unresolved_refs WHERE file_path = ? ORDER BY line, col LIMIT ?');
|
|
2449
|
+
}
|
|
2450
|
+
const rows = this.stmts.getUnresolvedInFile.all(filePath, limit);
|
|
2451
|
+
return rows.map((row) => ({
|
|
2452
|
+
fromNodeId: row.from_node_id,
|
|
2453
|
+
referenceName: row.reference_name,
|
|
2454
|
+
referenceKind: row.reference_kind,
|
|
2455
|
+
line: row.line,
|
|
2456
|
+
column: row.col,
|
|
2457
|
+
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
2458
|
+
filePath: row.file_path,
|
|
2459
|
+
language: row.language,
|
|
2460
|
+
rowId: row.id,
|
|
2461
|
+
}));
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* References recorded against a symbol that never resolved to a node — the
|
|
2465
|
+
* calls and type mentions that leave the index (a third-party package, a
|
|
2466
|
+
* runtime builtin, a language construct extraction doesn't model).
|
|
2467
|
+
*
|
|
2468
|
+
* Read-only. It exists so a reader can say "N calls into symbols outside the
|
|
2469
|
+
* index" instead of silently showing a callee list shorter than the body's
|
|
2470
|
+
* call sites, which reads as "nothing else happens here".
|
|
2471
|
+
*/
|
|
2472
|
+
getUnresolvedReferencesFrom(fromNodeId) {
|
|
2473
|
+
if (!this.stmts.getUnresolvedFromNode) {
|
|
2474
|
+
// Insertion (extraction) order, stated: without ORDER BY the row order
|
|
2475
|
+
// follows whichever index the planner picks — (from_node_id,
|
|
2476
|
+
// reference_name) returns name order — and readers that stable-sort by
|
|
2477
|
+
// position then break ties differently from build to build.
|
|
2478
|
+
this.stmts.getUnresolvedFromNode = this.db.prepare('SELECT * FROM unresolved_refs WHERE from_node_id = ? ORDER BY id');
|
|
2479
|
+
}
|
|
2480
|
+
const rows = this.stmts.getUnresolvedFromNode.all(fromNodeId);
|
|
2481
|
+
return rows.map((row) => ({
|
|
2482
|
+
fromNodeId: row.from_node_id,
|
|
2483
|
+
referenceName: row.reference_name,
|
|
2484
|
+
referenceKind: row.reference_kind,
|
|
2485
|
+
line: row.line,
|
|
2486
|
+
column: row.col,
|
|
2487
|
+
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
2488
|
+
filePath: row.file_path,
|
|
2489
|
+
language: row.language,
|
|
2490
|
+
rowId: row.id,
|
|
2491
|
+
}));
|
|
2492
|
+
}
|
|
1540
2493
|
/**
|
|
1541
2494
|
* Find all edges where both source and target are in the given node set.
|
|
1542
2495
|
* Useful for recovering inter-node connectivity after BFS.
|
|
@@ -1647,8 +2600,8 @@ class QueryBuilder {
|
|
|
1647
2600
|
upsertFile(file) {
|
|
1648
2601
|
if (!this.stmts.upsertFile) {
|
|
1649
2602
|
this.stmts.upsertFile = this.db.prepare(`
|
|
1650
|
-
INSERT INTO files (path, content_hash, language, size, modified_at, indexed_at, node_count, extraction_version, errors)
|
|
1651
|
-
VALUES (@path, @contentHash, @language, @size, @modifiedAt, @indexedAt, @nodeCount, @extractionVersion, @errors)
|
|
2603
|
+
INSERT INTO files (path, content_hash, language, size, modified_at, indexed_at, node_count, extraction_version, errors, generated)
|
|
2604
|
+
VALUES (@path, @contentHash, @language, @size, @modifiedAt, @indexedAt, @nodeCount, @extractionVersion, @errors, @generated)
|
|
1652
2605
|
ON CONFLICT(path) DO UPDATE SET
|
|
1653
2606
|
content_hash = @contentHash,
|
|
1654
2607
|
language = @language,
|
|
@@ -1657,7 +2610,8 @@ class QueryBuilder {
|
|
|
1657
2610
|
indexed_at = @indexedAt,
|
|
1658
2611
|
node_count = @nodeCount,
|
|
1659
2612
|
extraction_version = @extractionVersion,
|
|
1660
|
-
errors = @errors
|
|
2613
|
+
errors = @errors,
|
|
2614
|
+
generated = @generated
|
|
1661
2615
|
`);
|
|
1662
2616
|
}
|
|
1663
2617
|
this.stmts.upsertFile.run({
|
|
@@ -1670,6 +2624,9 @@ class QueryBuilder {
|
|
|
1670
2624
|
nodeCount: file.nodeCount,
|
|
1671
2625
|
extractionVersion: file.extractionVersion,
|
|
1672
2626
|
errors: file.errors ? JSON.stringify(file.errors) : null,
|
|
2627
|
+
// The upsert always REWRITES the flag: a file that loses its banner in an
|
|
2628
|
+
// edit must lose the flag on the next sync, not keep a stale 1.
|
|
2629
|
+
generated: file.generated ? 1 : 0,
|
|
1673
2630
|
});
|
|
1674
2631
|
}
|
|
1675
2632
|
/**
|
|
@@ -1711,6 +2668,173 @@ class QueryBuilder {
|
|
|
1711
2668
|
.get(currentVersion);
|
|
1712
2669
|
return row.ahead;
|
|
1713
2670
|
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Which of `filePaths` the index flagged as tool-generated (schema v9+).
|
|
2673
|
+
*
|
|
2674
|
+
* Bounded-lookup by design: every consumer already holds a short candidate
|
|
2675
|
+
* list (a ranked file group, an FTS result page, a LIMIT-20 aggregate), so
|
|
2676
|
+
* this stays a partial-index probe over a handful of paths — no whole-repo
|
|
2677
|
+
* set to materialize, and no cache to invalidate, which means a ranking call
|
|
2678
|
+
* can never serve a verdict the last sync already replaced.
|
|
2679
|
+
*
|
|
2680
|
+
* Returns ONLY the content/index signal; callers union it with
|
|
2681
|
+
* {@link isGeneratedFile} so pre-v9 databases (column present, all zeros
|
|
2682
|
+
* until a re-index) keep the path-only behavior rather than regressing.
|
|
2683
|
+
*/
|
|
2684
|
+
getGeneratedPathsAmong(filePaths) {
|
|
2685
|
+
const unique = [...new Set(filePaths)];
|
|
2686
|
+
const found = new Set();
|
|
2687
|
+
if (unique.length === 0)
|
|
2688
|
+
return found;
|
|
2689
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2690
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2691
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2692
|
+
const rows = this.db
|
|
2693
|
+
.prepare(`SELECT path FROM files WHERE generated = 1 AND path IN (${placeholders})`)
|
|
2694
|
+
.all(...chunk);
|
|
2695
|
+
for (const row of rows)
|
|
2696
|
+
found.add(row.path);
|
|
2697
|
+
}
|
|
2698
|
+
return found;
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* A reusable `(path) => boolean` over a bounded candidate list, unioning the
|
|
2702
|
+
* indexed flag with the path convention. This is the shape every ranking
|
|
2703
|
+
* comparator wants: one query up front, then O(1) per comparison.
|
|
2704
|
+
*/
|
|
2705
|
+
generatedPredicateFor(filePaths) {
|
|
2706
|
+
const flagged = this.getGeneratedPathsAmong(filePaths);
|
|
2707
|
+
return (filePath) => flagged.has(filePath) || (0, generated_detection_1.isGeneratedFile)(filePath);
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Which of `filePaths` are AMBIENT DECLARATION files — they declare nothing
|
|
2711
|
+
* but types, and nothing in the index depends on them (CG-28). A hand-written
|
|
2712
|
+
* ambient `.d.ts` of global shims, a vendored typings file, module
|
|
2713
|
+
* augmentation: reachable only by name, structurally attached to nothing.
|
|
2714
|
+
*
|
|
2715
|
+
* Structural, not extension-based, so a hand-written `types.ts` and a `.d.ts`
|
|
2716
|
+
* are judged by the same rule and a `.d.ts` that does declare a class or a
|
|
2717
|
+
* const is (correctly) not caught. Four conditions, all required:
|
|
2718
|
+
*
|
|
2719
|
+
* 1. it declares at least one symbol — an empty or unparsed file is not a
|
|
2720
|
+
* declaration file, it is a file we know nothing about;
|
|
2721
|
+
* 2. EVERY declared symbol is a type-level kind (interface / type alias /
|
|
2722
|
+
* enum / namespace). The narrowness is deliberate and measured: a rule
|
|
2723
|
+
* of "no callables" alone flags 1–18% of a repo, including Kotlin sealed
|
|
2724
|
+
* classes, Rust `mod.rs` re-exports and django's locale constant tables —
|
|
2725
|
+
* real source that must not be demoted. This rule flags 0–4%;
|
|
2726
|
+
* 3. no symbol in it originates a `calls`/`instantiates` edge — the direct
|
|
2727
|
+
* evidence that nothing here has a body;
|
|
2728
|
+
* 4. NOTHING ELSE IN THE INDEX points at it. This is the condition that
|
|
2729
|
+
* separates an ambient shim from a working type module, and it is why
|
|
2730
|
+
* the flag is narrow enough to be safe: `displacement-ts`'s pipeline
|
|
2731
|
+
* `types.ts` passes 1–3 identically but carries 13 inbound imports and
|
|
2732
|
+
* 21 references, so the files that answer a query about the pipeline are
|
|
2733
|
+
* typed BY it — it is part of that answer's structure. An ambient
|
|
2734
|
+
* `declare global` shim has zero. Deliberately index-wide rather than
|
|
2735
|
+
* restricted to the candidate list: the file that imports it is usually
|
|
2736
|
+
* not itself a candidate.
|
|
2737
|
+
*
|
|
2738
|
+
* ### Interface MEMBERS are transparent to all four conditions
|
|
2739
|
+
*
|
|
2740
|
+
* A `method_signature` / `property_signature` inside an interface enters the
|
|
2741
|
+
* graph as a `method` / `property` node (#1638). Read literally that would
|
|
2742
|
+
* break every condition here at once: condition 2 sees non-type kinds and
|
|
2743
|
+
* stops flagging, and — worse, because it is silent — condition 4 starts
|
|
2744
|
+
* seeing inbound `calls` edges the moment a call site through the shim's API
|
|
2745
|
+
* finally has a signature to land on. An ambient `.d.ts` would quietly lose
|
|
2746
|
+
* its damping precisely BECAUSE the platform API it declares is widely used.
|
|
2747
|
+
*
|
|
2748
|
+
* So an interface-owned member is treated the way `parameter` already is: it
|
|
2749
|
+
* neither qualifies, disqualifies, nor counts as inbound dependency. That is
|
|
2750
|
+
* not a new judgement call, it is what keeps the rule measuring what it was
|
|
2751
|
+
* measured on — before #1638 these nodes did not exist, so excluding them
|
|
2752
|
+
* reproduces the 0–4% flag rate the thresholds above were tuned against. It
|
|
2753
|
+
* is also the semantically right answer: a signature with no body is on the
|
|
2754
|
+
* same side of the line as the interface that owns it, and a call edge
|
|
2755
|
+
* landing on one is still not a file that can answer a flow question.
|
|
2756
|
+
*
|
|
2757
|
+
* The interface ITSELF is untouched: the `references` edges an importing
|
|
2758
|
+
* module aims at `UploadStorage` still disqualify the file under (4), which
|
|
2759
|
+
* is what keeps a depended-on `types.ts` out of the flag.
|
|
2760
|
+
*
|
|
2761
|
+
* Bounded-lookup like {@link getGeneratedPathsAmong}: callers hold a ranked
|
|
2762
|
+
* candidate list, so this is a partial-index probe over a handful of paths.
|
|
2763
|
+
*/
|
|
2764
|
+
getAmbientDeclarationPathsAmong(filePaths) {
|
|
2765
|
+
const unique = [...new Set(filePaths)];
|
|
2766
|
+
const found = new Set();
|
|
2767
|
+
if (unique.length === 0)
|
|
2768
|
+
return found;
|
|
2769
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2770
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2771
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2772
|
+
// `file`/`import`/`export`/`parameter` are structural bookkeeping, not
|
|
2773
|
+
// things the file declares, so they neither qualify nor disqualify.
|
|
2774
|
+
const rows = this.db
|
|
2775
|
+
.prepare(`
|
|
2776
|
+
SELECT n.file_path AS file_path,
|
|
2777
|
+
SUM(CASE WHEN n.kind NOT IN ('file','import','export','parameter')
|
|
2778
|
+
AND NOT ${IS_INTERFACE_MEMBER('n')}
|
|
2779
|
+
THEN 1 ELSE 0 END) AS declared,
|
|
2780
|
+
SUM(CASE WHEN n.kind IN ('interface','type_alias','enum','enum_member','namespace')
|
|
2781
|
+
THEN 1 ELSE 0 END) AS typeDeclared
|
|
2782
|
+
FROM nodes n
|
|
2783
|
+
WHERE n.file_path IN (${placeholders})
|
|
2784
|
+
GROUP BY n.file_path
|
|
2785
|
+
`)
|
|
2786
|
+
.all(...chunk);
|
|
2787
|
+
let candidates = rows
|
|
2788
|
+
.filter((r) => r.declared > 0 && r.declared === r.typeDeclared)
|
|
2789
|
+
.map((r) => r.file_path);
|
|
2790
|
+
if (candidates.length === 0)
|
|
2791
|
+
continue;
|
|
2792
|
+
const disqualify = (sql) => {
|
|
2793
|
+
if (candidates.length === 0)
|
|
2794
|
+
return;
|
|
2795
|
+
const hit = new Set(this.db
|
|
2796
|
+
.prepare(sql.replace('$IN$', candidates.map(() => '?').join(',')))
|
|
2797
|
+
.all(...candidates).map((r) => r.file_path));
|
|
2798
|
+
candidates = candidates.filter((p) => !hit.has(p));
|
|
2799
|
+
};
|
|
2800
|
+
// (3) originates behaviour — a signature has no body to originate from,
|
|
2801
|
+
// so an edge attributed to one is not evidence about this file.
|
|
2802
|
+
disqualify(`
|
|
2803
|
+
SELECT DISTINCT n.file_path AS file_path
|
|
2804
|
+
FROM edges e JOIN nodes n ON n.id = e.source
|
|
2805
|
+
WHERE e.kind IN ('calls','instantiates') AND n.file_path IN ($IN$)
|
|
2806
|
+
AND NOT ${IS_INTERFACE_MEMBER('n')}
|
|
2807
|
+
`);
|
|
2808
|
+
// (4) something outside the file depends on it — but a call that lands on
|
|
2809
|
+
// an interface's own signature is a use of the API, not a dependency on
|
|
2810
|
+
// this file's structure. The edges aimed at the interface still count.
|
|
2811
|
+
disqualify(`
|
|
2812
|
+
SELECT DISTINCT t.file_path AS file_path
|
|
2813
|
+
FROM edges e JOIN nodes t ON t.id = e.target JOIN nodes s ON s.id = e.source
|
|
2814
|
+
WHERE t.file_path IN ($IN$) AND s.file_path <> t.file_path
|
|
2815
|
+
AND NOT ${IS_INTERFACE_MEMBER('t')}
|
|
2816
|
+
`);
|
|
2817
|
+
for (const path of candidates)
|
|
2818
|
+
found.add(path);
|
|
2819
|
+
}
|
|
2820
|
+
return found;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* A reusable `(path) => boolean` ambient-declaration test over a bounded
|
|
2824
|
+
* candidate list — the shape a ranking comparator wants: one query up front,
|
|
2825
|
+
* O(1) per comparison.
|
|
2826
|
+
*/
|
|
2827
|
+
ambientDeclarationPredicateFor(filePaths) {
|
|
2828
|
+
const flagged = this.getAmbientDeclarationPathsAmong(filePaths);
|
|
2829
|
+
return (filePath) => flagged.has(filePath);
|
|
2830
|
+
}
|
|
2831
|
+
/** How many indexed files carry the generated flag. Surfaced by `status`. */
|
|
2832
|
+
countGeneratedFiles() {
|
|
2833
|
+
const row = this.db
|
|
2834
|
+
.prepare('SELECT COUNT(*) AS n FROM files WHERE generated = 1')
|
|
2835
|
+
.get();
|
|
2836
|
+
return row?.n ?? 0;
|
|
2837
|
+
}
|
|
1714
2838
|
/**
|
|
1715
2839
|
* Delete a file record and its nodes
|
|
1716
2840
|
*/
|
|
@@ -1753,6 +2877,40 @@ class QueryBuilder {
|
|
|
1753
2877
|
.get();
|
|
1754
2878
|
return row?.last ?? null;
|
|
1755
2879
|
}
|
|
2880
|
+
/**
|
|
2881
|
+
* The index's revision marker: how far the last sync got, and how many files
|
|
2882
|
+
* it left behind — one query, both numbers.
|
|
2883
|
+
*
|
|
2884
|
+
* This is the cheapest honest answer to "has the index moved since I last
|
|
2885
|
+
* looked". `MAX(indexed_at)` alone is not enough: a sync that only DELETES
|
|
2886
|
+
* files (a branch checkout that removed a directory) advances nothing, and
|
|
2887
|
+
* the graph the viewer is showing has still changed underneath it. The row
|
|
2888
|
+
* count catches exactly that case.
|
|
2889
|
+
*/
|
|
2890
|
+
getIndexRevision() {
|
|
2891
|
+
const row = this.db
|
|
2892
|
+
.prepare('SELECT MAX(indexed_at) AS last, COUNT(*) AS files FROM files')
|
|
2893
|
+
.get();
|
|
2894
|
+
return { lastIndexedAt: row?.last ?? null, fileCount: row?.files ?? 0 };
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Files re-indexed strictly after `since` (ms since epoch), newest first.
|
|
2898
|
+
*
|
|
2899
|
+
* `total` is the real count; `paths` is capped at `limit`. Used by the
|
|
2900
|
+
* viewer's live channel to name what a sync just picked up. A file the same
|
|
2901
|
+
* sync DELETED cannot appear here — it has no row left — which is why the
|
|
2902
|
+
* caller compares {@link getIndexRevision} as well rather than treating an
|
|
2903
|
+
* empty list as "nothing happened".
|
|
2904
|
+
*/
|
|
2905
|
+
getFilesIndexedSince(since, limit) {
|
|
2906
|
+
const count = this.db
|
|
2907
|
+
.prepare('SELECT COUNT(*) AS n FROM files WHERE indexed_at > ?')
|
|
2908
|
+
.get(since);
|
|
2909
|
+
const rows = this.db
|
|
2910
|
+
.prepare('SELECT path FROM files WHERE indexed_at > ? ORDER BY indexed_at DESC, path LIMIT ?')
|
|
2911
|
+
.all(since, Math.max(0, limit));
|
|
2912
|
+
return { paths: rows.map((r) => r.path), total: count?.n ?? rows.length };
|
|
2913
|
+
}
|
|
1756
2914
|
/**
|
|
1757
2915
|
* Get files that need re-indexing (hash changed)
|
|
1758
2916
|
*/
|
|
@@ -1917,11 +3075,19 @@ class QueryBuilder {
|
|
|
1917
3075
|
* (§7a.2) — while the seek is O(batch) forever. `id` is the rowid alias, so
|
|
1918
3076
|
* the enumeration order is identical to the OFFSET reader's.
|
|
1919
3077
|
*/
|
|
1920
|
-
getUnresolvedReferencesBatchAfter(afterRowId, limit) {
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
3078
|
+
getUnresolvedReferencesBatchAfter(afterRowId, limit, prerequisites) {
|
|
3079
|
+
// Resolution prerequisites must be committed before dependent calls,
|
|
3080
|
+
// even when an interrupted sync queued their rows in a different order
|
|
3081
|
+
// from a clean index (#1577). Each phase still seeks by row id in bounded
|
|
3082
|
+
// memory; the default preserves the public reader's original enumeration.
|
|
3083
|
+
const key = prerequisites === undefined ? 'getUnresolvedBatchAfter'
|
|
3084
|
+
: prerequisites ? 'getUnresolvedPrerequisitesAfter' : 'getUnresolvedDependentsAfter';
|
|
3085
|
+
if (!this.stmts[key]) {
|
|
3086
|
+
const filter = prerequisites === undefined ? ''
|
|
3087
|
+
: ` AND reference_kind ${prerequisites ? 'IN' : 'NOT IN'} ('imports', 'extends', 'implements')`;
|
|
3088
|
+
this.stmts[key] = this.db.prepare(`SELECT * FROM unresolved_refs WHERE status = 'pending' AND id > ?${filter} ORDER BY id LIMIT ?`);
|
|
3089
|
+
}
|
|
3090
|
+
const rows = this.stmts[key].all(afterRowId, limit);
|
|
1925
3091
|
return rows.map((row) => ({
|
|
1926
3092
|
fromNodeId: row.from_node_id,
|
|
1927
3093
|
referenceName: row.reference_name,
|
|
@@ -1991,7 +3157,13 @@ class QueryBuilder {
|
|
|
1991
3157
|
const chunkRows = this.db
|
|
1992
3158
|
.prepare(`SELECT * FROM unresolved_refs WHERE status = 'pending' AND file_path IN (${placeholders})`)
|
|
1993
3159
|
.all(...chunk);
|
|
1994
|
-
|
|
3160
|
+
// Append with a loop, never a spread: the INPUT chunk is bounded, but
|
|
3161
|
+
// the RESULT rows per chunk are not — a dense recovery sync (e.g. the
|
|
3162
|
+
// #1541 self-heal re-indexing hundreds of files) returns more rows than
|
|
3163
|
+
// V8 allows as arguments, and `push(...chunkRows)` dies with "Maximum
|
|
3164
|
+
// call stack size exceeded", aborting resolution mid-sync (#1558).
|
|
3165
|
+
for (const row of chunkRows)
|
|
3166
|
+
rows.push(row);
|
|
1995
3167
|
}
|
|
1996
3168
|
return rows.map((row) => ({
|
|
1997
3169
|
fromNodeId: row.from_node_id,
|
|
@@ -2191,7 +3363,11 @@ class QueryBuilder {
|
|
|
2191
3363
|
const chunkRows = this.db
|
|
2192
3364
|
.prepare(`SELECT * FROM unresolved_refs WHERE status = 'failed' AND name_tail IN (${placeholders})`)
|
|
2193
3365
|
.all(...chunk);
|
|
2194
|
-
|
|
3366
|
+
// Loop, not spread — same V8 argument-limit hazard as
|
|
3367
|
+
// getUnresolvedReferencesByFiles (#1558): a large definition delta can
|
|
3368
|
+
// select an unbounded number of failed rows per chunk.
|
|
3369
|
+
for (const row of chunkRows)
|
|
3370
|
+
rows.push(row);
|
|
2195
3371
|
}
|
|
2196
3372
|
return rows.map((row) => ({
|
|
2197
3373
|
fromNodeId: row.from_node_id,
|
|
@@ -2207,6 +3383,103 @@ class QueryBuilder {
|
|
|
2207
3383
|
importedName: row.imported_name ?? undefined,
|
|
2208
3384
|
}));
|
|
2209
3385
|
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Resolution edges whose TARGET symbol is named one of `names` — the edges a
|
|
3388
|
+
* sync must re-resolve after `names` gained or lost a definition (CG-33).
|
|
3389
|
+
*
|
|
3390
|
+
* Resolution binds a reference to a node whose name matches the reference's
|
|
3391
|
+
* tail, and it picks among ALL same-named definitions project-wide. So adding
|
|
3392
|
+
* or removing one definition of `pct` changes the answer for every `pct(...)`
|
|
3393
|
+
* reference in the repo — including references in files this sync never
|
|
3394
|
+
* touches, whose edges nothing else revisits. Those edges' current target is,
|
|
3395
|
+
* by that same rule, a node named `pct`, which is why the target's name is a
|
|
3396
|
+
* sufficient (and index-backed, via idx_nodes_name) way to find them without
|
|
3397
|
+
* a schema change or a scan of edge metadata.
|
|
3398
|
+
*
|
|
3399
|
+
* Returns the source file/language alongside each edge so the caller can
|
|
3400
|
+
* resurrect it as its original reference. Excludes `provenance='heuristic'`
|
|
3401
|
+
* (synthesized dispatch edges are not resolution output and carry no refName
|
|
3402
|
+
* stamp to resurrect from — deleting one would be a permanent loss).
|
|
3403
|
+
*
|
|
3404
|
+
* Names matching more than `perNameCeiling` edges are skipped entirely, same
|
|
3405
|
+
* rationale and same default as {@link getRetryableFailedReferences}: at that
|
|
3406
|
+
* population the name is generic (`get`, `clear`, …), one definition changing
|
|
3407
|
+
* won't flip most of them, and rebinding an arbitrary subset is both wasted
|
|
3408
|
+
* work and incoherent coverage.
|
|
3409
|
+
*/
|
|
3410
|
+
getResolutionEdgesByTargetName(names, perNameCeiling = 500) {
|
|
3411
|
+
if (names.length === 0)
|
|
3412
|
+
return [];
|
|
3413
|
+
// Pass 1: per-name edge counts, chunked under the SQLite parameter limit.
|
|
3414
|
+
const keep = [];
|
|
3415
|
+
for (let i = 0; i < names.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
3416
|
+
const chunk = names.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
3417
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
3418
|
+
const counts = this.db
|
|
3419
|
+
.prepare(`SELECT tgt.name AS name, COUNT(*) AS count
|
|
3420
|
+
FROM edges e
|
|
3421
|
+
JOIN nodes tgt ON tgt.id = e.target
|
|
3422
|
+
WHERE tgt.name IN (${placeholders})
|
|
3423
|
+
AND (e.provenance IS NULL OR e.provenance != 'heuristic')
|
|
3424
|
+
GROUP BY tgt.name`)
|
|
3425
|
+
.all(...chunk);
|
|
3426
|
+
for (const row of counts) {
|
|
3427
|
+
if (row.count <= perNameCeiling)
|
|
3428
|
+
keep.push(row.name);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
if (keep.length === 0)
|
|
3432
|
+
return [];
|
|
3433
|
+
// Pass 2: load the surviving edges with the source file context a
|
|
3434
|
+
// resurrection needs.
|
|
3435
|
+
const out = [];
|
|
3436
|
+
for (let i = 0; i < keep.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
3437
|
+
const chunk = keep.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
3438
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
3439
|
+
const rows = this.db
|
|
3440
|
+
.prepare(`SELECT e.*, src.file_path AS source_file_path, src.language AS source_language
|
|
3441
|
+
FROM edges e
|
|
3442
|
+
JOIN nodes tgt ON tgt.id = e.target
|
|
3443
|
+
JOIN nodes src ON src.id = e.source
|
|
3444
|
+
WHERE tgt.name IN (${placeholders})
|
|
3445
|
+
AND (e.provenance IS NULL OR e.provenance != 'heuristic')`)
|
|
3446
|
+
.all(...chunk);
|
|
3447
|
+
for (const row of rows) {
|
|
3448
|
+
out.push({
|
|
3449
|
+
...rowToEdge(row),
|
|
3450
|
+
edgeId: row.id,
|
|
3451
|
+
sourceFilePath: row.source_file_path,
|
|
3452
|
+
sourceLanguage: row.source_language,
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
return out;
|
|
3457
|
+
}
|
|
3458
|
+
/** Delete edges by primary key — the rebind pass's half of a re-resolution. */
|
|
3459
|
+
deleteEdgesByIds(edgeIds) {
|
|
3460
|
+
if (edgeIds.length === 0)
|
|
3461
|
+
return 0;
|
|
3462
|
+
let changed = 0;
|
|
3463
|
+
this.db.transaction(() => {
|
|
3464
|
+
for (let i = 0; i < edgeIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
3465
|
+
const chunk = edgeIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
3466
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
3467
|
+
changed += this.db.prepare(`DELETE FROM edges WHERE id IN (${placeholders})`).run(...chunk).changes;
|
|
3468
|
+
}
|
|
3469
|
+
})();
|
|
3470
|
+
return changed;
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Replace resolution edges with their original unresolved references as one
|
|
3474
|
+
* transaction. If ref insertion fails, the edge deletion is rolled back.
|
|
3475
|
+
*/
|
|
3476
|
+
replaceResolutionEdgesWithUnresolvedRefs(edgeIds, refs) {
|
|
3477
|
+
return this.db.transaction(() => {
|
|
3478
|
+
const changed = this.deleteEdgesByIds(edgeIds);
|
|
3479
|
+
this.insertUnresolvedRefsBatch(refs);
|
|
3480
|
+
return changed;
|
|
3481
|
+
})();
|
|
3482
|
+
}
|
|
2210
3483
|
/**
|
|
2211
3484
|
* Distinct node names present in the given files — the symbol names a sync
|
|
2212
3485
|
* pass uses to look up retryable failed refs after those files changed.
|
|
@@ -2226,6 +3499,34 @@ class QueryBuilder {
|
|
|
2226
3499
|
}
|
|
2227
3500
|
return [...names];
|
|
2228
3501
|
}
|
|
3502
|
+
/**
|
|
3503
|
+
* Distinct `file\0name` pairs defined by the given files — the shape sync's
|
|
3504
|
+
* definition delta needs (CG-33).
|
|
3505
|
+
*
|
|
3506
|
+
* Deliberately NOT `getNodeNamesByFiles`: a bare name set is taken over the
|
|
3507
|
+
* WHOLE changed batch, so a name that moves between two files in one commit
|
|
3508
|
+
* (or exists in one changed file and is newly added to another) appears on
|
|
3509
|
+
* both sides and cancels out of the symmetric difference — even though a
|
|
3510
|
+
* definition genuinely appeared or vanished and every reference to that name
|
|
3511
|
+
* repo-wide may now bind elsewhere. Keying by file makes each definition its
|
|
3512
|
+
* own fact, so the move is seen as one removal plus one addition.
|
|
3513
|
+
*/
|
|
3514
|
+
getNodeNamePairsByFiles(filePaths) {
|
|
3515
|
+
const pairs = new Set();
|
|
3516
|
+
if (filePaths.length === 0)
|
|
3517
|
+
return pairs;
|
|
3518
|
+
for (let i = 0; i < filePaths.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
3519
|
+
const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
3520
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
3521
|
+
const rows = this.db
|
|
3522
|
+
.prepare(`SELECT DISTINCT file_path, name FROM nodes WHERE file_path IN (${placeholders})`)
|
|
3523
|
+
.all(...chunk);
|
|
3524
|
+
// NUL-joined: a path or a symbol name can contain a space, never a NUL.
|
|
3525
|
+
for (const row of rows)
|
|
3526
|
+
pairs.add(`${row.file_path}\0${row.name}`);
|
|
3527
|
+
}
|
|
3528
|
+
return pairs;
|
|
3529
|
+
}
|
|
2229
3530
|
// ===========================================================================
|
|
2230
3531
|
// Statistics
|
|
2231
3532
|
// ===========================================================================
|
|
@@ -2326,4 +3627,71 @@ class QueryBuilder {
|
|
|
2326
3627
|
}
|
|
2327
3628
|
}
|
|
2328
3629
|
exports.QueryBuilder = QueryBuilder;
|
|
3630
|
+
function foldModuleRows(rows, options) {
|
|
3631
|
+
// A module id is a path and may contain anything printable, so the key
|
|
3632
|
+
// separator has to be something a path cannot hold.
|
|
3633
|
+
const SEP = '\u0000';
|
|
3634
|
+
const links = new Map();
|
|
3635
|
+
const pairKinds = new Set(options.pairKinds);
|
|
3636
|
+
const wantPairs = options.topPairsPerLink > 0 && pairKinds.size > 0;
|
|
3637
|
+
const pairTotals = new Map();
|
|
3638
|
+
for (const row of rows) {
|
|
3639
|
+
const linkKey = `${row.source}${SEP}${row.target}${SEP}${row.kind}`;
|
|
3640
|
+
const link = links.get(linkKey);
|
|
3641
|
+
if (link) {
|
|
3642
|
+
link.count += row.count;
|
|
3643
|
+
link.declared += row.declared;
|
|
3644
|
+
link.uncertain += row.uncertain;
|
|
3645
|
+
}
|
|
3646
|
+
else {
|
|
3647
|
+
links.set(linkKey, {
|
|
3648
|
+
source: row.source,
|
|
3649
|
+
target: row.target,
|
|
3650
|
+
kind: row.kind,
|
|
3651
|
+
count: row.count,
|
|
3652
|
+
declared: row.declared,
|
|
3653
|
+
uncertain: row.uncertain,
|
|
3654
|
+
});
|
|
3655
|
+
}
|
|
3656
|
+
// Only the confident half of a row can be named: an uncertain edge is a
|
|
3657
|
+
// guess, and printing "a to b, 12" for twelve guesses is the map claiming
|
|
3658
|
+
// something it does not know.
|
|
3659
|
+
if (!wantPairs || row.count === 0 || !pairKinds.has(row.kind))
|
|
3660
|
+
continue;
|
|
3661
|
+
const pairKey = `${row.source}${SEP}${row.target}${SEP}${row.from}${SEP}${row.to}`;
|
|
3662
|
+
const pair = pairTotals.get(pairKey);
|
|
3663
|
+
if (pair) {
|
|
3664
|
+
pair.count += row.count;
|
|
3665
|
+
pair.declared += row.declared;
|
|
3666
|
+
}
|
|
3667
|
+
else {
|
|
3668
|
+
pairTotals.set(pairKey, {
|
|
3669
|
+
source: row.source,
|
|
3670
|
+
target: row.target,
|
|
3671
|
+
from: row.from,
|
|
3672
|
+
to: row.to,
|
|
3673
|
+
count: row.count,
|
|
3674
|
+
declared: row.declared,
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
const byLink = new Map();
|
|
3679
|
+
for (const pair of pairTotals.values()) {
|
|
3680
|
+
const key = `${pair.source}${SEP}${pair.target}`;
|
|
3681
|
+
let list = byLink.get(key);
|
|
3682
|
+
if (!list)
|
|
3683
|
+
byLink.set(key, (list = []));
|
|
3684
|
+
list.push(pair);
|
|
3685
|
+
}
|
|
3686
|
+
const pairs = [];
|
|
3687
|
+
for (const list of byLink.values()) {
|
|
3688
|
+
list.sort((a, b) => b.declared - a.declared ||
|
|
3689
|
+
b.count - a.count ||
|
|
3690
|
+
a.from.localeCompare(b.from) ||
|
|
3691
|
+
a.to.localeCompare(b.to));
|
|
3692
|
+
for (const pair of list.slice(0, options.topPairsPerLink))
|
|
3693
|
+
pairs.push(pair);
|
|
3694
|
+
}
|
|
3695
|
+
return { links: [...links.values()], pairs };
|
|
3696
|
+
}
|
|
2329
3697
|
//# sourceMappingURL=queries.js.map
|