@vizalice/codegraph 1.5.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/LICENSE +21 -0
- package/README.md +872 -0
- package/dist/bin/codegraph.d.ts +26 -0
- package/dist/bin/codegraph.d.ts.map +1 -0
- package/dist/bin/codegraph.js +2324 -0
- package/dist/bin/codegraph.js.map +1 -0
- package/dist/bin/command-supervision.d.ts +49 -0
- package/dist/bin/command-supervision.d.ts.map +1 -0
- package/dist/bin/command-supervision.js +95 -0
- package/dist/bin/command-supervision.js.map +1 -0
- package/dist/bin/fatal-handler.d.ts +20 -0
- package/dist/bin/fatal-handler.d.ts.map +1 -0
- package/dist/bin/fatal-handler.js +118 -0
- package/dist/bin/fatal-handler.js.map +1 -0
- package/dist/bin/node-version-check.d.ts +37 -0
- package/dist/bin/node-version-check.d.ts.map +1 -0
- package/dist/bin/node-version-check.js +79 -0
- package/dist/bin/node-version-check.js.map +1 -0
- package/dist/bin/uninstall.d.ts +14 -0
- package/dist/bin/uninstall.d.ts.map +1 -0
- package/dist/bin/uninstall.js +36 -0
- package/dist/bin/uninstall.js.map +1 -0
- package/dist/context/formatter.d.ts +36 -0
- package/dist/context/formatter.d.ts.map +1 -0
- package/dist/context/formatter.js +269 -0
- package/dist/context/formatter.js.map +1 -0
- package/dist/context/index.d.ts +119 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +1333 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/markers.d.ts +19 -0
- package/dist/context/markers.d.ts.map +1 -0
- package/dist/context/markers.js +22 -0
- package/dist/context/markers.js.map +1 -0
- package/dist/db/index.d.ts +378 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +831 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrations.d.ts +44 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +236 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/queries.d.ts +705 -0
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +2435 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.sql +207 -0
- package/dist/db/sqlite-adapter.d.ts +55 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/sqlite-adapter.js +135 -0
- package/dist/db/sqlite-adapter.js.map +1 -0
- package/dist/db/wal-valve.d.ts +119 -0
- package/dist/db/wal-valve.d.ts.map +1 -0
- package/dist/db/wal-valve.js +290 -0
- package/dist/db/wal-valve.js.map +1 -0
- package/dist/directory.d.ts +183 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +797 -0
- package/dist/directory.js.map +1 -0
- package/dist/errors.d.ts +136 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +219 -0
- package/dist/errors.js.map +1 -0
- package/dist/extraction/astro-extractor.d.ts +79 -0
- package/dist/extraction/astro-extractor.d.ts.map +1 -0
- package/dist/extraction/astro-extractor.js +320 -0
- package/dist/extraction/astro-extractor.js.map +1 -0
- package/dist/extraction/cfml-extractor.d.ts +107 -0
- package/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/dist/extraction/cfml-extractor.js +494 -0
- package/dist/extraction/cfml-extractor.js.map +1 -0
- package/dist/extraction/dfm-extractor.d.ts +31 -0
- package/dist/extraction/dfm-extractor.d.ts.map +1 -0
- package/dist/extraction/dfm-extractor.js +151 -0
- package/dist/extraction/dfm-extractor.js.map +1 -0
- package/dist/extraction/extraction-version.d.ts +25 -0
- package/dist/extraction/extraction-version.d.ts.map +1 -0
- package/dist/extraction/extraction-version.js +28 -0
- package/dist/extraction/extraction-version.js.map +1 -0
- package/dist/extraction/function-ref.d.ts +118 -0
- package/dist/extraction/function-ref.d.ts.map +1 -0
- package/dist/extraction/function-ref.js +733 -0
- package/dist/extraction/function-ref.js.map +1 -0
- package/dist/extraction/generated-detection.d.ts +66 -0
- package/dist/extraction/generated-detection.d.ts.map +1 -0
- package/dist/extraction/generated-detection.js +248 -0
- package/dist/extraction/generated-detection.js.map +1 -0
- package/dist/extraction/grammars.d.ts +138 -0
- package/dist/extraction/grammars.d.ts.map +1 -0
- package/dist/extraction/grammars.js +694 -0
- package/dist/extraction/grammars.js.map +1 -0
- package/dist/extraction/index.d.ts +308 -0
- package/dist/extraction/index.d.ts.map +1 -0
- package/dist/extraction/index.js +2711 -0
- package/dist/extraction/index.js.map +1 -0
- package/dist/extraction/kernel/decode.d.ts +9 -0
- package/dist/extraction/kernel/decode.d.ts.map +1 -0
- package/dist/extraction/kernel/decode.js +162 -0
- package/dist/extraction/kernel/decode.js.map +1 -0
- package/dist/extraction/kernel/index.d.ts +64 -0
- package/dist/extraction/kernel/index.d.ts.map +1 -0
- package/dist/extraction/kernel/index.js +279 -0
- package/dist/extraction/kernel/index.js.map +1 -0
- package/dist/extraction/kernel/layout.d.ts +101 -0
- package/dist/extraction/kernel/layout.d.ts.map +1 -0
- package/dist/extraction/kernel/layout.js +104 -0
- package/dist/extraction/kernel/layout.js.map +1 -0
- package/dist/extraction/kernel/loader.d.ts +103 -0
- package/dist/extraction/kernel/loader.d.ts.map +1 -0
- package/dist/extraction/kernel/loader.js +146 -0
- package/dist/extraction/kernel/loader.js.map +1 -0
- package/dist/extraction/languages/arkts.d.ts +3 -0
- package/dist/extraction/languages/arkts.d.ts.map +1 -0
- package/dist/extraction/languages/arkts.js +127 -0
- package/dist/extraction/languages/arkts.js.map +1 -0
- package/dist/extraction/languages/c-cpp.d.ts +124 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
- package/dist/extraction/languages/c-cpp.js +1689 -0
- package/dist/extraction/languages/c-cpp.js.map +1 -0
- package/dist/extraction/languages/cfquery.d.ts +12 -0
- package/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/dist/extraction/languages/cfquery.js +28 -0
- package/dist/extraction/languages/cfquery.js.map +1 -0
- package/dist/extraction/languages/cfscript.d.ts +3 -0
- package/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/dist/extraction/languages/cfscript.js +73 -0
- package/dist/extraction/languages/cfscript.js.map +1 -0
- package/dist/extraction/languages/cobol.d.ts +33 -0
- package/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/dist/extraction/languages/cobol.js +499 -0
- package/dist/extraction/languages/cobol.js.map +1 -0
- package/dist/extraction/languages/csharp.d.ts +25 -0
- package/dist/extraction/languages/csharp.d.ts.map +1 -0
- package/dist/extraction/languages/csharp.js +175 -0
- package/dist/extraction/languages/csharp.js.map +1 -0
- package/dist/extraction/languages/dart.d.ts +3 -0
- package/dist/extraction/languages/dart.d.ts.map +1 -0
- package/dist/extraction/languages/dart.js +374 -0
- package/dist/extraction/languages/dart.js.map +1 -0
- package/dist/extraction/languages/erlang.d.ts +3 -0
- package/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/dist/extraction/languages/erlang.js +350 -0
- package/dist/extraction/languages/erlang.js.map +1 -0
- package/dist/extraction/languages/go.d.ts +3 -0
- package/dist/extraction/languages/go.d.ts.map +1 -0
- package/dist/extraction/languages/go.js +111 -0
- package/dist/extraction/languages/go.js.map +1 -0
- package/dist/extraction/languages/index.d.ts +10 -0
- package/dist/extraction/languages/index.d.ts.map +1 -0
- package/dist/extraction/languages/index.js +71 -0
- package/dist/extraction/languages/index.js.map +1 -0
- package/dist/extraction/languages/java.d.ts +3 -0
- package/dist/extraction/languages/java.d.ts.map +1 -0
- package/dist/extraction/languages/java.js +315 -0
- package/dist/extraction/languages/java.js.map +1 -0
- package/dist/extraction/languages/javascript.d.ts +3 -0
- package/dist/extraction/languages/javascript.d.ts.map +1 -0
- package/dist/extraction/languages/javascript.js +106 -0
- package/dist/extraction/languages/javascript.js.map +1 -0
- package/dist/extraction/languages/kotlin.d.ts +3 -0
- package/dist/extraction/languages/kotlin.d.ts.map +1 -0
- package/dist/extraction/languages/kotlin.js +379 -0
- package/dist/extraction/languages/kotlin.js.map +1 -0
- package/dist/extraction/languages/lua.d.ts +3 -0
- package/dist/extraction/languages/lua.d.ts.map +1 -0
- package/dist/extraction/languages/lua.js +150 -0
- package/dist/extraction/languages/lua.js.map +1 -0
- package/dist/extraction/languages/luau.d.ts +3 -0
- package/dist/extraction/languages/luau.d.ts.map +1 -0
- package/dist/extraction/languages/luau.js +37 -0
- package/dist/extraction/languages/luau.js.map +1 -0
- package/dist/extraction/languages/nix.d.ts +3 -0
- package/dist/extraction/languages/nix.d.ts.map +1 -0
- package/dist/extraction/languages/nix.js +294 -0
- package/dist/extraction/languages/nix.js.map +1 -0
- package/dist/extraction/languages/objc.d.ts +3 -0
- package/dist/extraction/languages/objc.d.ts.map +1 -0
- package/dist/extraction/languages/objc.js +179 -0
- package/dist/extraction/languages/objc.js.map +1 -0
- package/dist/extraction/languages/pascal.d.ts +3 -0
- package/dist/extraction/languages/pascal.d.ts.map +1 -0
- package/dist/extraction/languages/pascal.js +77 -0
- package/dist/extraction/languages/pascal.js.map +1 -0
- package/dist/extraction/languages/php.d.ts +3 -0
- package/dist/extraction/languages/php.d.ts.map +1 -0
- package/dist/extraction/languages/php.js +196 -0
- package/dist/extraction/languages/php.js.map +1 -0
- package/dist/extraction/languages/python.d.ts +3 -0
- package/dist/extraction/languages/python.d.ts.map +1 -0
- package/dist/extraction/languages/python.js +56 -0
- package/dist/extraction/languages/python.js.map +1 -0
- package/dist/extraction/languages/r.d.ts +3 -0
- package/dist/extraction/languages/r.d.ts.map +1 -0
- package/dist/extraction/languages/r.js +314 -0
- package/dist/extraction/languages/r.js.map +1 -0
- package/dist/extraction/languages/ruby.d.ts +3 -0
- package/dist/extraction/languages/ruby.d.ts.map +1 -0
- package/dist/extraction/languages/ruby.js +149 -0
- package/dist/extraction/languages/ruby.js.map +1 -0
- package/dist/extraction/languages/rust.d.ts +3 -0
- package/dist/extraction/languages/rust.d.ts.map +1 -0
- package/dist/extraction/languages/rust.js +145 -0
- package/dist/extraction/languages/rust.js.map +1 -0
- package/dist/extraction/languages/scala.d.ts +3 -0
- package/dist/extraction/languages/scala.d.ts.map +1 -0
- package/dist/extraction/languages/scala.js +209 -0
- package/dist/extraction/languages/scala.js.map +1 -0
- package/dist/extraction/languages/solidity.d.ts +3 -0
- package/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/dist/extraction/languages/solidity.js +293 -0
- package/dist/extraction/languages/solidity.js.map +1 -0
- package/dist/extraction/languages/swift.d.ts +3 -0
- package/dist/extraction/languages/swift.d.ts.map +1 -0
- package/dist/extraction/languages/swift.js +152 -0
- package/dist/extraction/languages/swift.js.map +1 -0
- package/dist/extraction/languages/terraform.d.ts +3 -0
- package/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/dist/extraction/languages/terraform.js +641 -0
- package/dist/extraction/languages/terraform.js.map +1 -0
- package/dist/extraction/languages/typescript.d.ts +16 -0
- package/dist/extraction/languages/typescript.d.ts.map +1 -0
- package/dist/extraction/languages/typescript.js +167 -0
- package/dist/extraction/languages/typescript.js.map +1 -0
- package/dist/extraction/languages/vbnet.d.ts +11 -0
- package/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/dist/extraction/languages/vbnet.js +141 -0
- package/dist/extraction/languages/vbnet.js.map +1 -0
- package/dist/extraction/liquid-extractor.d.ts +59 -0
- package/dist/extraction/liquid-extractor.d.ts.map +1 -0
- package/dist/extraction/liquid-extractor.js +357 -0
- package/dist/extraction/liquid-extractor.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +68 -0
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -0
- package/dist/extraction/mybatis-extractor.js +300 -0
- package/dist/extraction/mybatis-extractor.js.map +1 -0
- package/dist/extraction/parse-pool.d.ts +160 -0
- package/dist/extraction/parse-pool.d.ts.map +1 -0
- package/dist/extraction/parse-pool.js +402 -0
- package/dist/extraction/parse-pool.js.map +1 -0
- package/dist/extraction/parse-worker.d.ts +8 -0
- package/dist/extraction/parse-worker.d.ts.map +1 -0
- package/dist/extraction/parse-worker.js +138 -0
- package/dist/extraction/parse-worker.js.map +1 -0
- package/dist/extraction/razor-extractor.d.ts +42 -0
- package/dist/extraction/razor-extractor.d.ts.map +1 -0
- package/dist/extraction/razor-extractor.js +285 -0
- package/dist/extraction/razor-extractor.js.map +1 -0
- package/dist/extraction/store-worker.d.ts +24 -0
- package/dist/extraction/store-worker.d.ts.map +1 -0
- package/dist/extraction/store-worker.js +139 -0
- package/dist/extraction/store-worker.js.map +1 -0
- package/dist/extraction/store-writer.d.ts +63 -0
- package/dist/extraction/store-writer.d.ts.map +1 -0
- package/dist/extraction/store-writer.js +174 -0
- package/dist/extraction/store-writer.js.map +1 -0
- package/dist/extraction/svelte-extractor.d.ts +56 -0
- package/dist/extraction/svelte-extractor.d.ts.map +1 -0
- package/dist/extraction/svelte-extractor.js +275 -0
- package/dist/extraction/svelte-extractor.js.map +1 -0
- package/dist/extraction/tree-sitter-helpers.d.ts +28 -0
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
- package/dist/extraction/tree-sitter-helpers.js +153 -0
- package/dist/extraction/tree-sitter-helpers.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +260 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
- package/dist/extraction/tree-sitter-types.js +10 -0
- package/dist/extraction/tree-sitter-types.js.map +1 -0
- package/dist/extraction/tree-sitter.d.ts +728 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -0
- package/dist/extraction/tree-sitter.js +6676 -0
- package/dist/extraction/tree-sitter.js.map +1 -0
- package/dist/extraction/vue-extractor.d.ts +51 -0
- package/dist/extraction/vue-extractor.d.ts.map +1 -0
- package/dist/extraction/vue-extractor.js +254 -0
- package/dist/extraction/vue-extractor.js.map +1 -0
- package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-c.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cpp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-dart.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-kotlin.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-php.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-ruby.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-rust.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-swift.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +53 -0
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
- package/dist/extraction/wasm-runtime-flags.js +130 -0
- package/dist/extraction/wasm-runtime-flags.js.map +1 -0
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +13 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/queries.d.ts +106 -0
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +341 -0
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/traversal.d.ts +127 -0
- package/dist/graph/traversal.d.ts.map +1 -0
- package/dist/graph/traversal.js +599 -0
- package/dist/graph/traversal.js.map +1 -0
- package/dist/index.d.ts +677 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1718 -0
- package/dist/index.js.map +1 -0
- package/dist/installer/beta-signup.d.ts +54 -0
- package/dist/installer/beta-signup.d.ts.map +1 -0
- package/dist/installer/beta-signup.js +178 -0
- package/dist/installer/beta-signup.js.map +1 -0
- package/dist/installer/config-writer.d.ts +28 -0
- package/dist/installer/config-writer.d.ts.map +1 -0
- package/dist/installer/config-writer.js +91 -0
- package/dist/installer/config-writer.js.map +1 -0
- package/dist/installer/index.d.ts +144 -0
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +634 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/instructions-template.d.ts +41 -0
- package/dist/installer/instructions-template.d.ts.map +1 -0
- package/dist/installer/instructions-template.js +53 -0
- package/dist/installer/instructions-template.js.map +1 -0
- package/dist/installer/targets/antigravity.d.ts +57 -0
- package/dist/installer/targets/antigravity.d.ts.map +1 -0
- package/dist/installer/targets/antigravity.js +308 -0
- package/dist/installer/targets/antigravity.js.map +1 -0
- package/dist/installer/targets/claude.d.ts +62 -0
- package/dist/installer/targets/claude.d.ts.map +1 -0
- package/dist/installer/targets/claude.js +486 -0
- package/dist/installer/targets/claude.js.map +1 -0
- package/dist/installer/targets/codex.d.ts +18 -0
- package/dist/installer/targets/codex.d.ts.map +1 -0
- package/dist/installer/targets/codex.js +185 -0
- package/dist/installer/targets/codex.js.map +1 -0
- package/dist/installer/targets/copilot-cli.d.ts +32 -0
- package/dist/installer/targets/copilot-cli.d.ts.map +1 -0
- package/dist/installer/targets/copilot-cli.js +209 -0
- package/dist/installer/targets/copilot-cli.js.map +1 -0
- package/dist/installer/targets/copilot-jetbrains.d.ts +42 -0
- package/dist/installer/targets/copilot-jetbrains.d.ts.map +1 -0
- package/dist/installer/targets/copilot-jetbrains.js +238 -0
- package/dist/installer/targets/copilot-jetbrains.js.map +1 -0
- package/dist/installer/targets/copilot-vscode.d.ts +47 -0
- package/dist/installer/targets/copilot-vscode.d.ts.map +1 -0
- package/dist/installer/targets/copilot-vscode.js +219 -0
- package/dist/installer/targets/copilot-vscode.js.map +1 -0
- package/dist/installer/targets/cursor.d.ts +35 -0
- package/dist/installer/targets/cursor.d.ts.map +1 -0
- package/dist/installer/targets/cursor.js +254 -0
- package/dist/installer/targets/cursor.js.map +1 -0
- package/dist/installer/targets/dsh.d.ts +97 -0
- package/dist/installer/targets/dsh.d.ts.map +1 -0
- package/dist/installer/targets/dsh.js +358 -0
- package/dist/installer/targets/dsh.js.map +1 -0
- package/dist/installer/targets/gemini.d.ts +26 -0
- package/dist/installer/targets/gemini.d.ts.map +1 -0
- package/dist/installer/targets/gemini.js +165 -0
- package/dist/installer/targets/gemini.js.map +1 -0
- package/dist/installer/targets/hermes.d.ts +18 -0
- package/dist/installer/targets/hermes.d.ts.map +1 -0
- package/dist/installer/targets/hermes.js +359 -0
- package/dist/installer/targets/hermes.js.map +1 -0
- package/dist/installer/targets/kiro.d.ts +27 -0
- package/dist/installer/targets/kiro.d.ts.map +1 -0
- package/dist/installer/targets/kiro.js +178 -0
- package/dist/installer/targets/kiro.js.map +1 -0
- package/dist/installer/targets/opencode.d.ts +38 -0
- package/dist/installer/targets/opencode.d.ts.map +1 -0
- package/dist/installer/targets/opencode.js +288 -0
- package/dist/installer/targets/opencode.js.map +1 -0
- package/dist/installer/targets/registry.d.ts +35 -0
- package/dist/installer/targets/registry.d.ts.map +1 -0
- package/dist/installer/targets/registry.js +101 -0
- package/dist/installer/targets/registry.js.map +1 -0
- package/dist/installer/targets/shared.d.ts +132 -0
- package/dist/installer/targets/shared.d.ts.map +1 -0
- package/dist/installer/targets/shared.js +314 -0
- package/dist/installer/targets/shared.js.map +1 -0
- package/dist/installer/targets/toml.d.ts +53 -0
- package/dist/installer/targets/toml.d.ts.map +1 -0
- package/dist/installer/targets/toml.js +226 -0
- package/dist/installer/targets/toml.js.map +1 -0
- package/dist/installer/targets/types.d.ts +109 -0
- package/dist/installer/targets/types.d.ts.map +1 -0
- package/dist/installer/targets/types.js +16 -0
- package/dist/installer/targets/types.js.map +1 -0
- package/dist/installer/targets/zcode.d.ts +106 -0
- package/dist/installer/targets/zcode.d.ts.map +1 -0
- package/dist/installer/targets/zcode.js +451 -0
- package/dist/installer/targets/zcode.js.map +1 -0
- package/dist/mcp/daemon-manager.d.ts +42 -0
- package/dist/mcp/daemon-manager.d.ts.map +1 -0
- package/dist/mcp/daemon-manager.js +129 -0
- package/dist/mcp/daemon-manager.js.map +1 -0
- package/dist/mcp/daemon-paths.d.ts +73 -0
- package/dist/mcp/daemon-paths.d.ts.map +1 -0
- package/dist/mcp/daemon-paths.js +165 -0
- package/dist/mcp/daemon-paths.js.map +1 -0
- package/dist/mcp/daemon-registry.d.ts +47 -0
- package/dist/mcp/daemon-registry.d.ts.map +1 -0
- package/dist/mcp/daemon-registry.js +233 -0
- package/dist/mcp/daemon-registry.js.map +1 -0
- package/dist/mcp/daemon.d.ts +290 -0
- package/dist/mcp/daemon.d.ts.map +1 -0
- package/dist/mcp/daemon.js +862 -0
- package/dist/mcp/daemon.js.map +1 -0
- package/dist/mcp/dynamic-boundaries.d.ts +41 -0
- package/dist/mcp/dynamic-boundaries.d.ts.map +1 -0
- package/dist/mcp/dynamic-boundaries.js +360 -0
- package/dist/mcp/dynamic-boundaries.js.map +1 -0
- package/dist/mcp/early-ppid.d.ts +26 -0
- package/dist/mcp/early-ppid.d.ts.map +1 -0
- package/dist/mcp/early-ppid.js +29 -0
- package/dist/mcp/early-ppid.js.map +1 -0
- package/dist/mcp/engine.d.ts +122 -0
- package/dist/mcp/engine.d.ts.map +1 -0
- package/dist/mcp/engine.js +350 -0
- package/dist/mcp/engine.js.map +1 -0
- package/dist/mcp/explore-dedup.d.ts +137 -0
- package/dist/mcp/explore-dedup.d.ts.map +1 -0
- package/dist/mcp/explore-dedup.js +236 -0
- package/dist/mcp/explore-dedup.js.map +1 -0
- package/dist/mcp/explore-diagnostics.d.ts +287 -0
- package/dist/mcp/explore-diagnostics.d.ts.map +1 -0
- package/dist/mcp/explore-diagnostics.js +555 -0
- package/dist/mcp/explore-diagnostics.js.map +1 -0
- package/dist/mcp/explore-session-state.d.ts +217 -0
- package/dist/mcp/explore-session-state.d.ts.map +1 -0
- package/dist/mcp/explore-session-state.js +322 -0
- package/dist/mcp/explore-session-state.js.map +1 -0
- package/dist/mcp/index.d.ts +137 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +554 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/liveness-watchdog.d.ts +35 -0
- package/dist/mcp/liveness-watchdog.d.ts.map +1 -0
- package/dist/mcp/liveness-watchdog.js +269 -0
- package/dist/mcp/liveness-watchdog.js.map +1 -0
- package/dist/mcp/ppid-watchdog.d.ts +62 -0
- package/dist/mcp/ppid-watchdog.d.ts.map +1 -0
- package/dist/mcp/ppid-watchdog.js +64 -0
- package/dist/mcp/ppid-watchdog.js.map +1 -0
- package/dist/mcp/proxy.d.ts +87 -0
- package/dist/mcp/proxy.d.ts.map +1 -0
- package/dist/mcp/proxy.js +672 -0
- package/dist/mcp/proxy.js.map +1 -0
- package/dist/mcp/query-pool.d.ts +108 -0
- package/dist/mcp/query-pool.d.ts.map +1 -0
- package/dist/mcp/query-pool.js +315 -0
- package/dist/mcp/query-pool.js.map +1 -0
- package/dist/mcp/query-worker.d.ts +24 -0
- package/dist/mcp/query-worker.d.ts.map +1 -0
- package/dist/mcp/query-worker.js +87 -0
- package/dist/mcp/query-worker.js.map +1 -0
- package/dist/mcp/server-instructions.d.ts +34 -0
- package/dist/mcp/server-instructions.d.ts.map +1 -0
- package/dist/mcp/server-instructions.js +111 -0
- package/dist/mcp/server-instructions.js.map +1 -0
- package/dist/mcp/session.d.ts +109 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +375 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/startup-handshake.d.ts +44 -0
- package/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/dist/mcp/startup-handshake.js +73 -0
- package/dist/mcp/startup-handshake.js.map +1 -0
- package/dist/mcp/stdin-teardown.d.ts +27 -0
- package/dist/mcp/stdin-teardown.d.ts.map +1 -0
- package/dist/mcp/stdin-teardown.js +49 -0
- package/dist/mcp/stdin-teardown.js.map +1 -0
- package/dist/mcp/tools.d.ts +862 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +6526 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/transport.d.ts +188 -0
- package/dist/mcp/transport.d.ts.map +1 -0
- package/dist/mcp/transport.js +377 -0
- package/dist/mcp/transport.js.map +1 -0
- package/dist/mcp/version.d.ts +19 -0
- package/dist/mcp/version.d.ts.map +1 -0
- package/dist/mcp/version.js +71 -0
- package/dist/mcp/version.js.map +1 -0
- package/dist/project-config.d.ts +94 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +374 -0
- package/dist/project-config.js.map +1 -0
- package/dist/prompt-hook-output.d.ts +23 -0
- package/dist/prompt-hook-output.d.ts.map +1 -0
- package/dist/prompt-hook-output.js +35 -0
- package/dist/prompt-hook-output.js.map +1 -0
- package/dist/resolution/c-fnptr-synthesizer.d.ts +6 -0
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -0
- package/dist/resolution/c-fnptr-synthesizer.js +1506 -0
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -0
- package/dist/resolution/callback-synthesizer.d.ts +59 -0
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
- package/dist/resolution/callback-synthesizer.js +4030 -0
- package/dist/resolution/callback-synthesizer.js.map +1 -0
- package/dist/resolution/cooperative-yield.d.ts +40 -0
- package/dist/resolution/cooperative-yield.d.ts.map +1 -0
- package/dist/resolution/cooperative-yield.js +44 -0
- package/dist/resolution/cooperative-yield.js.map +1 -0
- package/dist/resolution/frameworks/astro.d.ts +9 -0
- package/dist/resolution/frameworks/astro.d.ts.map +1 -0
- package/dist/resolution/frameworks/astro.js +169 -0
- package/dist/resolution/frameworks/astro.js.map +1 -0
- package/dist/resolution/frameworks/cargo-workspace.d.ts +18 -0
- package/dist/resolution/frameworks/cargo-workspace.d.ts.map +1 -0
- package/dist/resolution/frameworks/cargo-workspace.js +225 -0
- package/dist/resolution/frameworks/cargo-workspace.js.map +1 -0
- package/dist/resolution/frameworks/cics.d.ts +20 -0
- package/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/dist/resolution/frameworks/cics.js +90 -0
- package/dist/resolution/frameworks/cics.js.map +1 -0
- package/dist/resolution/frameworks/csharp.d.ts +8 -0
- package/dist/resolution/frameworks/csharp.d.ts.map +1 -0
- package/dist/resolution/frameworks/csharp.js +241 -0
- package/dist/resolution/frameworks/csharp.js.map +1 -0
- package/dist/resolution/frameworks/drupal.d.ts +51 -0
- package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
- package/dist/resolution/frameworks/drupal.js +367 -0
- package/dist/resolution/frameworks/drupal.js.map +1 -0
- package/dist/resolution/frameworks/expo-modules.d.ts +3 -0
- package/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
- package/dist/resolution/frameworks/expo-modules.js +148 -0
- package/dist/resolution/frameworks/expo-modules.js.map +1 -0
- package/dist/resolution/frameworks/express.d.ts +8 -0
- package/dist/resolution/frameworks/express.d.ts.map +1 -0
- package/dist/resolution/frameworks/express.js +308 -0
- package/dist/resolution/frameworks/express.js.map +1 -0
- package/dist/resolution/frameworks/fabric.d.ts +3 -0
- package/dist/resolution/frameworks/fabric.d.ts.map +1 -0
- package/dist/resolution/frameworks/fabric.js +354 -0
- package/dist/resolution/frameworks/fabric.js.map +1 -0
- package/dist/resolution/frameworks/go.d.ts +8 -0
- package/dist/resolution/frameworks/go.d.ts.map +1 -0
- package/dist/resolution/frameworks/go.js +184 -0
- package/dist/resolution/frameworks/go.js.map +1 -0
- package/dist/resolution/frameworks/goframe.d.ts +41 -0
- package/dist/resolution/frameworks/goframe.d.ts.map +1 -0
- package/dist/resolution/frameworks/goframe.js +112 -0
- package/dist/resolution/frameworks/goframe.js.map +1 -0
- package/dist/resolution/frameworks/index.d.ts +50 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -0
- package/dist/resolution/frameworks/index.js +175 -0
- package/dist/resolution/frameworks/index.js.map +1 -0
- package/dist/resolution/frameworks/java.d.ts +8 -0
- package/dist/resolution/frameworks/java.d.ts.map +1 -0
- package/dist/resolution/frameworks/java.js +517 -0
- package/dist/resolution/frameworks/java.js.map +1 -0
- package/dist/resolution/frameworks/laravel.d.ts +13 -0
- package/dist/resolution/frameworks/laravel.d.ts.map +1 -0
- package/dist/resolution/frameworks/laravel.js +257 -0
- package/dist/resolution/frameworks/laravel.js.map +1 -0
- package/dist/resolution/frameworks/nestjs.d.ts +26 -0
- package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
- package/dist/resolution/frameworks/nestjs.js +698 -0
- package/dist/resolution/frameworks/nestjs.js.map +1 -0
- package/dist/resolution/frameworks/play.d.ts +19 -0
- package/dist/resolution/frameworks/play.d.ts.map +1 -0
- package/dist/resolution/frameworks/play.js +111 -0
- package/dist/resolution/frameworks/play.js.map +1 -0
- package/dist/resolution/frameworks/python.d.ts +10 -0
- package/dist/resolution/frameworks/python.d.ts.map +1 -0
- package/dist/resolution/frameworks/python.js +400 -0
- package/dist/resolution/frameworks/python.js.map +1 -0
- package/dist/resolution/frameworks/react-native.d.ts +3 -0
- package/dist/resolution/frameworks/react-native.d.ts.map +1 -0
- package/dist/resolution/frameworks/react-native.js +410 -0
- package/dist/resolution/frameworks/react-native.js.map +1 -0
- package/dist/resolution/frameworks/react.d.ts +8 -0
- package/dist/resolution/frameworks/react.d.ts.map +1 -0
- package/dist/resolution/frameworks/react.js +334 -0
- package/dist/resolution/frameworks/react.js.map +1 -0
- package/dist/resolution/frameworks/ruby.d.ts +8 -0
- package/dist/resolution/frameworks/ruby.d.ts.map +1 -0
- package/dist/resolution/frameworks/ruby.js +302 -0
- package/dist/resolution/frameworks/ruby.js.map +1 -0
- package/dist/resolution/frameworks/rust.d.ts +8 -0
- package/dist/resolution/frameworks/rust.d.ts.map +1 -0
- package/dist/resolution/frameworks/rust.js +304 -0
- package/dist/resolution/frameworks/rust.js.map +1 -0
- package/dist/resolution/frameworks/svelte.d.ts +9 -0
- package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
- package/dist/resolution/frameworks/svelte.js +253 -0
- package/dist/resolution/frameworks/svelte.js.map +1 -0
- package/dist/resolution/frameworks/swift-objc.d.ts +37 -0
- package/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
- package/dist/resolution/frameworks/swift-objc.js +252 -0
- package/dist/resolution/frameworks/swift-objc.js.map +1 -0
- package/dist/resolution/frameworks/swift.d.ts +10 -0
- package/dist/resolution/frameworks/swift.d.ts.map +1 -0
- package/dist/resolution/frameworks/swift.js +400 -0
- package/dist/resolution/frameworks/swift.js.map +1 -0
- package/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/dist/resolution/frameworks/terraform.js +277 -0
- package/dist/resolution/frameworks/terraform.js.map +1 -0
- package/dist/resolution/frameworks/vue.d.ts +9 -0
- package/dist/resolution/frameworks/vue.d.ts.map +1 -0
- package/dist/resolution/frameworks/vue.js +303 -0
- package/dist/resolution/frameworks/vue.js.map +1 -0
- package/dist/resolution/go-module.d.ts +26 -0
- package/dist/resolution/go-module.d.ts.map +1 -0
- package/dist/resolution/go-module.js +78 -0
- package/dist/resolution/go-module.js.map +1 -0
- package/dist/resolution/goframe-synthesizer.d.ts +29 -0
- package/dist/resolution/goframe-synthesizer.d.ts.map +1 -0
- package/dist/resolution/goframe-synthesizer.js +163 -0
- package/dist/resolution/goframe-synthesizer.js.map +1 -0
- package/dist/resolution/import-resolver.d.ts +84 -0
- package/dist/resolution/import-resolver.d.ts.map +1 -0
- package/dist/resolution/import-resolver.js +2134 -0
- package/dist/resolution/import-resolver.js.map +1 -0
- package/dist/resolution/index.d.ts +343 -0
- package/dist/resolution/index.d.ts.map +1 -0
- package/dist/resolution/index.js +2339 -0
- package/dist/resolution/index.js.map +1 -0
- package/dist/resolution/lru-cache.d.ts +24 -0
- package/dist/resolution/lru-cache.d.ts.map +1 -0
- package/dist/resolution/lru-cache.js +62 -0
- package/dist/resolution/lru-cache.js.map +1 -0
- package/dist/resolution/memory-budget.d.ts +48 -0
- package/dist/resolution/memory-budget.d.ts.map +1 -0
- package/dist/resolution/memory-budget.js +162 -0
- package/dist/resolution/memory-budget.js.map +1 -0
- package/dist/resolution/name-matcher.d.ts +123 -0
- package/dist/resolution/name-matcher.d.ts.map +1 -0
- package/dist/resolution/name-matcher.js +2134 -0
- package/dist/resolution/name-matcher.js.map +1 -0
- package/dist/resolution/path-aliases.d.ts +68 -0
- package/dist/resolution/path-aliases.d.ts.map +1 -0
- package/dist/resolution/path-aliases.js +238 -0
- package/dist/resolution/path-aliases.js.map +1 -0
- package/dist/resolution/resolver-pool.d.ts +106 -0
- package/dist/resolution/resolver-pool.d.ts.map +1 -0
- package/dist/resolution/resolver-pool.js +344 -0
- package/dist/resolution/resolver-pool.js.map +1 -0
- package/dist/resolution/resolver-worker.d.ts +17 -0
- package/dist/resolution/resolver-worker.d.ts.map +1 -0
- package/dist/resolution/resolver-worker.js +141 -0
- package/dist/resolution/resolver-worker.js.map +1 -0
- package/dist/resolution/strip-comments.d.ts +27 -0
- package/dist/resolution/strip-comments.d.ts.map +1 -0
- package/dist/resolution/strip-comments.js +492 -0
- package/dist/resolution/strip-comments.js.map +1 -0
- package/dist/resolution/swift-objc-bridge.d.ts +134 -0
- package/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
- package/dist/resolution/swift-objc-bridge.js +256 -0
- package/dist/resolution/swift-objc-bridge.js.map +1 -0
- package/dist/resolution/types.d.ts +265 -0
- package/dist/resolution/types.d.ts.map +1 -0
- package/dist/resolution/types.js +8 -0
- package/dist/resolution/types.js.map +1 -0
- package/dist/resolution/workspace-packages.d.ts +58 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -0
- package/dist/resolution/workspace-packages.js +346 -0
- package/dist/resolution/workspace-packages.js.map +1 -0
- package/dist/search/identifier-segments.d.ts +60 -0
- package/dist/search/identifier-segments.d.ts.map +1 -0
- package/dist/search/identifier-segments.js +176 -0
- package/dist/search/identifier-segments.js.map +1 -0
- package/dist/search/query-parser.d.ts +57 -0
- package/dist/search/query-parser.d.ts.map +1 -0
- package/dist/search/query-parser.js +177 -0
- package/dist/search/query-parser.js.map +1 -0
- package/dist/search/query-utils.d.ts +87 -0
- package/dist/search/query-utils.d.ts.map +1 -0
- package/dist/search/query-utils.js +450 -0
- package/dist/search/query-utils.js.map +1 -0
- package/dist/sync/git-hooks.d.ts +45 -0
- package/dist/sync/git-hooks.d.ts.map +1 -0
- package/dist/sync/git-hooks.js +227 -0
- package/dist/sync/git-hooks.js.map +1 -0
- package/dist/sync/index.d.ts +19 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +35 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/watch-policy.d.ts +48 -0
- package/dist/sync/watch-policy.d.ts.map +1 -0
- package/dist/sync/watch-policy.js +124 -0
- package/dist/sync/watch-policy.js.map +1 -0
- package/dist/sync/watcher.d.ts +379 -0
- package/dist/sync/watcher.d.ts.map +1 -0
- package/dist/sync/watcher.js +921 -0
- package/dist/sync/watcher.js.map +1 -0
- package/dist/sync/worktree.d.ts +63 -0
- package/dist/sync/worktree.d.ts.map +1 -0
- package/dist/sync/worktree.js +182 -0
- package/dist/sync/worktree.js.map +1 -0
- package/dist/telemetry/index.d.ts +143 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +541 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/types.d.ts +465 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +117 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/color.d.ts +21 -0
- package/dist/ui/color.d.ts.map +1 -0
- package/dist/ui/color.js +42 -0
- package/dist/ui/color.js.map +1 -0
- package/dist/ui/glyphs.d.ts +67 -0
- package/dist/ui/glyphs.d.ts.map +1 -0
- package/dist/ui/glyphs.js +125 -0
- package/dist/ui/glyphs.js.map +1 -0
- package/dist/ui/shimmer-progress.d.ts +11 -0
- package/dist/ui/shimmer-progress.d.ts.map +1 -0
- package/dist/ui/shimmer-progress.js +164 -0
- package/dist/ui/shimmer-progress.js.map +1 -0
- package/dist/ui/shimmer-worker.d.ts +2 -0
- package/dist/ui/shimmer-worker.d.ts.map +1 -0
- package/dist/ui/shimmer-worker.js +126 -0
- package/dist/ui/shimmer-worker.js.map +1 -0
- package/dist/ui/types.d.ts +15 -0
- package/dist/ui/types.d.ts.map +1 -0
- package/dist/ui/types.js +3 -0
- package/dist/ui/types.js.map +1 -0
- package/dist/upgrade/index.d.ts +171 -0
- package/dist/upgrade/index.d.ts.map +1 -0
- package/dist/upgrade/index.js +659 -0
- package/dist/upgrade/index.js.map +1 -0
- package/dist/upgrade/remove-binary.d.ts +87 -0
- package/dist/upgrade/remove-binary.d.ts.map +1 -0
- package/dist/upgrade/remove-binary.js +289 -0
- package/dist/upgrade/remove-binary.js.map +1 -0
- package/dist/upgrade/update-check.d.ts +92 -0
- package/dist/upgrade/update-check.d.ts.map +1 -0
- package/dist/upgrade/update-check.js +258 -0
- package/dist/upgrade/update-check.js.map +1 -0
- package/dist/utils.d.ts +224 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +583 -0
- package/dist/utils.js.map +1 -0
- package/package.json +61 -0
- package/scripts/add-lang/bench.sh +60 -0
- package/scripts/add-lang/check-grammar.mjs +75 -0
- package/scripts/add-lang/dump-ast.mjs +103 -0
- package/scripts/add-lang/verify-extraction.mjs +70 -0
- package/scripts/agent-eval/ab-adoption.sh +91 -0
- package/scripts/agent-eval/ab-hook.sh +86 -0
- package/scripts/agent-eval/ab-impl.sh +78 -0
- package/scripts/agent-eval/ab-new-vs-baseline.sh +161 -0
- package/scripts/agent-eval/ab-sufficiency.sh +78 -0
- package/scripts/agent-eval/allocation-fixtures.json +344 -0
- package/scripts/agent-eval/arms-F.sh +21 -0
- package/scripts/agent-eval/arms-matrix.sh +37 -0
- package/scripts/agent-eval/audit.sh +68 -0
- package/scripts/agent-eval/bench-readme.sh +48 -0
- package/scripts/agent-eval/bench-why-repo.sh +22 -0
- package/scripts/agent-eval/block-read-hook.sh +19 -0
- package/scripts/agent-eval/compare-arms.mjs +209 -0
- package/scripts/agent-eval/diff-index-drift.mjs +102 -0
- package/scripts/agent-eval/hook-settings.json +15 -0
- package/scripts/agent-eval/itrun.sh +120 -0
- package/scripts/agent-eval/no-cli-shim.sh +96 -0
- package/scripts/agent-eval/offload-eval-3arm.sh +72 -0
- package/scripts/agent-eval/offload-eval-cost.mjs +133 -0
- package/scripts/agent-eval/offload-eval-effort.mjs +108 -0
- package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -0
- package/scripts/agent-eval/offload-eval-frontload.sh +47 -0
- package/scripts/agent-eval/offload-eval-ground-truth.json +18 -0
- package/scripts/agent-eval/offload-eval-hook.mjs +84 -0
- package/scripts/agent-eval/offload-eval-judge.mjs +103 -0
- package/scripts/agent-eval/offload-eval-matrix.sh +20 -0
- package/scripts/agent-eval/offload-eval-metrics.mjs +94 -0
- package/scripts/agent-eval/offload-eval-refs1.sh +50 -0
- package/scripts/agent-eval/offload-eval-setup.sh +24 -0
- package/scripts/agent-eval/offload-eval-styles.sh +72 -0
- package/scripts/agent-eval/offload-eval-summarize.mjs +68 -0
- package/scripts/agent-eval/offload-eval.md +76 -0
- package/scripts/agent-eval/parse-arms.mjs +116 -0
- package/scripts/agent-eval/parse-bench-readme.mjs +238 -0
- package/scripts/agent-eval/parse-run.mjs +1401 -0
- package/scripts/agent-eval/parse-session.mjs +130 -0
- package/scripts/agent-eval/probe-allocation.mjs +335 -0
- package/scripts/agent-eval/probe-context.mjs +21 -0
- package/scripts/agent-eval/probe-decl-only.mjs +198 -0
- package/scripts/agent-eval/probe-explore.mjs +40 -0
- package/scripts/agent-eval/probe-factory-closure.mjs +147 -0
- package/scripts/agent-eval/probe-file-spend.mjs +195 -0
- package/scripts/agent-eval/probe-named-symbol.mjs +163 -0
- package/scripts/agent-eval/probe-node.mjs +20 -0
- package/scripts/agent-eval/probe-suite-envelope.mjs +124 -0
- package/scripts/agent-eval/probe-sweep.mjs +119 -0
- package/scripts/agent-eval/probe-trace.mjs +20 -0
- package/scripts/agent-eval/redirect-read-hook.sh +38 -0
- package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
- package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
- package/scripts/agent-eval/run-agent.sh +34 -0
- package/scripts/agent-eval/run-all.sh +149 -0
- package/scripts/agent-eval/run-arms.sh +56 -0
- package/scripts/agent-eval/seq-matrix.mjs +137 -0
- package/scripts/build-bundle.sh +145 -0
- package/scripts/build-kernel.sh +87 -0
- package/scripts/dump-graph.mjs +57 -0
- package/scripts/extract-release-notes.mjs +130 -0
- package/scripts/kernel-parity.mjs +283 -0
- package/scripts/local-install.sh +41 -0
- package/scripts/npm-sdk.js +75 -0
- package/scripts/npm-shim.js +279 -0
- package/scripts/pack-npm.sh +123 -0
- package/scripts/prepare-release.mjs +270 -0
|
@@ -0,0 +1,2435 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Database Queries
|
|
4
|
+
*
|
|
5
|
+
* Prepared statements for CRUD operations on the knowledge graph.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.QueryBuilder = void 0;
|
|
9
|
+
const utils_1 = require("../utils");
|
|
10
|
+
const query_utils_1 = require("../search/query-utils");
|
|
11
|
+
const query_parser_1 = require("../search/query-parser");
|
|
12
|
+
const generated_detection_1 = require("../extraction/generated-detection");
|
|
13
|
+
const identifier_segments_1 = require("../search/identifier-segments");
|
|
14
|
+
/**
|
|
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).
|
|
24
|
+
*/
|
|
25
|
+
function isLowValueFile(filePath, generated) {
|
|
26
|
+
if (generated?.has(filePath))
|
|
27
|
+
return true;
|
|
28
|
+
const lp = filePath.toLowerCase();
|
|
29
|
+
return (/(?:^|\/)(tests?|__tests?__|spec)\//.test(lp) ||
|
|
30
|
+
/_test\.go$/.test(lp) ||
|
|
31
|
+
/(?:^|\/)test_[^/]+\.py$/.test(lp) ||
|
|
32
|
+
/_test\.py$/.test(lp) ||
|
|
33
|
+
/_spec\.rb$/.test(lp) ||
|
|
34
|
+
/_test\.rb$/.test(lp) ||
|
|
35
|
+
/\.(test|spec)\.[jt]sx?$/.test(lp) ||
|
|
36
|
+
/(test|spec|tests)\.(java|kt|scala)$/.test(lp) ||
|
|
37
|
+
/(tests?|spec)\.cs$/.test(lp) ||
|
|
38
|
+
/tests?\.swift$/.test(lp) ||
|
|
39
|
+
/_test\.dart$/.test(lp) ||
|
|
40
|
+
(0, generated_detection_1.isGeneratedFile)(filePath));
|
|
41
|
+
}
|
|
42
|
+
const SQLITE_PARAM_CHUNK_SIZE = 500;
|
|
43
|
+
/**
|
|
44
|
+
* Last segment of a (possibly dotted/qualified) reference name — the part a
|
|
45
|
+
* new symbol's plain node name could match: 'util.greet' → 'greet',
|
|
46
|
+
* 'mod::fn' → 'fn', 'greet' → 'greet'. Written to unresolved_refs.name_tail
|
|
47
|
+
* when a ref is marked failed, so the #1240 retry lookup can match dotted
|
|
48
|
+
* refs against newly-added node names.
|
|
49
|
+
*/
|
|
50
|
+
function referenceNameTail(referenceName) {
|
|
51
|
+
const idx = Math.max(referenceName.lastIndexOf('.'), referenceName.lastIndexOf(':'));
|
|
52
|
+
return idx >= 0 ? referenceName.slice(idx + 1) : referenceName;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Convert database row to Node object
|
|
56
|
+
*/
|
|
57
|
+
function rowToNode(row) {
|
|
58
|
+
return {
|
|
59
|
+
id: row.id,
|
|
60
|
+
kind: row.kind,
|
|
61
|
+
name: row.name,
|
|
62
|
+
qualifiedName: row.qualified_name,
|
|
63
|
+
filePath: row.file_path,
|
|
64
|
+
language: row.language,
|
|
65
|
+
startLine: row.start_line,
|
|
66
|
+
endLine: row.end_line,
|
|
67
|
+
startColumn: row.start_column,
|
|
68
|
+
endColumn: row.end_column,
|
|
69
|
+
docstring: row.docstring ?? undefined,
|
|
70
|
+
signature: row.signature ?? undefined,
|
|
71
|
+
visibility: row.visibility,
|
|
72
|
+
isExported: row.is_exported === 1,
|
|
73
|
+
isAsync: row.is_async === 1,
|
|
74
|
+
isStatic: row.is_static === 1,
|
|
75
|
+
isAbstract: row.is_abstract === 1,
|
|
76
|
+
decorators: row.decorators ? (0, utils_1.safeJsonParse)(row.decorators, undefined) : undefined,
|
|
77
|
+
typeParameters: row.type_parameters ? (0, utils_1.safeJsonParse)(row.type_parameters, undefined) : undefined,
|
|
78
|
+
returnType: row.return_type ?? undefined,
|
|
79
|
+
updatedAt: row.updated_at,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Convert database row to Edge object
|
|
84
|
+
*/
|
|
85
|
+
function rowToEdge(row) {
|
|
86
|
+
return {
|
|
87
|
+
source: row.source,
|
|
88
|
+
target: row.target,
|
|
89
|
+
kind: row.kind,
|
|
90
|
+
metadata: row.metadata ? (0, utils_1.safeJsonParse)(row.metadata, undefined) : undefined,
|
|
91
|
+
line: row.line ?? undefined,
|
|
92
|
+
column: row.col ?? undefined,
|
|
93
|
+
provenance: row.provenance,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Convert database row to FileRecord object
|
|
98
|
+
*/
|
|
99
|
+
function rowToFileRecord(row) {
|
|
100
|
+
return {
|
|
101
|
+
path: row.path,
|
|
102
|
+
contentHash: row.content_hash,
|
|
103
|
+
language: row.language,
|
|
104
|
+
size: row.size,
|
|
105
|
+
modifiedAt: row.modified_at,
|
|
106
|
+
indexedAt: row.indexed_at,
|
|
107
|
+
nodeCount: row.node_count,
|
|
108
|
+
errors: row.errors ? (0, utils_1.safeJsonParse)(row.errors, undefined) : undefined,
|
|
109
|
+
generated: row.generated === 1,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Query builder for the knowledge graph database
|
|
114
|
+
*/
|
|
115
|
+
class QueryBuilder {
|
|
116
|
+
db;
|
|
117
|
+
// Project-name tokens (go.mod / package.json / repo dir), normalized. A query
|
|
118
|
+
// word matching one is dropped from path-relevance scoring — it names the
|
|
119
|
+
// whole project, not a symbol, so it carries no discriminative signal (#720).
|
|
120
|
+
// Set once by the CodeGraph instance; empty by default (no down-weighting).
|
|
121
|
+
projectNameTokens = new Set();
|
|
122
|
+
// Node cache for frequently accessed nodes (LRU-style, max 1000 entries)
|
|
123
|
+
nodeCache = new Map();
|
|
124
|
+
maxCacheSize = 1000;
|
|
125
|
+
// Prepared statements (lazily initialized)
|
|
126
|
+
stmts = {};
|
|
127
|
+
// Names whose segments were already written this session — skips re-splitting
|
|
128
|
+
// and re-inserting for the same-named nodes that repeat across files ("get",
|
|
129
|
+
// "render", …). Purely a write-path fast path; INSERT OR IGNORE is the
|
|
130
|
+
// correctness backstop. Bounded so a pathological repo can't grow it forever.
|
|
131
|
+
segmentedNames = new Set();
|
|
132
|
+
static MAX_SEGMENTED_NAMES = 65536;
|
|
133
|
+
// Multi-row INSERT statements, cached per (statement kind × row count). The
|
|
134
|
+
// bulk write path decomposes N rows into a few fixed batch sizes so each
|
|
135
|
+
// size's statement is prepared once and reused — one .run() binds a whole
|
|
136
|
+
// chunk instead of one row, which is where the per-call overhead lives.
|
|
137
|
+
// Row order within and across chunks is the input order, so rowid assignment
|
|
138
|
+
// (and therefore resolution's insertion-order disambiguation) is identical
|
|
139
|
+
// to the one-row-per-run path.
|
|
140
|
+
batchStmts = new Map();
|
|
141
|
+
static BATCH_SIZES = [128, 32, 8, 1];
|
|
142
|
+
/**
|
|
143
|
+
* Run `rows` through a multi-row `INSERT` built as `head + (tuple,)*n`,
|
|
144
|
+
* decomposed greedily into the cached batch sizes. Preserves row order.
|
|
145
|
+
*/
|
|
146
|
+
runBatched(kind, head, tuple, rows) {
|
|
147
|
+
if (rows.length === 0)
|
|
148
|
+
return;
|
|
149
|
+
let i = 0;
|
|
150
|
+
for (const size of QueryBuilder.BATCH_SIZES) {
|
|
151
|
+
while (rows.length - i >= size) {
|
|
152
|
+
const key = `${kind}:${size}`;
|
|
153
|
+
let stmt = this.batchStmts.get(key);
|
|
154
|
+
if (!stmt) {
|
|
155
|
+
stmt = this.db.prepare(head + new Array(size).fill(tuple).join(','));
|
|
156
|
+
this.batchStmts.set(key, stmt);
|
|
157
|
+
}
|
|
158
|
+
if (size === 1) {
|
|
159
|
+
stmt.run(...rows[i]);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const params = [];
|
|
163
|
+
for (let r = 0; r < size; r++) {
|
|
164
|
+
const row = rows[i + r];
|
|
165
|
+
for (let c = 0; c < row.length; c++)
|
|
166
|
+
params.push(row[c]);
|
|
167
|
+
}
|
|
168
|
+
stmt.run(...params);
|
|
169
|
+
}
|
|
170
|
+
i += size;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
constructor(db) {
|
|
175
|
+
this.db = db;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Swap the underlying connection in place. Used by pool workers'
|
|
179
|
+
* connection recycling (plan §7a.6, writes-under-readers): a long-lived
|
|
180
|
+
* read connection pins WAL checkpoint progress, and the deep WAL that
|
|
181
|
+
* accumulates behind it taxes every main-thread B-tree page operation
|
|
182
|
+
* (deletes measured 42.6s → 118.8s from 0 to 4 attached readers on
|
|
183
|
+
* identical hardware). Workers therefore close and reopen their read-only
|
|
184
|
+
* connection at the pool-idle boundary; everything above the connection —
|
|
185
|
+
* this QueryBuilder, the resolver and its warm caches — survives, and only
|
|
186
|
+
* connection-derived state (prepared statements) resets, re-preparing
|
|
187
|
+
* lazily on next use.
|
|
188
|
+
*/
|
|
189
|
+
rebind(db) {
|
|
190
|
+
this.db = db;
|
|
191
|
+
this.stmts = {};
|
|
192
|
+
this.batchStmts.clear();
|
|
193
|
+
}
|
|
194
|
+
/** Set the normalized project-name tokens used to down-weight non-discriminative
|
|
195
|
+
* query words in path scoring (#720). Called once when the project opens. */
|
|
196
|
+
setProjectNameTokens(tokens) {
|
|
197
|
+
this.projectNameTokens = tokens;
|
|
198
|
+
}
|
|
199
|
+
/** The normalized project-name tokens (#720); empty if none were derived. */
|
|
200
|
+
getProjectNameTokens() {
|
|
201
|
+
return this.projectNameTokens;
|
|
202
|
+
}
|
|
203
|
+
// ===========================================================================
|
|
204
|
+
// Node Operations
|
|
205
|
+
// ===========================================================================
|
|
206
|
+
/**
|
|
207
|
+
* Insert a new node
|
|
208
|
+
*/
|
|
209
|
+
insertNode(node) {
|
|
210
|
+
if (!this.stmts.insertNode) {
|
|
211
|
+
this.stmts.insertNode = this.db.prepare(`
|
|
212
|
+
INSERT OR REPLACE INTO nodes (
|
|
213
|
+
id, kind, name, qualified_name, file_path, language,
|
|
214
|
+
start_line, end_line, start_column, end_column,
|
|
215
|
+
docstring, signature, visibility,
|
|
216
|
+
is_exported, is_async, is_static, is_abstract,
|
|
217
|
+
decorators, type_parameters, return_type, updated_at
|
|
218
|
+
) VALUES (
|
|
219
|
+
@id, @kind, @name, @qualifiedName, @filePath, @language,
|
|
220
|
+
@startLine, @endLine, @startColumn, @endColumn,
|
|
221
|
+
@docstring, @signature, @visibility,
|
|
222
|
+
@isExported, @isAsync, @isStatic, @isAbstract,
|
|
223
|
+
@decorators, @typeParameters, @returnType, @updatedAt
|
|
224
|
+
)
|
|
225
|
+
`);
|
|
226
|
+
}
|
|
227
|
+
// Validate required fields to prevent SQLite bind errors
|
|
228
|
+
if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
|
|
229
|
+
console.error('[CodeGraph] Skipping node with missing required fields:', {
|
|
230
|
+
id: node.id,
|
|
231
|
+
kind: node.kind,
|
|
232
|
+
name: node.name,
|
|
233
|
+
filePath: node.filePath,
|
|
234
|
+
language: node.language,
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// INSERT OR REPLACE may overwrite a node we have cached. Drop the
|
|
239
|
+
// stale entry so the next getNodeById sees the new row, not the old
|
|
240
|
+
// one (matches the cache-invalidation pattern used by updateNode and
|
|
241
|
+
// deleteNode below).
|
|
242
|
+
this.nodeCache.delete(node.id);
|
|
243
|
+
this.stmts.insertNode.run({
|
|
244
|
+
id: node.id,
|
|
245
|
+
kind: node.kind,
|
|
246
|
+
name: node.name,
|
|
247
|
+
qualifiedName: node.qualifiedName ?? node.name,
|
|
248
|
+
filePath: node.filePath,
|
|
249
|
+
language: node.language,
|
|
250
|
+
startLine: node.startLine ?? 0,
|
|
251
|
+
endLine: node.endLine ?? 0,
|
|
252
|
+
startColumn: node.startColumn ?? 0,
|
|
253
|
+
endColumn: node.endColumn ?? 0,
|
|
254
|
+
docstring: node.docstring ?? null,
|
|
255
|
+
signature: node.signature ?? null,
|
|
256
|
+
visibility: node.visibility ?? null,
|
|
257
|
+
isExported: node.isExported ? 1 : 0,
|
|
258
|
+
isAsync: node.isAsync ? 1 : 0,
|
|
259
|
+
isStatic: node.isStatic ? 1 : 0,
|
|
260
|
+
isAbstract: node.isAbstract ? 1 : 0,
|
|
261
|
+
decorators: node.decorators ? JSON.stringify(node.decorators) : null,
|
|
262
|
+
typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
|
|
263
|
+
returnType: node.returnType ?? null,
|
|
264
|
+
updatedAt: node.updatedAt ?? Date.now(),
|
|
265
|
+
});
|
|
266
|
+
// Segment vocabulary rides the same write path (and transaction) so it can
|
|
267
|
+
// never drift ahead of the nodes it describes. Deletes intentionally leave
|
|
268
|
+
// orphans behind — vocab rows are proposals re-verified against nodes
|
|
269
|
+
// before use, and a full index clears the table at its start. File nodes
|
|
270
|
+
// are excluded: a file's basename duplicates the symbols inside it
|
|
271
|
+
// (state-machine.ts / OrderStateMachine), which double-counts every
|
|
272
|
+
// concept and defeats the singleton-vs-cluster rarity statistics. Import
|
|
273
|
+
// nodes are excluded too (#1144): they're named after module specifiers
|
|
274
|
+
// ("external-unindexed-pkg", "./utils/helpers"), not symbols — an
|
|
275
|
+
// import-only name can never be surfaced (getSegmentMatches requires a
|
|
276
|
+
// real definition), so its rows would only inflate the rarity statistics.
|
|
277
|
+
if (this.isSegmentableKind(node.kind))
|
|
278
|
+
this.insertNameSegments(node.name);
|
|
279
|
+
}
|
|
280
|
+
/** Which node kinds contribute their name to the segment vocabulary — the
|
|
281
|
+
* single gate shared by insertNode, updateNode, and the rebuild page query
|
|
282
|
+
* (getDistinctNodeNames), so the write paths can't drift apart. */
|
|
283
|
+
isSegmentableKind(kind) {
|
|
284
|
+
return kind !== 'file' && kind !== 'import';
|
|
285
|
+
}
|
|
286
|
+
/** Write `name`'s segments into name_segment_vocab (idempotent). */
|
|
287
|
+
insertNameSegments(name) {
|
|
288
|
+
const rows = [];
|
|
289
|
+
this.collectNameSegmentRows(name, rows);
|
|
290
|
+
this.runBatched('insertNameSegments', 'INSERT OR IGNORE INTO name_segment_vocab (segment, name) VALUES ', '(?,?)', rows);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Insert multiple nodes in a transaction
|
|
294
|
+
*/
|
|
295
|
+
insertNodes(nodes) {
|
|
296
|
+
this.db.transaction(() => {
|
|
297
|
+
// Bulk path: same semantics as insertNode() per row (validation, cache
|
|
298
|
+
// invalidation, segment vocab), but bound as multi-row INSERTs — the
|
|
299
|
+
// per-.run() call overhead dominates the store phase on full indexes.
|
|
300
|
+
const rows = [];
|
|
301
|
+
const segmentRows = [];
|
|
302
|
+
for (const node of nodes) {
|
|
303
|
+
if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
|
|
304
|
+
console.error('[CodeGraph] Skipping node with missing required fields:', {
|
|
305
|
+
id: node.id,
|
|
306
|
+
kind: node.kind,
|
|
307
|
+
name: node.name,
|
|
308
|
+
filePath: node.filePath,
|
|
309
|
+
language: node.language,
|
|
310
|
+
});
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
this.nodeCache.delete(node.id);
|
|
314
|
+
rows.push([
|
|
315
|
+
node.id,
|
|
316
|
+
node.kind,
|
|
317
|
+
node.name,
|
|
318
|
+
node.qualifiedName ?? node.name,
|
|
319
|
+
node.filePath,
|
|
320
|
+
node.language,
|
|
321
|
+
node.startLine ?? 0,
|
|
322
|
+
node.endLine ?? 0,
|
|
323
|
+
node.startColumn ?? 0,
|
|
324
|
+
node.endColumn ?? 0,
|
|
325
|
+
node.docstring ?? null,
|
|
326
|
+
node.signature ?? null,
|
|
327
|
+
node.visibility ?? null,
|
|
328
|
+
node.isExported ? 1 : 0,
|
|
329
|
+
node.isAsync ? 1 : 0,
|
|
330
|
+
node.isStatic ? 1 : 0,
|
|
331
|
+
node.isAbstract ? 1 : 0,
|
|
332
|
+
node.decorators ? JSON.stringify(node.decorators) : null,
|
|
333
|
+
node.typeParameters ? JSON.stringify(node.typeParameters) : null,
|
|
334
|
+
node.returnType ?? null,
|
|
335
|
+
node.updatedAt ?? Date.now(),
|
|
336
|
+
]);
|
|
337
|
+
if (this.isSegmentableKind(node.kind))
|
|
338
|
+
this.collectNameSegmentRows(node.name, segmentRows);
|
|
339
|
+
}
|
|
340
|
+
this.runBatched('insertNodes', `INSERT OR REPLACE INTO nodes (
|
|
341
|
+
id, kind, name, qualified_name, file_path, language,
|
|
342
|
+
start_line, end_line, start_column, end_column,
|
|
343
|
+
docstring, signature, visibility,
|
|
344
|
+
is_exported, is_async, is_static, is_abstract,
|
|
345
|
+
decorators, type_parameters, return_type, updated_at
|
|
346
|
+
) VALUES `, '(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', rows);
|
|
347
|
+
this.runBatched('insertNameSegments', 'INSERT OR IGNORE INTO name_segment_vocab (segment, name) VALUES ', '(?,?)', segmentRows);
|
|
348
|
+
})();
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Store one file's whole extraction bundle — nodes, edges, unresolved refs,
|
|
352
|
+
* and the file record — in a SINGLE transaction. The bulk-index path calls
|
|
353
|
+
* this once per file instead of opening one transaction per table (#1015
|
|
354
|
+
* file-order commit discipline is unchanged: callers still invoke it in file
|
|
355
|
+
* order, and row order within is input order).
|
|
356
|
+
*
|
|
357
|
+
* Edges MUST already be endpoint-filtered by the caller (the store path
|
|
358
|
+
* filters to the file's own inserted node ids), so the per-file existence
|
|
359
|
+
* SELECT that insertEdges() pays is skipped here.
|
|
360
|
+
*/
|
|
361
|
+
storeFileBundle(bundle) {
|
|
362
|
+
this.db.transaction(() => {
|
|
363
|
+
this.insertNodes(bundle.nodes);
|
|
364
|
+
if (bundle.edges.length > 0) {
|
|
365
|
+
const rows = [];
|
|
366
|
+
for (const edge of bundle.edges) {
|
|
367
|
+
rows.push([
|
|
368
|
+
edge.source,
|
|
369
|
+
edge.target,
|
|
370
|
+
edge.kind,
|
|
371
|
+
edge.metadata ? JSON.stringify(edge.metadata) : null,
|
|
372
|
+
edge.line ?? null,
|
|
373
|
+
edge.column ?? null,
|
|
374
|
+
edge.provenance ?? null,
|
|
375
|
+
]);
|
|
376
|
+
}
|
|
377
|
+
this.runBatched('insertEdges', 'INSERT OR IGNORE INTO edges (source, target, kind, metadata, line, col, provenance) VALUES ', '(?,?,?,?,?,?,?)', rows);
|
|
378
|
+
}
|
|
379
|
+
if (bundle.refs.length > 0)
|
|
380
|
+
this.insertUnresolvedRefsBatch(bundle.refs);
|
|
381
|
+
this.upsertFile(bundle.file);
|
|
382
|
+
})();
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Collect (segment, name) rows for a name, honouring the same session-dedupe
|
|
386
|
+
* semantics as insertNameSegments(). Shared by the bulk write paths.
|
|
387
|
+
*/
|
|
388
|
+
collectNameSegmentRows(name, out) {
|
|
389
|
+
if (this.segmentedNames.has(name))
|
|
390
|
+
return;
|
|
391
|
+
if (this.segmentedNames.size >= QueryBuilder.MAX_SEGMENTED_NAMES)
|
|
392
|
+
this.segmentedNames.clear();
|
|
393
|
+
this.segmentedNames.add(name);
|
|
394
|
+
for (const segment of (0, identifier_segments_1.splitIdentifierSegments)(name))
|
|
395
|
+
out.push([segment, name]);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Update an existing node
|
|
399
|
+
*/
|
|
400
|
+
updateNode(node) {
|
|
401
|
+
if (!this.stmts.updateNode) {
|
|
402
|
+
this.stmts.updateNode = this.db.prepare(`
|
|
403
|
+
UPDATE nodes SET
|
|
404
|
+
kind = @kind,
|
|
405
|
+
name = @name,
|
|
406
|
+
qualified_name = @qualifiedName,
|
|
407
|
+
file_path = @filePath,
|
|
408
|
+
language = @language,
|
|
409
|
+
start_line = @startLine,
|
|
410
|
+
end_line = @endLine,
|
|
411
|
+
start_column = @startColumn,
|
|
412
|
+
end_column = @endColumn,
|
|
413
|
+
docstring = @docstring,
|
|
414
|
+
signature = @signature,
|
|
415
|
+
visibility = @visibility,
|
|
416
|
+
is_exported = @isExported,
|
|
417
|
+
is_async = @isAsync,
|
|
418
|
+
is_static = @isStatic,
|
|
419
|
+
is_abstract = @isAbstract,
|
|
420
|
+
decorators = @decorators,
|
|
421
|
+
type_parameters = @typeParameters,
|
|
422
|
+
return_type = @returnType,
|
|
423
|
+
updated_at = @updatedAt
|
|
424
|
+
WHERE id = @id
|
|
425
|
+
`);
|
|
426
|
+
}
|
|
427
|
+
// Invalidate cache before update
|
|
428
|
+
this.nodeCache.delete(node.id);
|
|
429
|
+
// Validate required fields
|
|
430
|
+
if (!node.id || !node.kind || !node.name || !node.filePath || !node.language) {
|
|
431
|
+
console.error('[CodeGraph] Skipping node update with missing required fields:', node.id);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
this.stmts.updateNode.run({
|
|
435
|
+
id: node.id,
|
|
436
|
+
kind: node.kind,
|
|
437
|
+
name: node.name,
|
|
438
|
+
qualifiedName: node.qualifiedName ?? node.name,
|
|
439
|
+
filePath: node.filePath,
|
|
440
|
+
language: node.language,
|
|
441
|
+
startLine: node.startLine ?? 0,
|
|
442
|
+
endLine: node.endLine ?? 0,
|
|
443
|
+
startColumn: node.startColumn ?? 0,
|
|
444
|
+
endColumn: node.endColumn ?? 0,
|
|
445
|
+
docstring: node.docstring ?? null,
|
|
446
|
+
signature: node.signature ?? null,
|
|
447
|
+
visibility: node.visibility ?? null,
|
|
448
|
+
isExported: node.isExported ? 1 : 0,
|
|
449
|
+
isAsync: node.isAsync ? 1 : 0,
|
|
450
|
+
isStatic: node.isStatic ? 1 : 0,
|
|
451
|
+
isAbstract: node.isAbstract ? 1 : 0,
|
|
452
|
+
decorators: node.decorators ? JSON.stringify(node.decorators) : null,
|
|
453
|
+
typeParameters: node.typeParameters ? JSON.stringify(node.typeParameters) : null,
|
|
454
|
+
returnType: node.returnType ?? null,
|
|
455
|
+
updatedAt: node.updatedAt ?? Date.now(),
|
|
456
|
+
});
|
|
457
|
+
// updateNode is a second real write path to `nodes` — framework
|
|
458
|
+
// post-extract passes rewrite names through it (NestJS route prefixing),
|
|
459
|
+
// and a renamed node's new name must reach the segment vocabulary just
|
|
460
|
+
// like an inserted one's (#1141). Without this the rename left the new
|
|
461
|
+
// name permanently unsearchable: the old name's rows became honest-gate
|
|
462
|
+
// orphans and the only backfill is gated on the vocab being EMPTY.
|
|
463
|
+
// insertNameSegments is idempotent (in-memory set + INSERT OR IGNORE),
|
|
464
|
+
// so no name-changed check is needed.
|
|
465
|
+
if (this.isSegmentableKind(node.kind))
|
|
466
|
+
this.insertNameSegments(node.name);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Delete a node by ID
|
|
470
|
+
*/
|
|
471
|
+
deleteNode(id) {
|
|
472
|
+
if (!this.stmts.deleteNode) {
|
|
473
|
+
this.stmts.deleteNode = this.db.prepare('DELETE FROM nodes WHERE id = ?');
|
|
474
|
+
}
|
|
475
|
+
// Invalidate cache
|
|
476
|
+
this.nodeCache.delete(id);
|
|
477
|
+
this.stmts.deleteNode.run(id);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Delete all nodes for a file
|
|
481
|
+
*/
|
|
482
|
+
deleteNodesByFile(filePath) {
|
|
483
|
+
if (!this.stmts.deleteNodesByFile) {
|
|
484
|
+
this.stmts.deleteNodesByFile = this.db.prepare('DELETE FROM nodes WHERE file_path = ?');
|
|
485
|
+
}
|
|
486
|
+
// Invalidate cache for nodes in this file
|
|
487
|
+
for (const [id, node] of this.nodeCache) {
|
|
488
|
+
if (node.filePath === filePath) {
|
|
489
|
+
this.nodeCache.delete(id);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
this.stmts.deleteNodesByFile.run(filePath);
|
|
493
|
+
}
|
|
494
|
+
// ===========================================================================
|
|
495
|
+
// Name-segment vocabulary (prompt-hook graph-derived gate)
|
|
496
|
+
// ===========================================================================
|
|
497
|
+
/** Wipe the segment vocabulary. A full index calls this at its start; the
|
|
498
|
+
* node write path repopulates it as files (re-)index, so the end state is
|
|
499
|
+
* exactly the current names with no orphan rows. */
|
|
500
|
+
clearNameSegmentVocab() {
|
|
501
|
+
this.db.exec('DELETE FROM name_segment_vocab');
|
|
502
|
+
this.segmentedNames.clear();
|
|
503
|
+
}
|
|
504
|
+
/** True when the vocab has no rows — an index built before the table existed.
|
|
505
|
+
* `sync` uses this to heal such databases (see rebuildNameSegmentVocabFrom). */
|
|
506
|
+
isNameSegmentVocabEmpty() {
|
|
507
|
+
const row = this.db.prepare('SELECT 1 FROM name_segment_vocab LIMIT 1').get();
|
|
508
|
+
return row === undefined;
|
|
509
|
+
}
|
|
510
|
+
/** One page of distinct segmentable node names, for batched vocab rebuilds
|
|
511
|
+
* (file basenames and import specifiers are excluded from the vocab — see
|
|
512
|
+
* insertNode). */
|
|
513
|
+
getDistinctNodeNames(limit, offset) {
|
|
514
|
+
const rows = this.db
|
|
515
|
+
.prepare("SELECT DISTINCT name FROM nodes WHERE kind NOT IN ('file', 'import') ORDER BY name LIMIT ? OFFSET ?")
|
|
516
|
+
.all(limit, offset);
|
|
517
|
+
return rows.map((r) => r.name);
|
|
518
|
+
}
|
|
519
|
+
/** Insert segments for a batch of names in one transaction (vocab heal path). */
|
|
520
|
+
insertNameSegmentsBatch(names) {
|
|
521
|
+
this.db.transaction(() => {
|
|
522
|
+
const rows = [];
|
|
523
|
+
for (const name of names)
|
|
524
|
+
this.collectNameSegmentRows(name, rows);
|
|
525
|
+
this.runBatched('insertNameSegments', 'INSERT OR IGNORE INTO name_segment_vocab (segment, name) VALUES ', '(?,?)', rows);
|
|
526
|
+
})();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Names whose segments cover at least `minWords` distinct PROMPT WORDS —
|
|
530
|
+
* the co-occurrence probe behind the prompt hook's medium tier: the words
|
|
531
|
+
* "state" and "machine" both being segments of `OrderStateMachine` is strong
|
|
532
|
+
* evidence the prompt names that symbol in prose. Ordered by coverage.
|
|
533
|
+
*
|
|
534
|
+
* Takes (segment variant → original word) pairs and folds variants back to
|
|
535
|
+
* their word INSIDE the SQL: a name matching both `service` and `services`
|
|
536
|
+
* counts ONE word, not two. Counting raw variants let plural-variant pairs
|
|
537
|
+
* of a single word tie with genuine two-word matches and — because ORDER
|
|
538
|
+
* BY/LIMIT run here, before any JS-side re-check — crowd a real match past
|
|
539
|
+
* the LIMIT on vocab-heavy repos (#1146).
|
|
540
|
+
*/
|
|
541
|
+
getSegmentCoOccurrence(variants, minWords, limit) {
|
|
542
|
+
if (variants.length === 0)
|
|
543
|
+
return [];
|
|
544
|
+
const placeholders = variants.map(() => '?').join(', ');
|
|
545
|
+
const whens = variants.map(() => 'WHEN ? THEN ?').join(' ');
|
|
546
|
+
const rows = this.db
|
|
547
|
+
.prepare(`SELECT name, COUNT(DISTINCT CASE segment ${whens} END) AS matches
|
|
548
|
+
FROM name_segment_vocab
|
|
549
|
+
WHERE segment IN (${placeholders})
|
|
550
|
+
GROUP BY name
|
|
551
|
+
HAVING matches >= ?
|
|
552
|
+
ORDER BY matches DESC, length(name) ASC
|
|
553
|
+
LIMIT ?`)
|
|
554
|
+
.all(...variants.flatMap((v) => [v.segment, v.word]), ...variants.map((v) => v.segment), minWords, limit);
|
|
555
|
+
return rows;
|
|
556
|
+
}
|
|
557
|
+
/** How many distinct names each segment appears in — the rarity signal that
|
|
558
|
+
* separates a discriminative word ("checkout") from a ubiquitous one ("state"). */
|
|
559
|
+
getSegmentNameCounts(segments) {
|
|
560
|
+
if (segments.length === 0)
|
|
561
|
+
return new Map();
|
|
562
|
+
const placeholders = segments.map(() => '?').join(', ');
|
|
563
|
+
const rows = this.db
|
|
564
|
+
.prepare(`SELECT segment, COUNT(*) AS n FROM name_segment_vocab
|
|
565
|
+
WHERE segment IN (${placeholders}) GROUP BY segment`)
|
|
566
|
+
.all(...segments);
|
|
567
|
+
return new Map(rows.map((r) => [r.segment, r.n]));
|
|
568
|
+
}
|
|
569
|
+
/** Names containing the given segment (rare-single-word tier). */
|
|
570
|
+
getNamesForSegment(segment, limit) {
|
|
571
|
+
const rows = this.db
|
|
572
|
+
.prepare('SELECT name FROM name_segment_vocab WHERE segment = ? ORDER BY length(name) ASC LIMIT ?')
|
|
573
|
+
.all(segment, limit);
|
|
574
|
+
return rows.map((r) => r.name);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Get a node by ID
|
|
578
|
+
*/
|
|
579
|
+
getNodeById(id) {
|
|
580
|
+
// Check cache first
|
|
581
|
+
if (this.nodeCache.has(id)) {
|
|
582
|
+
const cached = this.nodeCache.get(id);
|
|
583
|
+
// Move to end to implement LRU (delete and re-add)
|
|
584
|
+
this.nodeCache.delete(id);
|
|
585
|
+
this.nodeCache.set(id, cached);
|
|
586
|
+
return cached;
|
|
587
|
+
}
|
|
588
|
+
if (!this.stmts.getNodeById) {
|
|
589
|
+
this.stmts.getNodeById = this.db.prepare('SELECT * FROM nodes WHERE id = ?');
|
|
590
|
+
}
|
|
591
|
+
const row = this.stmts.getNodeById.get(id);
|
|
592
|
+
if (!row) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
const node = rowToNode(row);
|
|
596
|
+
this.cacheNode(node);
|
|
597
|
+
return node;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Batch lookup: fetch many nodes by ID in a single SQL round-trip.
|
|
601
|
+
*
|
|
602
|
+
* Replaces the N+1 pattern in graph traversal where every edge would
|
|
603
|
+
* trigger its own `getNodeById` call. For a function with 50 callers
|
|
604
|
+
* this collapses 50 point reads into one IN-list query (~10-50x
|
|
605
|
+
* faster end-to-end).
|
|
606
|
+
*
|
|
607
|
+
* Returns a Map keyed by id so callers can preserve their own ordering
|
|
608
|
+
* (typically the order edges were returned from the graph). Missing IDs
|
|
609
|
+
* are simply absent from the map.
|
|
610
|
+
*
|
|
611
|
+
* Cache-aware: ids already in the LRU cache are served from memory and
|
|
612
|
+
* the SQL query only touches the misses.
|
|
613
|
+
*/
|
|
614
|
+
getNodesByIds(ids) {
|
|
615
|
+
const out = new Map();
|
|
616
|
+
if (ids.length === 0)
|
|
617
|
+
return out;
|
|
618
|
+
// Serve cache hits first; build the miss list for SQL.
|
|
619
|
+
const misses = [];
|
|
620
|
+
for (const id of ids) {
|
|
621
|
+
const cached = this.nodeCache.get(id);
|
|
622
|
+
if (cached !== undefined) {
|
|
623
|
+
// LRU touch
|
|
624
|
+
this.nodeCache.delete(id);
|
|
625
|
+
this.nodeCache.set(id, cached);
|
|
626
|
+
out.set(id, cached);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
misses.push(id);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (misses.length === 0)
|
|
633
|
+
return out;
|
|
634
|
+
// Chunk under SQLite's parameter limit (default 999, raised to 32766
|
|
635
|
+
// in better-sqlite3 builds — chunk at 500 for safety across both
|
|
636
|
+
// backends and to keep the query plan simple).
|
|
637
|
+
for (let i = 0; i < misses.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
638
|
+
const chunk = misses.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
639
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
640
|
+
const rows = this.db
|
|
641
|
+
.prepare(`SELECT * FROM nodes WHERE id IN (${placeholders})`)
|
|
642
|
+
.all(...chunk);
|
|
643
|
+
for (const row of rows) {
|
|
644
|
+
const node = rowToNode(row);
|
|
645
|
+
out.set(node.id, node);
|
|
646
|
+
this.cacheNode(node);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
getExistingNodeIds(ids) {
|
|
652
|
+
const out = new Set();
|
|
653
|
+
if (ids.length === 0)
|
|
654
|
+
return out;
|
|
655
|
+
const uniqueIds = [...new Set(ids)];
|
|
656
|
+
for (let i = 0; i < uniqueIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
657
|
+
const chunk = uniqueIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
658
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
659
|
+
const rows = this.db
|
|
660
|
+
.prepare(`SELECT id FROM nodes WHERE id IN (${placeholders})`)
|
|
661
|
+
.all(...chunk);
|
|
662
|
+
for (const row of rows) {
|
|
663
|
+
out.add(row.id);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Add a node to the cache, evicting oldest if needed
|
|
670
|
+
*/
|
|
671
|
+
cacheNode(node) {
|
|
672
|
+
if (this.nodeCache.size >= this.maxCacheSize) {
|
|
673
|
+
// Evict oldest (first) entry
|
|
674
|
+
const firstKey = this.nodeCache.keys().next().value;
|
|
675
|
+
if (firstKey) {
|
|
676
|
+
this.nodeCache.delete(firstKey);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
this.nodeCache.set(node.id, node);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Clear the node cache
|
|
683
|
+
*/
|
|
684
|
+
clearCache() {
|
|
685
|
+
this.nodeCache.clear();
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Get all nodes in a file
|
|
689
|
+
*/
|
|
690
|
+
getNodesByFile(filePath) {
|
|
691
|
+
if (!this.stmts.getNodesByFile) {
|
|
692
|
+
this.stmts.getNodesByFile = this.db.prepare('SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line');
|
|
693
|
+
}
|
|
694
|
+
const rows = this.stmts.getNodesByFile.all(filePath);
|
|
695
|
+
return rows.map(rowToNode);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Find the file that holds the densest concentration of the project's
|
|
699
|
+
* internal call graph — the "core" file. Used by context-builder to
|
|
700
|
+
* boost ranking of symbols in that file's directory (so e.g. sinatra
|
|
701
|
+
* queries surface `lib/sinatra/base.rb`'s `route!` instead of
|
|
702
|
+
* `sinatra-contrib/lib/sinatra/multi_route.rb`'s `route` extension).
|
|
703
|
+
*
|
|
704
|
+
* Returns null if no file has a meaningful concentration (e.g. spread
|
|
705
|
+
* evenly across many files, or empty index).
|
|
706
|
+
*
|
|
707
|
+
* "Internal" = source and target are in the same file. Cross-file
|
|
708
|
+
* edges aren't useful here — they don't tell us which file is the
|
|
709
|
+
* functional center.
|
|
710
|
+
*
|
|
711
|
+
* Excludes test/spec files from candidacy via path-pattern. The agent's
|
|
712
|
+
* typical question is "how does X work", not "how is X tested", so
|
|
713
|
+
* boosting a test file's directory would be a misfire.
|
|
714
|
+
*/
|
|
715
|
+
getDominantFile() {
|
|
716
|
+
if (!this.stmts.getDominantFile) {
|
|
717
|
+
// Pull top 20 candidates; we then filter out test/generated files
|
|
718
|
+
// in code (regex-grade matching that SQL LIKE can't express). The
|
|
719
|
+
// generated-file filter is critical — without it, etcd's
|
|
720
|
+
// `api/etcdserverpb/rpc.pb.go` (1916 in-file edges, generated
|
|
721
|
+
// protobuf stub) outranks the real `server/etcdserver/server.go`
|
|
722
|
+
// (470 edges) by 4×, and the boost would push the agent toward
|
|
723
|
+
// generated code.
|
|
724
|
+
this.stmts.getDominantFile = this.db.prepare(`
|
|
725
|
+
SELECT n.file_path AS file_path, COUNT(*) AS edge_count
|
|
726
|
+
FROM edges e
|
|
727
|
+
JOIN nodes n ON e.source = n.id
|
|
728
|
+
JOIN nodes m ON e.target = m.id
|
|
729
|
+
WHERE n.file_path = m.file_path
|
|
730
|
+
GROUP BY n.file_path
|
|
731
|
+
ORDER BY edge_count DESC
|
|
732
|
+
LIMIT 20
|
|
733
|
+
`);
|
|
734
|
+
}
|
|
735
|
+
const rows = this.stmts.getDominantFile.all();
|
|
736
|
+
const generated = this.getGeneratedPathsAmong(rows.map(r => r.file_path));
|
|
737
|
+
const filtered = rows.filter(r => !isLowValueFile(r.file_path, generated));
|
|
738
|
+
if (filtered.length === 0 || filtered[0].edge_count < 20)
|
|
739
|
+
return null;
|
|
740
|
+
return {
|
|
741
|
+
filePath: filtered[0].file_path,
|
|
742
|
+
edgeCount: filtered[0].edge_count,
|
|
743
|
+
nextEdgeCount: filtered[1]?.edge_count ?? 0,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Find the file that holds the densest concentration of the project's
|
|
748
|
+
* `route` nodes (framework-emitted: Express/Gin/Flask/Rails/Drupal/etc.).
|
|
749
|
+
* Used by handleContext on small repos to inline the project's routing
|
|
750
|
+
* config when the agent's query is about request flow — eliminating the
|
|
751
|
+
* "Glob + Read routes.rb" pattern that beats codegraph on tiny realworld
|
|
752
|
+
* template repos.
|
|
753
|
+
*
|
|
754
|
+
* Excludes test/generated files from candidacy. Returns null if there
|
|
755
|
+
* are fewer than 3 non-test routes total, or if no file holds at least
|
|
756
|
+
* 30% of them (diffuse routing → no single answer file).
|
|
757
|
+
*/
|
|
758
|
+
getTopRouteFile() {
|
|
759
|
+
if (!this.stmts.getTopRouteFile) {
|
|
760
|
+
this.stmts.getTopRouteFile = this.db.prepare(`
|
|
761
|
+
SELECT file_path, COUNT(*) AS cnt
|
|
762
|
+
FROM nodes
|
|
763
|
+
WHERE kind = 'route'
|
|
764
|
+
GROUP BY file_path
|
|
765
|
+
ORDER BY cnt DESC
|
|
766
|
+
LIMIT 20
|
|
767
|
+
`);
|
|
768
|
+
}
|
|
769
|
+
const rows = this.stmts.getTopRouteFile.all();
|
|
770
|
+
const generated = this.getGeneratedPathsAmong(rows.map(r => r.file_path));
|
|
771
|
+
const filtered = rows.filter(r => !isLowValueFile(r.file_path, generated));
|
|
772
|
+
if (filtered.length === 0)
|
|
773
|
+
return null;
|
|
774
|
+
const totalRoutes = filtered.reduce((sum, r) => sum + r.cnt, 0);
|
|
775
|
+
const top = filtered[0];
|
|
776
|
+
if (totalRoutes < 3 || top.cnt < 3)
|
|
777
|
+
return null;
|
|
778
|
+
if (top.cnt / totalRoutes < 0.30)
|
|
779
|
+
return null;
|
|
780
|
+
return { filePath: top.file_path, routeCount: top.cnt, totalRoutes };
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Build a URL → handler manifest from the index. Each route node's
|
|
784
|
+
* `references` edge points at the function/method that handles the
|
|
785
|
+
* request. We join them in one pass; the agent gets the canonical
|
|
786
|
+
* routing answer ("POST /users/login → AuthController#login") without
|
|
787
|
+
* having to parse the framework's route DSL itself.
|
|
788
|
+
*
|
|
789
|
+
* Also returns the file with the most handler endpoints — used as the
|
|
790
|
+
* "top handler file" to inline source for, so the agent has both the
|
|
791
|
+
* mapping AND the handler implementations.
|
|
792
|
+
*/
|
|
793
|
+
getRoutingManifest(limit = 40) {
|
|
794
|
+
if (!this.stmts.getRoutingManifest) {
|
|
795
|
+
// Edge kind varies across framework resolvers: Spring/Rails/
|
|
796
|
+
// Laravel/Drupal emit `references`, Express emits `calls`. Accept
|
|
797
|
+
// both — the semantic is the same (route → its handler).
|
|
798
|
+
this.stmts.getRoutingManifest = this.db.prepare(`
|
|
799
|
+
SELECT
|
|
800
|
+
r.name AS url,
|
|
801
|
+
h.name AS handler,
|
|
802
|
+
h.file_path AS handler_file,
|
|
803
|
+
h.start_line AS handler_line,
|
|
804
|
+
h.kind AS handler_kind
|
|
805
|
+
FROM nodes r
|
|
806
|
+
JOIN edges e ON e.source = r.id
|
|
807
|
+
JOIN nodes h ON e.target = h.id
|
|
808
|
+
WHERE r.kind = 'route'
|
|
809
|
+
AND e.kind IN ('references', 'calls')
|
|
810
|
+
AND h.kind IN ('function', 'method', 'class')
|
|
811
|
+
ORDER BY r.file_path, r.start_line
|
|
812
|
+
LIMIT ?
|
|
813
|
+
`);
|
|
814
|
+
}
|
|
815
|
+
const rows = this.stmts.getRoutingManifest.all(limit);
|
|
816
|
+
// Drop test/generated handlers — same hygiene as elsewhere.
|
|
817
|
+
const generated = this.getGeneratedPathsAmong(rows.map(r => r.handler_file));
|
|
818
|
+
const filtered = rows.filter(r => !isLowValueFile(r.handler_file, generated));
|
|
819
|
+
if (filtered.length < 3)
|
|
820
|
+
return null;
|
|
821
|
+
// Identify the file holding the most handlers (the "primary handler file").
|
|
822
|
+
const fileCounts = new Map();
|
|
823
|
+
for (const r of filtered) {
|
|
824
|
+
fileCounts.set(r.handler_file, (fileCounts.get(r.handler_file) ?? 0) + 1);
|
|
825
|
+
}
|
|
826
|
+
let topHandlerFile = null;
|
|
827
|
+
let topHandlerFileCount = 0;
|
|
828
|
+
for (const [file, count] of fileCounts) {
|
|
829
|
+
if (count > topHandlerFileCount) {
|
|
830
|
+
topHandlerFile = file;
|
|
831
|
+
topHandlerFileCount = count;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
entries: filtered.map(r => ({
|
|
836
|
+
url: r.url,
|
|
837
|
+
handler: r.handler,
|
|
838
|
+
handlerFile: r.handler_file,
|
|
839
|
+
handlerLine: r.handler_line,
|
|
840
|
+
handlerKind: r.handler_kind,
|
|
841
|
+
})),
|
|
842
|
+
topHandlerFile,
|
|
843
|
+
topHandlerFileCount,
|
|
844
|
+
totalRoutes: filtered.length,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Get all nodes of a specific kind
|
|
849
|
+
*/
|
|
850
|
+
getNodesByKind(kind) {
|
|
851
|
+
if (!this.stmts.getNodesByKind) {
|
|
852
|
+
this.stmts.getNodesByKind = this.db.prepare('SELECT * FROM nodes WHERE kind = ?');
|
|
853
|
+
}
|
|
854
|
+
const rows = this.stmts.getNodesByKind.all(kind);
|
|
855
|
+
return rows.map(rowToNode);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Stream every node of a kind one at a time (lazy) instead of materializing
|
|
859
|
+
* them all like {@link getNodesByKind}. For unbounded kinds (`function`,
|
|
860
|
+
* `method`) on a symbol-dense project the full array is gigabytes; the
|
|
861
|
+
* dynamic-edge synthesizers only scan-and-filter, so they iterate to keep
|
|
862
|
+
* memory O(1) in the node count rather than O(nodes) (#610).
|
|
863
|
+
*/
|
|
864
|
+
*iterateNodesByKind(kind) {
|
|
865
|
+
// Fresh statement per call (not a cached one): an iterator holds an open
|
|
866
|
+
// cursor, so a shared statement would conflict across overlapping scans.
|
|
867
|
+
const stmt = this.db.prepare('SELECT * FROM nodes WHERE kind = ?');
|
|
868
|
+
for (const row of stmt.iterate(kind)) {
|
|
869
|
+
yield rowToNode(row);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Get all nodes in the database
|
|
874
|
+
*/
|
|
875
|
+
getAllNodes() {
|
|
876
|
+
const rows = this.db.prepare('SELECT * FROM nodes').all();
|
|
877
|
+
return rows.map(rowToNode);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Stream nodes of one language whose `decorators` JSON array contains
|
|
881
|
+
* `decorator`. The LIKE on the JSON text is a cheap index-free pre-filter
|
|
882
|
+
* (a decorator name can appear as a substring of another), so callers must
|
|
883
|
+
* still exact-check `node.decorators.includes(decorator)`. Exists so the
|
|
884
|
+
* kotlin expect/actual synthesizer never materializes the whole node table
|
|
885
|
+
* the way `getAllNodes().filter(...)` did — that array alone OOM'd Node's
|
|
886
|
+
* default heap on a 2M-node graph (#1212).
|
|
887
|
+
*/
|
|
888
|
+
*iterateNodesByLanguageWithDecorator(language, decorator) {
|
|
889
|
+
// Fresh statement per call — an iterator holds an open cursor (see
|
|
890
|
+
// iterateNodesByKind).
|
|
891
|
+
const stmt = this.db.prepare("SELECT * FROM nodes WHERE language = ? AND decorators LIKE '%' || ? || '%'");
|
|
892
|
+
for (const row of stmt.iterate(language, `"${decorator}"`)) {
|
|
893
|
+
yield rowToNode(row);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Distinct languages present in the files table. One indexed aggregate —
|
|
898
|
+
* lets the dynamic-edge synthesizers skip passes for languages the project
|
|
899
|
+
* doesn't contain at all (a Kotlin pass has no work on a pure-C repo), so
|
|
900
|
+
* their cost is zero rather than a full-graph scan that finds nothing (#1212).
|
|
901
|
+
*/
|
|
902
|
+
getDistinctFileLanguages() {
|
|
903
|
+
const rows = this.db.prepare('SELECT DISTINCT language FROM files').all();
|
|
904
|
+
return new Set(rows.map((r) => r.language));
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Get nodes by exact name match (uses idx_nodes_name index).
|
|
908
|
+
*
|
|
909
|
+
* This is resolution's candidate list, and the ORDER BY is load-bearing for
|
|
910
|
+
* index correctness, not cosmetic (CG-33). When a reference names a symbol
|
|
911
|
+
* that several files define and nothing disambiguates them, resolution binds
|
|
912
|
+
* to the first candidate — so without an ORDER BY the winner was decided by
|
|
913
|
+
* rowid, i.e. by the order files happened to be WRITTEN. A full index writes
|
|
914
|
+
* them in scan order; an incremental sync appends each file as it changes, so
|
|
915
|
+
* the same tree resolved to different edges depending on how the index was
|
|
916
|
+
* built, and a long-lived synced index drifted away from a rebuild of itself
|
|
917
|
+
* (measured at 4.3% of distinct edges, mostly `calls`).
|
|
918
|
+
*
|
|
919
|
+
* `(file_path, start_line)` is a property of the CODE, so both paths now pick
|
|
920
|
+
* the same candidate. The sort is paid once per distinct name per resolution
|
|
921
|
+
* run — ReferenceResolver memoizes this in its nameCache — and the population
|
|
922
|
+
* is capped by AMBIGUOUS_NAME_CEILING (#999).
|
|
923
|
+
*/
|
|
924
|
+
getNodesByName(name) {
|
|
925
|
+
if (!this.stmts.getNodesByName) {
|
|
926
|
+
this.stmts.getNodesByName = this.db.prepare('SELECT * FROM nodes WHERE name = ? ORDER BY file_path, start_line');
|
|
927
|
+
}
|
|
928
|
+
const rows = this.stmts.getNodesByName.all(name);
|
|
929
|
+
return rows.map(rowToNode);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Nodes whose name starts with `prefix`, by index range scan (a LIKE would
|
|
933
|
+
* skip idx_nodes_name under SQLite's default case-insensitive LIKE).
|
|
934
|
+
*/
|
|
935
|
+
getNodesByNamePrefix(prefix, limit = 20) {
|
|
936
|
+
if (!this.stmts.getNodesByNamePrefix) {
|
|
937
|
+
this.stmts.getNodesByNamePrefix = this.db.prepare('SELECT * FROM nodes WHERE name >= ? AND name < ? ORDER BY name LIMIT ?');
|
|
938
|
+
}
|
|
939
|
+
const rows = this.stmts.getNodesByNamePrefix.all(prefix, prefix + '', limit);
|
|
940
|
+
return rows.map(rowToNode);
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Get nodes by exact qualified name match (uses idx_nodes_qualified_name index)
|
|
944
|
+
*/
|
|
945
|
+
getNodesByQualifiedNameExact(qualifiedName) {
|
|
946
|
+
if (!this.stmts.getNodesByQualifiedNameExact) {
|
|
947
|
+
this.stmts.getNodesByQualifiedNameExact = this.db.prepare('SELECT * FROM nodes WHERE qualified_name = ?');
|
|
948
|
+
}
|
|
949
|
+
const rows = this.stmts.getNodesByQualifiedNameExact.all(qualifiedName);
|
|
950
|
+
return rows.map(rowToNode);
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Get nodes by lowercase name match (uses idx_nodes_lower_name expression index)
|
|
954
|
+
*/
|
|
955
|
+
getNodesByLowerName(lowerName) {
|
|
956
|
+
if (!this.stmts.getNodesByLowerName) {
|
|
957
|
+
this.stmts.getNodesByLowerName = this.db.prepare('SELECT * FROM nodes WHERE lower(name) = ?');
|
|
958
|
+
}
|
|
959
|
+
const rows = this.stmts.getNodesByLowerName.all(lowerName);
|
|
960
|
+
return rows.map(rowToNode);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Search nodes by name using FTS with fallback to LIKE for better matching
|
|
964
|
+
*
|
|
965
|
+
* Search strategy:
|
|
966
|
+
* 1. Try FTS5 prefix match (query*) for word-start matching
|
|
967
|
+
* 2. If no results, try LIKE for substring matching (e.g., "signIn" finds "signInWithGoogle")
|
|
968
|
+
* 3. Score results based on match quality
|
|
969
|
+
*/
|
|
970
|
+
searchNodes(query, options = {}) {
|
|
971
|
+
const { limit = 100, offset = 0 } = options;
|
|
972
|
+
// Parse field-qualified bits out of the raw query (kind:, lang:,
|
|
973
|
+
// path:, name:). Anything not recognised stays in `text` and goes
|
|
974
|
+
// to FTS unchanged. Filters compose with the SearchOptions arg —
|
|
975
|
+
// both are applied (intersection-style).
|
|
976
|
+
const parsed = (0, query_parser_1.parseQuery)(query);
|
|
977
|
+
const mergedKinds = parsed.kinds.length > 0
|
|
978
|
+
? Array.from(new Set([...(options.kinds ?? []), ...parsed.kinds]))
|
|
979
|
+
: options.kinds;
|
|
980
|
+
const mergedLanguages = parsed.languages.length > 0
|
|
981
|
+
? Array.from(new Set([...(options.languages ?? []), ...parsed.languages]))
|
|
982
|
+
: options.languages;
|
|
983
|
+
const pathFilters = parsed.pathFilters;
|
|
984
|
+
const nameFilters = parsed.nameFilters;
|
|
985
|
+
// The text portion drives FTS/LIKE; if all the user typed was
|
|
986
|
+
// filters (`kind:function`), we still need *some* candidate set,
|
|
987
|
+
// so synthesise an empty-text path that returns everything matching
|
|
988
|
+
// the filters.
|
|
989
|
+
const text = parsed.text;
|
|
990
|
+
const kinds = mergedKinds;
|
|
991
|
+
const languages = mergedLanguages;
|
|
992
|
+
// First try FTS5 with prefix matching
|
|
993
|
+
let results = text
|
|
994
|
+
? this.searchNodesFTS(text, { kinds, languages, limit, offset })
|
|
995
|
+
// Over-fetch by 5× when running filter-only (no text). The
|
|
996
|
+
// post-scoring path: + name: filters can be very selective, so
|
|
997
|
+
// a smaller multiplier risks returning fewer than `limit`
|
|
998
|
+
// results despite the DB having plenty of matches.
|
|
999
|
+
: this.searchAllByFilters({ kinds, languages, limit: limit * 5 });
|
|
1000
|
+
// If no FTS results, try LIKE-based substring search
|
|
1001
|
+
if (results.length === 0 && text.length >= 2) {
|
|
1002
|
+
results = this.searchNodesLike(text, { kinds, languages, limit, offset });
|
|
1003
|
+
}
|
|
1004
|
+
// Final fuzzy fallback: scan all known names and keep those within
|
|
1005
|
+
// a tight Levenshtein distance. Only fires when both FTS and LIKE
|
|
1006
|
+
// returned nothing AND there's a text portion long enough to be
|
|
1007
|
+
// worth fuzzing (1-char queries would match too much).
|
|
1008
|
+
if (results.length === 0 && text.length >= 3) {
|
|
1009
|
+
results = this.searchNodesFuzzy(text, { kinds, languages, limit });
|
|
1010
|
+
}
|
|
1011
|
+
// Supplement: ensure exact name matches are always candidates.
|
|
1012
|
+
// BM25 can bury short exact-match names (e.g. "getBean") under hundreds of
|
|
1013
|
+
// compound names (e.g. "getBeanDescriptor") in large codebases,
|
|
1014
|
+
// pushing them past the FTS fetch limit before post-hoc scoring can help.
|
|
1015
|
+
// Use the max BM25 score as the base so the nameMatchBonus (exact=30 vs
|
|
1016
|
+
// prefix=20) actually differentiates them after rescoring.
|
|
1017
|
+
if (results.length > 0 && query) {
|
|
1018
|
+
const existingIds = new Set(results.map(r => r.node.id));
|
|
1019
|
+
const maxFtsScore = Math.max(...results.map(r => r.score));
|
|
1020
|
+
const terms = query.split(/\s+/).filter(t => t.length >= 2);
|
|
1021
|
+
for (const term of terms) {
|
|
1022
|
+
let sql = 'SELECT * FROM nodes WHERE name = ? COLLATE NOCASE';
|
|
1023
|
+
const params = [term];
|
|
1024
|
+
if (kinds && kinds.length > 0) {
|
|
1025
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1026
|
+
params.push(...kinds);
|
|
1027
|
+
}
|
|
1028
|
+
if (languages && languages.length > 0) {
|
|
1029
|
+
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
1030
|
+
params.push(...languages);
|
|
1031
|
+
}
|
|
1032
|
+
sql += ' LIMIT 20';
|
|
1033
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1034
|
+
for (const row of rows) {
|
|
1035
|
+
if (!existingIds.has(row.id)) {
|
|
1036
|
+
results.push({ node: rowToNode(row), score: maxFtsScore });
|
|
1037
|
+
existingIds.add(row.id);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Apply multi-signal scoring
|
|
1043
|
+
if (results.length > 0 && (text || query)) {
|
|
1044
|
+
const scoringQuery = text || query;
|
|
1045
|
+
results = results.map(r => ({
|
|
1046
|
+
...r,
|
|
1047
|
+
score: r.score
|
|
1048
|
+
+ (0, query_utils_1.kindBonus)(r.node.kind)
|
|
1049
|
+
+ (0, query_utils_1.scorePathRelevance)(r.node.filePath, scoringQuery, this.projectNameTokens)
|
|
1050
|
+
+ (0, query_utils_1.nameMatchBonus)(r.node.name, scoringQuery),
|
|
1051
|
+
}));
|
|
1052
|
+
results.sort((a, b) => b.score - a.score);
|
|
1053
|
+
// Trim to requested limit after rescoring
|
|
1054
|
+
if (results.length > limit) {
|
|
1055
|
+
results = results.slice(0, limit);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
// Apply path: + name: filters AFTER scoring. Scoring already uses
|
|
1059
|
+
// path/name as a soft signal; the explicit filters here are a hard
|
|
1060
|
+
// gate. Done last so the FTS limit fetched plenty of candidates to
|
|
1061
|
+
// narrow from.
|
|
1062
|
+
if (pathFilters.length > 0) {
|
|
1063
|
+
const lowered = pathFilters.map((p) => p.toLowerCase());
|
|
1064
|
+
results = results.filter((r) => {
|
|
1065
|
+
const fp = r.node.filePath.toLowerCase();
|
|
1066
|
+
return lowered.some((p) => fp.includes(p));
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
if (nameFilters.length > 0) {
|
|
1070
|
+
const lowered = nameFilters.map((n) => n.toLowerCase());
|
|
1071
|
+
results = results.filter((r) => {
|
|
1072
|
+
const nm = r.node.name.toLowerCase();
|
|
1073
|
+
return lowered.some((n) => nm.includes(n));
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return results;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Match-everything path used when the user supplied only field
|
|
1080
|
+
* filters (`kind:function lang:typescript`) with no text. Returns
|
|
1081
|
+
* candidates ordered by name; the caller's filter pass narrows to
|
|
1082
|
+
* what was asked for.
|
|
1083
|
+
*/
|
|
1084
|
+
searchAllByFilters(options) {
|
|
1085
|
+
const { kinds, languages, limit } = options;
|
|
1086
|
+
let sql = 'SELECT * FROM nodes WHERE 1=1';
|
|
1087
|
+
const params = [];
|
|
1088
|
+
if (kinds && kinds.length > 0) {
|
|
1089
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1090
|
+
params.push(...kinds);
|
|
1091
|
+
}
|
|
1092
|
+
if (languages && languages.length > 0) {
|
|
1093
|
+
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
1094
|
+
params.push(...languages);
|
|
1095
|
+
}
|
|
1096
|
+
sql += ' ORDER BY name LIMIT ?';
|
|
1097
|
+
params.push(limit);
|
|
1098
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1099
|
+
return rows.map((row) => ({ node: rowToNode(row), score: 1 }));
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Fuzzy fallback: when zero FTS/LIKE hits, try an edit-distance
|
|
1103
|
+
* sweep over the distinct symbol-name set. Caps `maxDist` at 2 so
|
|
1104
|
+
* `getUssr` finds `getUser` but `process` doesn't match `prosody`.
|
|
1105
|
+
* Bounded edit distance keeps each comparison cheap; the per-query
|
|
1106
|
+
* scan is O(distinct-name-count) which is far smaller than total
|
|
1107
|
+
* node count on any real codebase.
|
|
1108
|
+
*/
|
|
1109
|
+
searchNodesFuzzy(text, options) {
|
|
1110
|
+
const { kinds, languages, limit } = options;
|
|
1111
|
+
const lowered = text.toLowerCase();
|
|
1112
|
+
const maxDist = lowered.length <= 4 ? 1 : 2;
|
|
1113
|
+
// Pull the distinct name list once. The set is cached on QueryBuilder
|
|
1114
|
+
// by getAllNodeNames(); even on a 200k-node project the distinct
|
|
1115
|
+
// name set is typically O(10k) because most names repeat. The
|
|
1116
|
+
// candidate-cap below bounds memory regardless.
|
|
1117
|
+
const allNames = this.getAllNodeNames();
|
|
1118
|
+
const candidates = [];
|
|
1119
|
+
for (const name of allNames) {
|
|
1120
|
+
const dist = (0, query_parser_1.boundedEditDistance)(name.toLowerCase(), lowered, maxDist);
|
|
1121
|
+
if (dist <= maxDist)
|
|
1122
|
+
candidates.push({ name, dist });
|
|
1123
|
+
}
|
|
1124
|
+
candidates.sort((a, b) => a.dist - b.dist);
|
|
1125
|
+
// Cap the per-name follow-up queries. Each survivor triggers a
|
|
1126
|
+
// separate `SELECT * FROM nodes WHERE name = ?`; without this cap
|
|
1127
|
+
// a project with many similar names (`getUser1`, `getUser2`...)
|
|
1128
|
+
// could fan out far beyond `limit` queries before the inner-loop
|
|
1129
|
+
// limit kicks in.
|
|
1130
|
+
const FUZZY_FOLLOWUP_CAP = Math.max(limit * 2, 50);
|
|
1131
|
+
const cappedCandidates = candidates.slice(0, FUZZY_FOLLOWUP_CAP);
|
|
1132
|
+
const results = [];
|
|
1133
|
+
const seen = new Set();
|
|
1134
|
+
for (const c of cappedCandidates) {
|
|
1135
|
+
if (results.length >= limit)
|
|
1136
|
+
break;
|
|
1137
|
+
let sql = 'SELECT * FROM nodes WHERE name = ?';
|
|
1138
|
+
const params = [c.name];
|
|
1139
|
+
if (kinds && kinds.length > 0) {
|
|
1140
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1141
|
+
params.push(...kinds);
|
|
1142
|
+
}
|
|
1143
|
+
if (languages && languages.length > 0) {
|
|
1144
|
+
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
1145
|
+
params.push(...languages);
|
|
1146
|
+
}
|
|
1147
|
+
sql += ' LIMIT 5';
|
|
1148
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1149
|
+
for (const row of rows) {
|
|
1150
|
+
if (seen.has(row.id))
|
|
1151
|
+
continue;
|
|
1152
|
+
seen.add(row.id);
|
|
1153
|
+
// Lower the score for each edit step away from the query so
|
|
1154
|
+
// exact-match fallbacks (dist 0) outrank dist-2 typos.
|
|
1155
|
+
results.push({ node: rowToNode(row), score: 1 / (1 + c.dist) });
|
|
1156
|
+
if (results.length >= limit)
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return results;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* FTS5 search with prefix matching
|
|
1164
|
+
*/
|
|
1165
|
+
searchNodesFTS(query, options) {
|
|
1166
|
+
const { kinds, languages, limit = 100, offset = 0 } = options;
|
|
1167
|
+
// Add prefix wildcard for better matching (e.g., "auth" matches "AuthService", "authenticate")
|
|
1168
|
+
// Escape special FTS5 characters and add prefix wildcard.
|
|
1169
|
+
//
|
|
1170
|
+
// `::` is a qualifier separator in Rust/C++/Ruby, not a token char,
|
|
1171
|
+
// so treat it as whitespace before the strip step. Otherwise queries
|
|
1172
|
+
// like `stage_apply::run` collapse to `stage_applyrun` (the colons
|
|
1173
|
+
// are stripped without splitting) and find nothing. See #173.
|
|
1174
|
+
const ftsQuery = query
|
|
1175
|
+
.replace(/::/g, ' ') // Rust/C++/Ruby qualifier separator
|
|
1176
|
+
.replace(/['"*():^]/g, '') // Remove FTS5 special chars
|
|
1177
|
+
.split(/\s+/)
|
|
1178
|
+
.filter(term => term.length > 0)
|
|
1179
|
+
// Strip FTS5 boolean operators to prevent query manipulation
|
|
1180
|
+
.filter(term => !/^(AND|OR|NOT|NEAR)$/i.test(term))
|
|
1181
|
+
.map(term => `"${term}"*`) // Prefix match each term
|
|
1182
|
+
.join(' OR ');
|
|
1183
|
+
if (!ftsQuery) {
|
|
1184
|
+
return [];
|
|
1185
|
+
}
|
|
1186
|
+
// BM25 column weights: id=0, name=20, qualified_name=5, docstring=1, signature=2
|
|
1187
|
+
// Heavy name weight ensures exact/prefix name matches rank above incidental
|
|
1188
|
+
// mentions in long docstrings or qualified names of nested symbols.
|
|
1189
|
+
// Fetch 5x requested limit so post-hoc rescoring (kindBonus, pathRelevance,
|
|
1190
|
+
// nameMatchBonus) can promote results that BM25 alone undervalues.
|
|
1191
|
+
const ftsLimit = Math.max(limit * 5, 100);
|
|
1192
|
+
let sql = `
|
|
1193
|
+
SELECT nodes.*, bm25(nodes_fts, 0, 20, 5, 1, 2) as score
|
|
1194
|
+
FROM nodes_fts
|
|
1195
|
+
JOIN nodes ON nodes_fts.id = nodes.id
|
|
1196
|
+
WHERE nodes_fts MATCH ?
|
|
1197
|
+
`;
|
|
1198
|
+
const params = [ftsQuery];
|
|
1199
|
+
if (kinds && kinds.length > 0) {
|
|
1200
|
+
sql += ` AND nodes.kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1201
|
+
params.push(...kinds);
|
|
1202
|
+
}
|
|
1203
|
+
if (languages && languages.length > 0) {
|
|
1204
|
+
sql += ` AND nodes.language IN (${languages.map(() => '?').join(',')})`;
|
|
1205
|
+
params.push(...languages);
|
|
1206
|
+
}
|
|
1207
|
+
sql += ' ORDER BY score LIMIT ? OFFSET ?';
|
|
1208
|
+
params.push(ftsLimit, offset);
|
|
1209
|
+
try {
|
|
1210
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1211
|
+
return rows.map((row) => ({
|
|
1212
|
+
node: rowToNode(row),
|
|
1213
|
+
score: Math.abs(row.score), // bm25 returns negative scores
|
|
1214
|
+
}));
|
|
1215
|
+
}
|
|
1216
|
+
catch {
|
|
1217
|
+
// FTS query failed, return empty
|
|
1218
|
+
return [];
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* LIKE-based substring search for cases where FTS doesn't match
|
|
1223
|
+
* Useful for camelCase matching (e.g., "signIn" finds "signInWithGoogle")
|
|
1224
|
+
*/
|
|
1225
|
+
searchNodesLike(query, options) {
|
|
1226
|
+
const { kinds, languages, limit = 100, offset = 0 } = options;
|
|
1227
|
+
let sql = `
|
|
1228
|
+
SELECT nodes.*,
|
|
1229
|
+
CASE
|
|
1230
|
+
WHEN name = ? THEN 1.0
|
|
1231
|
+
WHEN name LIKE ? THEN 0.9
|
|
1232
|
+
WHEN name LIKE ? THEN 0.8
|
|
1233
|
+
WHEN qualified_name LIKE ? THEN 0.7
|
|
1234
|
+
ELSE 0.5
|
|
1235
|
+
END as score
|
|
1236
|
+
FROM nodes
|
|
1237
|
+
WHERE (
|
|
1238
|
+
name LIKE ? OR
|
|
1239
|
+
qualified_name LIKE ? OR
|
|
1240
|
+
name LIKE ?
|
|
1241
|
+
)
|
|
1242
|
+
`;
|
|
1243
|
+
// Pattern variants for better matching
|
|
1244
|
+
const exactMatch = query;
|
|
1245
|
+
const startsWith = `${query}%`;
|
|
1246
|
+
const contains = `%${query}%`;
|
|
1247
|
+
const params = [
|
|
1248
|
+
exactMatch, // Exact match score
|
|
1249
|
+
startsWith, // Starts with score
|
|
1250
|
+
contains, // Contains score
|
|
1251
|
+
contains, // Qualified name score
|
|
1252
|
+
contains, // WHERE: name contains
|
|
1253
|
+
contains, // WHERE: qualified_name contains
|
|
1254
|
+
startsWith, // WHERE: name starts with
|
|
1255
|
+
];
|
|
1256
|
+
if (kinds && kinds.length > 0) {
|
|
1257
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1258
|
+
params.push(...kinds);
|
|
1259
|
+
}
|
|
1260
|
+
if (languages && languages.length > 0) {
|
|
1261
|
+
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
1262
|
+
params.push(...languages);
|
|
1263
|
+
}
|
|
1264
|
+
sql += ' ORDER BY score DESC, length(name) ASC LIMIT ? OFFSET ?';
|
|
1265
|
+
params.push(limit, offset);
|
|
1266
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1267
|
+
return rows.map((row) => ({
|
|
1268
|
+
node: rowToNode(row),
|
|
1269
|
+
score: row.score,
|
|
1270
|
+
}));
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Find nodes by exact name match
|
|
1274
|
+
*
|
|
1275
|
+
* Used for hybrid search - looks up symbols by exact name or case-insensitive match.
|
|
1276
|
+
* Returns high-confidence matches for known symbol names extracted from query.
|
|
1277
|
+
*
|
|
1278
|
+
* @param names - Array of symbol names to look up
|
|
1279
|
+
* @param options - Search options (kinds, languages, limit)
|
|
1280
|
+
* @returns SearchResult array with exact matches scored at 1.0
|
|
1281
|
+
*/
|
|
1282
|
+
findNodesByExactName(names, options = {}) {
|
|
1283
|
+
if (names.length === 0)
|
|
1284
|
+
return [];
|
|
1285
|
+
const { kinds, languages, limit = 50 } = options;
|
|
1286
|
+
// Two-pass approach to handle common names (e.g., "run" has 40+ matches):
|
|
1287
|
+
// Pass 1: Find which files contain distinctive (rare) symbols from the query.
|
|
1288
|
+
// Pass 2: Query each name, boosting results that co-locate with distinctive symbols.
|
|
1289
|
+
// Pass 1: Find files containing each queried name, identify distinctive names
|
|
1290
|
+
const nameToFiles = new Map();
|
|
1291
|
+
for (const name of names) {
|
|
1292
|
+
let sql = 'SELECT DISTINCT file_path FROM nodes WHERE name COLLATE NOCASE = ?';
|
|
1293
|
+
const params = [name];
|
|
1294
|
+
if (kinds && kinds.length > 0) {
|
|
1295
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1296
|
+
params.push(...kinds);
|
|
1297
|
+
}
|
|
1298
|
+
sql += ' LIMIT 100';
|
|
1299
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1300
|
+
nameToFiles.set(name.toLowerCase(), new Set(rows.map(r => r.file_path)));
|
|
1301
|
+
}
|
|
1302
|
+
// Distinctive names are those with fewer than 10 file matches (e.g., "scrapeLoop" = 1 file)
|
|
1303
|
+
const distinctiveFiles = new Set();
|
|
1304
|
+
for (const [, files] of nameToFiles) {
|
|
1305
|
+
if (files.size > 0 && files.size < 10) {
|
|
1306
|
+
for (const f of files)
|
|
1307
|
+
distinctiveFiles.add(f);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
// Pass 2: Query each name with per-name limit, scoring by co-location
|
|
1311
|
+
const perNameLimit = Math.max(8, Math.ceil(limit / names.length));
|
|
1312
|
+
const allResults = [];
|
|
1313
|
+
const seenIds = new Set();
|
|
1314
|
+
for (const name of names) {
|
|
1315
|
+
let sql = `
|
|
1316
|
+
SELECT nodes.*, 1.0 as score
|
|
1317
|
+
FROM nodes
|
|
1318
|
+
WHERE name COLLATE NOCASE = ?
|
|
1319
|
+
`;
|
|
1320
|
+
const params = [name];
|
|
1321
|
+
if (kinds && kinds.length > 0) {
|
|
1322
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1323
|
+
params.push(...kinds);
|
|
1324
|
+
}
|
|
1325
|
+
if (languages && languages.length > 0) {
|
|
1326
|
+
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
1327
|
+
params.push(...languages);
|
|
1328
|
+
}
|
|
1329
|
+
// Fetch enough to find co-located results among common names
|
|
1330
|
+
sql += ' LIMIT ?';
|
|
1331
|
+
params.push(Math.max(perNameLimit * 3, 50));
|
|
1332
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1333
|
+
const nameResults = [];
|
|
1334
|
+
for (const row of rows) {
|
|
1335
|
+
const node = rowToNode(row);
|
|
1336
|
+
if (seenIds.has(node.id))
|
|
1337
|
+
continue;
|
|
1338
|
+
// Boost results in files that also contain distinctive symbols
|
|
1339
|
+
const coLocationBoost = distinctiveFiles.has(node.filePath) ? 20 : 0;
|
|
1340
|
+
nameResults.push({ node, score: row.score + coLocationBoost });
|
|
1341
|
+
}
|
|
1342
|
+
// Sort by score (co-located first), take per-name limit
|
|
1343
|
+
nameResults.sort((a, b) => b.score - a.score);
|
|
1344
|
+
for (const r of nameResults.slice(0, perNameLimit)) {
|
|
1345
|
+
seenIds.add(r.node.id);
|
|
1346
|
+
allResults.push(r);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
// Sort all results by score so co-located results bubble up
|
|
1350
|
+
allResults.sort((a, b) => b.score - a.score);
|
|
1351
|
+
return allResults.slice(0, limit);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Find nodes whose name contains a substring (LIKE-based).
|
|
1355
|
+
* Useful for CamelCase-part matching where FTS fails because
|
|
1356
|
+
* e.g. "TransportSearchAction" is one FTS token, not matchable by "Search"*.
|
|
1357
|
+
*
|
|
1358
|
+
* Results are ordered by name length (shorter = more likely to be the core type).
|
|
1359
|
+
*/
|
|
1360
|
+
findNodesByNameSubstring(substring, options = {}) {
|
|
1361
|
+
const { kinds, languages, limit = 30, excludePrefix } = options;
|
|
1362
|
+
let sql = `
|
|
1363
|
+
SELECT nodes.*, 1.0 as score
|
|
1364
|
+
FROM nodes
|
|
1365
|
+
WHERE name LIKE ?
|
|
1366
|
+
`;
|
|
1367
|
+
const params = [`%${substring}%`];
|
|
1368
|
+
// Exclude prefix matches (handled by FTS-based prefix search in Step 2b)
|
|
1369
|
+
if (excludePrefix) {
|
|
1370
|
+
sql += ` AND name NOT LIKE ?`;
|
|
1371
|
+
params.push(`${substring}%`);
|
|
1372
|
+
}
|
|
1373
|
+
if (kinds && kinds.length > 0) {
|
|
1374
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1375
|
+
params.push(...kinds);
|
|
1376
|
+
}
|
|
1377
|
+
if (languages && languages.length > 0) {
|
|
1378
|
+
sql += ` AND language IN (${languages.map(() => '?').join(',')})`;
|
|
1379
|
+
params.push(...languages);
|
|
1380
|
+
}
|
|
1381
|
+
sql += ' ORDER BY length(name) ASC LIMIT ?';
|
|
1382
|
+
params.push(limit);
|
|
1383
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1384
|
+
return rows.map((row) => ({
|
|
1385
|
+
node: rowToNode(row),
|
|
1386
|
+
score: row.score,
|
|
1387
|
+
}));
|
|
1388
|
+
}
|
|
1389
|
+
// ===========================================================================
|
|
1390
|
+
// Edge Operations
|
|
1391
|
+
// ===========================================================================
|
|
1392
|
+
/**
|
|
1393
|
+
* Insert a new edge
|
|
1394
|
+
*/
|
|
1395
|
+
insertEdge(edge) {
|
|
1396
|
+
if (!this.stmts.insertEdge) {
|
|
1397
|
+
this.stmts.insertEdge = this.db.prepare(`
|
|
1398
|
+
INSERT OR IGNORE INTO edges (source, target, kind, metadata, line, col, provenance)
|
|
1399
|
+
VALUES (@source, @target, @kind, @metadata, @line, @col, @provenance)
|
|
1400
|
+
`);
|
|
1401
|
+
}
|
|
1402
|
+
this.stmts.insertEdge.run({
|
|
1403
|
+
source: edge.source,
|
|
1404
|
+
target: edge.target,
|
|
1405
|
+
kind: edge.kind,
|
|
1406
|
+
metadata: edge.metadata ? JSON.stringify(edge.metadata) : null,
|
|
1407
|
+
line: edge.line ?? null,
|
|
1408
|
+
col: edge.column ?? null,
|
|
1409
|
+
provenance: edge.provenance ?? null,
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Insert multiple edges in a transaction
|
|
1414
|
+
*/
|
|
1415
|
+
insertEdges(edges) {
|
|
1416
|
+
if (edges.length === 0)
|
|
1417
|
+
return;
|
|
1418
|
+
this.db.transaction(() => {
|
|
1419
|
+
const endpointIds = new Set();
|
|
1420
|
+
for (const edge of edges) {
|
|
1421
|
+
endpointIds.add(edge.source);
|
|
1422
|
+
endpointIds.add(edge.target);
|
|
1423
|
+
}
|
|
1424
|
+
const existingNodeIds = this.getExistingNodeIds([...endpointIds]);
|
|
1425
|
+
const rows = [];
|
|
1426
|
+
for (const edge of edges) {
|
|
1427
|
+
if (!existingNodeIds.has(edge.source) || !existingNodeIds.has(edge.target)) {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
rows.push([
|
|
1431
|
+
edge.source,
|
|
1432
|
+
edge.target,
|
|
1433
|
+
edge.kind,
|
|
1434
|
+
edge.metadata ? JSON.stringify(edge.metadata) : null,
|
|
1435
|
+
edge.line ?? null,
|
|
1436
|
+
edge.column ?? null,
|
|
1437
|
+
edge.provenance ?? null,
|
|
1438
|
+
]);
|
|
1439
|
+
}
|
|
1440
|
+
this.runBatched('insertEdges', 'INSERT OR IGNORE INTO edges (source, target, kind, metadata, line, col, provenance) VALUES ', '(?,?,?,?,?,?,?)', rows);
|
|
1441
|
+
})();
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Delete all edges from a source node
|
|
1445
|
+
*/
|
|
1446
|
+
deleteEdgesBySource(sourceId) {
|
|
1447
|
+
if (!this.stmts.deleteEdgesBySource) {
|
|
1448
|
+
this.stmts.deleteEdgesBySource = this.db.prepare('DELETE FROM edges WHERE source = ?');
|
|
1449
|
+
}
|
|
1450
|
+
this.stmts.deleteEdgesBySource.run(sourceId);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Get outgoing edges from a node
|
|
1454
|
+
*/
|
|
1455
|
+
getOutgoingEdges(sourceId, kinds, provenance) {
|
|
1456
|
+
if ((kinds && kinds.length > 0) || provenance) {
|
|
1457
|
+
let sql = 'SELECT * FROM edges WHERE source = ?';
|
|
1458
|
+
const params = [sourceId];
|
|
1459
|
+
if (kinds && kinds.length > 0) {
|
|
1460
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1461
|
+
params.push(...kinds);
|
|
1462
|
+
}
|
|
1463
|
+
if (provenance) {
|
|
1464
|
+
sql += ' AND provenance = ?';
|
|
1465
|
+
params.push(provenance);
|
|
1466
|
+
}
|
|
1467
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1468
|
+
return rows.map(rowToEdge);
|
|
1469
|
+
}
|
|
1470
|
+
if (!this.stmts.getEdgesBySource) {
|
|
1471
|
+
this.stmts.getEdgesBySource = this.db.prepare('SELECT * FROM edges WHERE source = ?');
|
|
1472
|
+
}
|
|
1473
|
+
const rows = this.stmts.getEdgesBySource.all(sourceId);
|
|
1474
|
+
return rows.map(rowToEdge);
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Get incoming edges to a node
|
|
1478
|
+
*/
|
|
1479
|
+
getIncomingEdges(targetId, kinds) {
|
|
1480
|
+
if (kinds && kinds.length > 0) {
|
|
1481
|
+
const sql = `SELECT * FROM edges WHERE target = ? AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1482
|
+
const rows = this.db.prepare(sql).all(targetId, ...kinds);
|
|
1483
|
+
return rows.map(rowToEdge);
|
|
1484
|
+
}
|
|
1485
|
+
if (!this.stmts.getEdgesByTarget) {
|
|
1486
|
+
this.stmts.getEdgesByTarget = this.db.prepare('SELECT * FROM edges WHERE target = ?');
|
|
1487
|
+
}
|
|
1488
|
+
const rows = this.stmts.getEdgesByTarget.all(targetId);
|
|
1489
|
+
return rows.map(rowToEdge);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Find all edges where both source and target are in the given node set.
|
|
1493
|
+
* Useful for recovering inter-node connectivity after BFS.
|
|
1494
|
+
*/
|
|
1495
|
+
findEdgesBetweenNodes(nodeIds, kinds) {
|
|
1496
|
+
if (nodeIds.length === 0)
|
|
1497
|
+
return [];
|
|
1498
|
+
const idsJson = JSON.stringify(nodeIds);
|
|
1499
|
+
let sql = `SELECT * FROM edges WHERE source IN (SELECT value FROM json_each(?)) AND target IN (SELECT value FROM json_each(?))`;
|
|
1500
|
+
const params = [idsJson, idsJson];
|
|
1501
|
+
if (kinds && kinds.length > 0) {
|
|
1502
|
+
sql += ` AND kind IN (${kinds.map(() => '?').join(',')})`;
|
|
1503
|
+
params.push(...kinds);
|
|
1504
|
+
}
|
|
1505
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
1506
|
+
return rows.map(rowToEdge);
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Distinct file paths that DEPEND ON `filePath`: every file containing a
|
|
1510
|
+
* symbol with a cross-file edge (any kind except `contains`) into a symbol
|
|
1511
|
+
* of this file. This is the file-level projection of the symbol dependency
|
|
1512
|
+
* graph and the basis for blast-radius / `affected` test selection.
|
|
1513
|
+
*
|
|
1514
|
+
* It deliberately does NOT restrict to `imports` edges. In this graph an
|
|
1515
|
+
* `imports` edge connects a file to its own local import declarations
|
|
1516
|
+
* (it is always same-file), so an imports-only lookup returns zero
|
|
1517
|
+
* cross-file dependents for every file. The real cross-file dependency
|
|
1518
|
+
* signal is the resolved call/reference graph — calls, references,
|
|
1519
|
+
* instantiates, extends, implements, overrides, type_of, returns,
|
|
1520
|
+
* decorates — exactly what {@link GraphTraverser.getImpactRadius} traverses.
|
|
1521
|
+
* `contains` is excluded: a parent containing a symbol does not *depend* on
|
|
1522
|
+
* it. One indexed query (idx_nodes_file_path + idx_edges_target_kind).
|
|
1523
|
+
*/
|
|
1524
|
+
getDependentFilePaths(filePath) {
|
|
1525
|
+
const sql = `SELECT DISTINCT src.file_path AS fp
|
|
1526
|
+
FROM edges e
|
|
1527
|
+
JOIN nodes tgt ON tgt.id = e.target
|
|
1528
|
+
JOIN nodes src ON src.id = e.source
|
|
1529
|
+
WHERE tgt.file_path = ?
|
|
1530
|
+
AND e.kind != 'contains'
|
|
1531
|
+
AND src.file_path != ?`;
|
|
1532
|
+
const rows = this.db.prepare(sql).all(filePath, filePath);
|
|
1533
|
+
return rows.map((r) => r.fp);
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Distinct file paths that `filePath` DEPENDS ON — the inverse of
|
|
1537
|
+
* {@link getDependentFilePaths}: every file containing a symbol that a
|
|
1538
|
+
* symbol of this file has a cross-file edge into. Same edge-kind rules
|
|
1539
|
+
* (all kinds except `contains`); same reason imports-only is insufficient.
|
|
1540
|
+
*/
|
|
1541
|
+
getDependencyFilePaths(filePath) {
|
|
1542
|
+
const sql = `SELECT DISTINCT tgt.file_path AS fp
|
|
1543
|
+
FROM edges e
|
|
1544
|
+
JOIN nodes src ON src.id = e.source
|
|
1545
|
+
JOIN nodes tgt ON tgt.id = e.target
|
|
1546
|
+
WHERE src.file_path = ?
|
|
1547
|
+
AND e.kind != 'contains'
|
|
1548
|
+
AND tgt.file_path != ?`;
|
|
1549
|
+
const rows = this.db.prepare(sql).all(filePath, filePath);
|
|
1550
|
+
return rows.map((r) => r.fp);
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Cross-file edges whose TARGET is a node in `filePath` and whose SOURCE is a
|
|
1554
|
+
* node in a *different* file, paired with the target node's (name, kind) so a
|
|
1555
|
+
* caller can re-resolve the edge to the re-indexed target's new ID (node IDs
|
|
1556
|
+
* are `sha256(filePath:kind:name:line)`, so any line shift in the callee file
|
|
1557
|
+
* changes target IDs and a naive re-insert by old ID silently drops them).
|
|
1558
|
+
* Used by `storeExtractionResult` to preserve incoming edges across a file
|
|
1559
|
+
* re-index (issue #899). Same edge-kind rules as
|
|
1560
|
+
* {@link getDependentFilePaths}: all kinds except `contains`.
|
|
1561
|
+
*/
|
|
1562
|
+
getCrossFileIncomingEdgesWithTarget(filePath) {
|
|
1563
|
+
const sql = `SELECT e.*, tgt.name AS target_name, tgt.kind AS target_kind,
|
|
1564
|
+
src.file_path AS source_file_path, src.language AS source_language
|
|
1565
|
+
FROM edges e
|
|
1566
|
+
JOIN nodes tgt ON tgt.id = e.target
|
|
1567
|
+
JOIN nodes src ON src.id = e.source
|
|
1568
|
+
WHERE tgt.file_path = ?
|
|
1569
|
+
AND e.kind != 'contains'
|
|
1570
|
+
AND src.file_path != ?`;
|
|
1571
|
+
const rows = this.db.prepare(sql).all(filePath, filePath);
|
|
1572
|
+
return rows.map(row => ({
|
|
1573
|
+
...rowToEdge(row),
|
|
1574
|
+
targetName: row.target_name,
|
|
1575
|
+
targetKind: row.target_kind,
|
|
1576
|
+
sourceFilePath: row.source_file_path,
|
|
1577
|
+
sourceLanguage: row.source_language,
|
|
1578
|
+
}));
|
|
1579
|
+
}
|
|
1580
|
+
// ===========================================================================
|
|
1581
|
+
// File Operations
|
|
1582
|
+
// ===========================================================================
|
|
1583
|
+
/**
|
|
1584
|
+
* Insert or update a file record
|
|
1585
|
+
*/
|
|
1586
|
+
upsertFile(file) {
|
|
1587
|
+
if (!this.stmts.upsertFile) {
|
|
1588
|
+
this.stmts.upsertFile = this.db.prepare(`
|
|
1589
|
+
INSERT INTO files (path, content_hash, language, size, modified_at, indexed_at, node_count, errors, generated)
|
|
1590
|
+
VALUES (@path, @contentHash, @language, @size, @modifiedAt, @indexedAt, @nodeCount, @errors, @generated)
|
|
1591
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
1592
|
+
content_hash = @contentHash,
|
|
1593
|
+
language = @language,
|
|
1594
|
+
size = @size,
|
|
1595
|
+
modified_at = @modifiedAt,
|
|
1596
|
+
indexed_at = @indexedAt,
|
|
1597
|
+
node_count = @nodeCount,
|
|
1598
|
+
errors = @errors,
|
|
1599
|
+
generated = @generated
|
|
1600
|
+
`);
|
|
1601
|
+
}
|
|
1602
|
+
this.stmts.upsertFile.run({
|
|
1603
|
+
path: file.path,
|
|
1604
|
+
contentHash: file.contentHash,
|
|
1605
|
+
language: file.language,
|
|
1606
|
+
size: file.size,
|
|
1607
|
+
modifiedAt: file.modifiedAt,
|
|
1608
|
+
indexedAt: file.indexedAt,
|
|
1609
|
+
nodeCount: file.nodeCount,
|
|
1610
|
+
errors: file.errors ? JSON.stringify(file.errors) : null,
|
|
1611
|
+
// The upsert always REWRITES the flag: a file that loses its banner in an
|
|
1612
|
+
// edit must lose the flag on the next sync, not keep a stale 1.
|
|
1613
|
+
generated: file.generated ? 1 : 0,
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Which of `filePaths` the index flagged as tool-generated (schema v9+).
|
|
1618
|
+
*
|
|
1619
|
+
* Bounded-lookup by design: every consumer already holds a short candidate
|
|
1620
|
+
* list (a ranked file group, an FTS result page, a LIMIT-20 aggregate), so
|
|
1621
|
+
* this stays a partial-index probe over a handful of paths — no whole-repo
|
|
1622
|
+
* set to materialize, and no cache to invalidate, which means a ranking call
|
|
1623
|
+
* can never serve a verdict the last sync already replaced.
|
|
1624
|
+
*
|
|
1625
|
+
* Returns ONLY the content/index signal; callers union it with
|
|
1626
|
+
* {@link isGeneratedFile} so pre-v9 databases (column present, all zeros
|
|
1627
|
+
* until a re-index) keep the path-only behavior rather than regressing.
|
|
1628
|
+
*/
|
|
1629
|
+
getGeneratedPathsAmong(filePaths) {
|
|
1630
|
+
const unique = [...new Set(filePaths)];
|
|
1631
|
+
const found = new Set();
|
|
1632
|
+
if (unique.length === 0)
|
|
1633
|
+
return found;
|
|
1634
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
1635
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
1636
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
1637
|
+
const rows = this.db
|
|
1638
|
+
.prepare(`SELECT path FROM files WHERE generated = 1 AND path IN (${placeholders})`)
|
|
1639
|
+
.all(...chunk);
|
|
1640
|
+
for (const row of rows)
|
|
1641
|
+
found.add(row.path);
|
|
1642
|
+
}
|
|
1643
|
+
return found;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* A reusable `(path) => boolean` over a bounded candidate list, unioning the
|
|
1647
|
+
* indexed flag with the path convention. This is the shape every ranking
|
|
1648
|
+
* comparator wants: one query up front, then O(1) per comparison.
|
|
1649
|
+
*/
|
|
1650
|
+
generatedPredicateFor(filePaths) {
|
|
1651
|
+
const flagged = this.getGeneratedPathsAmong(filePaths);
|
|
1652
|
+
return (filePath) => flagged.has(filePath) || (0, generated_detection_1.isGeneratedFile)(filePath);
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Which of `filePaths` are AMBIENT DECLARATION files — they declare nothing
|
|
1656
|
+
* but types, and nothing in the index depends on them (CG-28). A hand-written
|
|
1657
|
+
* ambient `.d.ts` of global shims, a vendored typings file, module
|
|
1658
|
+
* augmentation: reachable only by name, structurally attached to nothing.
|
|
1659
|
+
*
|
|
1660
|
+
* Structural, not extension-based, so a hand-written `types.ts` and a `.d.ts`
|
|
1661
|
+
* are judged by the same rule and a `.d.ts` that does declare a class or a
|
|
1662
|
+
* const is (correctly) not caught. Four conditions, all required:
|
|
1663
|
+
*
|
|
1664
|
+
* 1. it declares at least one symbol — an empty or unparsed file is not a
|
|
1665
|
+
* declaration file, it is a file we know nothing about;
|
|
1666
|
+
* 2. EVERY declared symbol is a type-level kind (interface / type alias /
|
|
1667
|
+
* enum / namespace). The narrowness is deliberate and measured: a rule
|
|
1668
|
+
* of "no callables" alone flags 1–18% of a repo, including Kotlin sealed
|
|
1669
|
+
* classes, Rust `mod.rs` re-exports and django's locale constant tables —
|
|
1670
|
+
* real source that must not be demoted. This rule flags 0–4%;
|
|
1671
|
+
* 3. no symbol in it originates a `calls`/`instantiates` edge — the direct
|
|
1672
|
+
* evidence that nothing here has a body;
|
|
1673
|
+
* 4. NOTHING ELSE IN THE INDEX points at it. This is the condition that
|
|
1674
|
+
* separates an ambient shim from a working type module, and it is why
|
|
1675
|
+
* the flag is narrow enough to be safe: `displacement-ts`'s pipeline
|
|
1676
|
+
* `types.ts` passes 1–3 identically but carries 13 inbound imports and
|
|
1677
|
+
* 21 references, so the files that answer a query about the pipeline are
|
|
1678
|
+
* typed BY it — it is part of that answer's structure. An ambient
|
|
1679
|
+
* `declare global` shim has zero. Deliberately index-wide rather than
|
|
1680
|
+
* restricted to the candidate list: the file that imports it is usually
|
|
1681
|
+
* not itself a candidate.
|
|
1682
|
+
*
|
|
1683
|
+
* Bounded-lookup like {@link getGeneratedPathsAmong}: callers hold a ranked
|
|
1684
|
+
* candidate list, so this is a partial-index probe over a handful of paths.
|
|
1685
|
+
*/
|
|
1686
|
+
getAmbientDeclarationPathsAmong(filePaths) {
|
|
1687
|
+
const unique = [...new Set(filePaths)];
|
|
1688
|
+
const found = new Set();
|
|
1689
|
+
if (unique.length === 0)
|
|
1690
|
+
return found;
|
|
1691
|
+
for (let i = 0; i < unique.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
1692
|
+
const chunk = unique.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
1693
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
1694
|
+
// `file`/`import`/`export`/`parameter` are structural bookkeeping, not
|
|
1695
|
+
// things the file declares, so they neither qualify nor disqualify.
|
|
1696
|
+
const rows = this.db
|
|
1697
|
+
.prepare(`
|
|
1698
|
+
SELECT file_path,
|
|
1699
|
+
SUM(CASE WHEN kind NOT IN ('file','import','export','parameter')
|
|
1700
|
+
THEN 1 ELSE 0 END) AS declared,
|
|
1701
|
+
SUM(CASE WHEN kind IN ('interface','type_alias','enum','enum_member','namespace')
|
|
1702
|
+
THEN 1 ELSE 0 END) AS typeDeclared
|
|
1703
|
+
FROM nodes
|
|
1704
|
+
WHERE file_path IN (${placeholders})
|
|
1705
|
+
GROUP BY file_path
|
|
1706
|
+
`)
|
|
1707
|
+
.all(...chunk);
|
|
1708
|
+
let candidates = rows
|
|
1709
|
+
.filter((r) => r.declared > 0 && r.declared === r.typeDeclared)
|
|
1710
|
+
.map((r) => r.file_path);
|
|
1711
|
+
if (candidates.length === 0)
|
|
1712
|
+
continue;
|
|
1713
|
+
const disqualify = (sql) => {
|
|
1714
|
+
if (candidates.length === 0)
|
|
1715
|
+
return;
|
|
1716
|
+
const hit = new Set(this.db
|
|
1717
|
+
.prepare(sql.replace('$IN$', candidates.map(() => '?').join(',')))
|
|
1718
|
+
.all(...candidates).map((r) => r.file_path));
|
|
1719
|
+
candidates = candidates.filter((p) => !hit.has(p));
|
|
1720
|
+
};
|
|
1721
|
+
// (3) originates behaviour
|
|
1722
|
+
disqualify(`
|
|
1723
|
+
SELECT DISTINCT n.file_path AS file_path
|
|
1724
|
+
FROM edges e JOIN nodes n ON n.id = e.source
|
|
1725
|
+
WHERE e.kind IN ('calls','instantiates') AND n.file_path IN ($IN$)
|
|
1726
|
+
`);
|
|
1727
|
+
// (4) something outside the file depends on it
|
|
1728
|
+
disqualify(`
|
|
1729
|
+
SELECT DISTINCT t.file_path AS file_path
|
|
1730
|
+
FROM edges e JOIN nodes t ON t.id = e.target JOIN nodes s ON s.id = e.source
|
|
1731
|
+
WHERE t.file_path IN ($IN$) AND s.file_path <> t.file_path
|
|
1732
|
+
`);
|
|
1733
|
+
for (const path of candidates)
|
|
1734
|
+
found.add(path);
|
|
1735
|
+
}
|
|
1736
|
+
return found;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* A reusable `(path) => boolean` ambient-declaration test over a bounded
|
|
1740
|
+
* candidate list — the shape a ranking comparator wants: one query up front,
|
|
1741
|
+
* O(1) per comparison.
|
|
1742
|
+
*/
|
|
1743
|
+
ambientDeclarationPredicateFor(filePaths) {
|
|
1744
|
+
const flagged = this.getAmbientDeclarationPathsAmong(filePaths);
|
|
1745
|
+
return (filePath) => flagged.has(filePath);
|
|
1746
|
+
}
|
|
1747
|
+
/** How many indexed files carry the generated flag. Surfaced by `status`. */
|
|
1748
|
+
countGeneratedFiles() {
|
|
1749
|
+
const row = this.db
|
|
1750
|
+
.prepare('SELECT COUNT(*) AS n FROM files WHERE generated = 1')
|
|
1751
|
+
.get();
|
|
1752
|
+
return row?.n ?? 0;
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Delete a file record and its nodes
|
|
1756
|
+
*/
|
|
1757
|
+
deleteFile(filePath) {
|
|
1758
|
+
this.db.transaction(() => {
|
|
1759
|
+
this.deleteNodesByFile(filePath);
|
|
1760
|
+
if (!this.stmts.deleteFile) {
|
|
1761
|
+
this.stmts.deleteFile = this.db.prepare('DELETE FROM files WHERE path = ?');
|
|
1762
|
+
}
|
|
1763
|
+
this.stmts.deleteFile.run(filePath);
|
|
1764
|
+
})();
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Get a file record by path
|
|
1768
|
+
*/
|
|
1769
|
+
getFileByPath(filePath) {
|
|
1770
|
+
if (!this.stmts.getFileByPath) {
|
|
1771
|
+
this.stmts.getFileByPath = this.db.prepare('SELECT * FROM files WHERE path = ?');
|
|
1772
|
+
}
|
|
1773
|
+
const row = this.stmts.getFileByPath.get(filePath);
|
|
1774
|
+
return row ? rowToFileRecord(row) : null;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Get all tracked files
|
|
1778
|
+
*/
|
|
1779
|
+
getAllFiles() {
|
|
1780
|
+
if (!this.stmts.getAllFiles) {
|
|
1781
|
+
this.stmts.getAllFiles = this.db.prepare('SELECT * FROM files ORDER BY path');
|
|
1782
|
+
}
|
|
1783
|
+
const rows = this.stmts.getAllFiles.all();
|
|
1784
|
+
return rows.map(rowToFileRecord);
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Most recent index timestamp (ms since epoch) across all tracked files, or
|
|
1788
|
+
* null when nothing is indexed yet. One indexed aggregate, no per-row scan. (#329)
|
|
1789
|
+
*/
|
|
1790
|
+
getLastIndexedAt() {
|
|
1791
|
+
const row = this.db
|
|
1792
|
+
.prepare('SELECT MAX(indexed_at) AS last FROM files')
|
|
1793
|
+
.get();
|
|
1794
|
+
return row?.last ?? null;
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Get files that need re-indexing (hash changed)
|
|
1798
|
+
*/
|
|
1799
|
+
getStaleFiles(currentHashes) {
|
|
1800
|
+
const files = this.getAllFiles();
|
|
1801
|
+
return files.filter((f) => {
|
|
1802
|
+
const currentHash = currentHashes.get(f.path);
|
|
1803
|
+
return currentHash && currentHash !== f.contentHash;
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
// ===========================================================================
|
|
1807
|
+
// Unresolved References
|
|
1808
|
+
// ===========================================================================
|
|
1809
|
+
/**
|
|
1810
|
+
* Insert an unresolved reference
|
|
1811
|
+
*/
|
|
1812
|
+
insertUnresolvedRef(ref) {
|
|
1813
|
+
if (!this.stmts.insertUnresolved) {
|
|
1814
|
+
this.stmts.insertUnresolved = this.db.prepare(`
|
|
1815
|
+
INSERT INTO unresolved_refs (from_node_id, reference_name, reference_kind, line, col, candidates, file_path, language)
|
|
1816
|
+
VALUES (@fromNodeId, @referenceName, @referenceKind, @line, @col, @candidates, @filePath, @language)
|
|
1817
|
+
`);
|
|
1818
|
+
}
|
|
1819
|
+
this.stmts.insertUnresolved.run({
|
|
1820
|
+
fromNodeId: ref.fromNodeId,
|
|
1821
|
+
referenceName: ref.referenceName,
|
|
1822
|
+
referenceKind: ref.referenceKind,
|
|
1823
|
+
line: ref.line,
|
|
1824
|
+
col: ref.column,
|
|
1825
|
+
candidates: ref.candidates ? JSON.stringify(ref.candidates) : null,
|
|
1826
|
+
filePath: ref.filePath ?? '',
|
|
1827
|
+
language: ref.language ?? 'unknown',
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Insert multiple unresolved references in a transaction
|
|
1832
|
+
*/
|
|
1833
|
+
insertUnresolvedRefsBatch(refs) {
|
|
1834
|
+
if (refs.length === 0)
|
|
1835
|
+
return;
|
|
1836
|
+
const insert = this.db.transaction(() => {
|
|
1837
|
+
const rows = [];
|
|
1838
|
+
for (const ref of refs) {
|
|
1839
|
+
rows.push([
|
|
1840
|
+
ref.fromNodeId,
|
|
1841
|
+
ref.referenceName,
|
|
1842
|
+
ref.referenceKind,
|
|
1843
|
+
ref.line,
|
|
1844
|
+
ref.column,
|
|
1845
|
+
ref.candidates ? JSON.stringify(ref.candidates) : null,
|
|
1846
|
+
ref.filePath ?? '',
|
|
1847
|
+
ref.language ?? 'unknown',
|
|
1848
|
+
]);
|
|
1849
|
+
}
|
|
1850
|
+
this.runBatched('insertUnresolvedRefs', 'INSERT INTO unresolved_refs (from_node_id, reference_name, reference_kind, line, col, candidates, file_path, language) VALUES ', '(?,?,?,?,?,?,?,?)', rows);
|
|
1851
|
+
});
|
|
1852
|
+
insert();
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Delete unresolved references from a node
|
|
1856
|
+
*/
|
|
1857
|
+
deleteUnresolvedByNode(nodeId) {
|
|
1858
|
+
if (!this.stmts.deleteUnresolvedByNode) {
|
|
1859
|
+
this.stmts.deleteUnresolvedByNode = this.db.prepare('DELETE FROM unresolved_refs WHERE from_node_id = ?');
|
|
1860
|
+
}
|
|
1861
|
+
this.stmts.deleteUnresolvedByNode.run(nodeId);
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Get unresolved references by name (for resolution)
|
|
1865
|
+
*/
|
|
1866
|
+
getUnresolvedByName(name) {
|
|
1867
|
+
if (!this.stmts.getUnresolvedByName) {
|
|
1868
|
+
this.stmts.getUnresolvedByName = this.db.prepare('SELECT * FROM unresolved_refs WHERE reference_name = ?');
|
|
1869
|
+
}
|
|
1870
|
+
const rows = this.stmts.getUnresolvedByName.all(name);
|
|
1871
|
+
return rows.map((row) => ({
|
|
1872
|
+
fromNodeId: row.from_node_id,
|
|
1873
|
+
referenceName: row.reference_name,
|
|
1874
|
+
referenceKind: row.reference_kind,
|
|
1875
|
+
line: row.line,
|
|
1876
|
+
column: row.col,
|
|
1877
|
+
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
1878
|
+
filePath: row.file_path,
|
|
1879
|
+
language: row.language,
|
|
1880
|
+
rowId: row.id,
|
|
1881
|
+
}));
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Get all unresolved references
|
|
1885
|
+
*/
|
|
1886
|
+
getUnresolvedReferences() {
|
|
1887
|
+
const rows = this.db.prepare('SELECT * FROM unresolved_refs').all();
|
|
1888
|
+
return rows.map((row) => ({
|
|
1889
|
+
fromNodeId: row.from_node_id,
|
|
1890
|
+
referenceName: row.reference_name,
|
|
1891
|
+
referenceKind: row.reference_kind,
|
|
1892
|
+
line: row.line,
|
|
1893
|
+
column: row.col,
|
|
1894
|
+
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
1895
|
+
filePath: row.file_path,
|
|
1896
|
+
language: row.language,
|
|
1897
|
+
rowId: row.id,
|
|
1898
|
+
}));
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Get the count of PENDING (never-attempted) references without loading
|
|
1902
|
+
* them into memory. Rows marked status='failed' — attempted by a completed
|
|
1903
|
+
* pass, no match — are excluded: they are not outstanding work, only retry
|
|
1904
|
+
* candidates for the #1240 sweep, so they must not trip the #1187 orphan
|
|
1905
|
+
* sweep or the `status` pending-refs warning.
|
|
1906
|
+
*/
|
|
1907
|
+
getUnresolvedReferencesCount() {
|
|
1908
|
+
if (!this.stmts.getUnresolvedCount) {
|
|
1909
|
+
this.stmts.getUnresolvedCount = this.db.prepare("SELECT COUNT(*) as count FROM unresolved_refs WHERE status = 'pending'");
|
|
1910
|
+
}
|
|
1911
|
+
const row = this.stmts.getUnresolvedCount.get();
|
|
1912
|
+
return row.count;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Get a batch of PENDING unresolved references using LIMIT/OFFSET
|
|
1916
|
+
* pagination. Used to process references in bounded memory chunks; failed
|
|
1917
|
+
* rows are excluded so the batched drain loop terminates once every row
|
|
1918
|
+
* has been attempted.
|
|
1919
|
+
*/
|
|
1920
|
+
getUnresolvedReferencesBatch(offset, limit) {
|
|
1921
|
+
if (!this.stmts.getUnresolvedBatch) {
|
|
1922
|
+
// ORDER BY rowid is load-bearing for the pipelined resolution loop: it
|
|
1923
|
+
// prefetches batch k+1 at OFFSET batch_k.length while batch k's rows are
|
|
1924
|
+
// still pending, which is only exact under a stable enumeration. (A plain
|
|
1925
|
+
// scan and the status index both return rowid order anyway — this pins
|
|
1926
|
+
// it.)
|
|
1927
|
+
this.stmts.getUnresolvedBatch = this.db.prepare("SELECT * FROM unresolved_refs WHERE status = 'pending' ORDER BY rowid LIMIT ? OFFSET ?");
|
|
1928
|
+
}
|
|
1929
|
+
const rows = this.stmts.getUnresolvedBatch.all(limit, offset);
|
|
1930
|
+
return rows.map((row) => ({
|
|
1931
|
+
fromNodeId: row.from_node_id,
|
|
1932
|
+
referenceName: row.reference_name,
|
|
1933
|
+
referenceKind: row.reference_kind,
|
|
1934
|
+
line: row.line,
|
|
1935
|
+
column: row.col,
|
|
1936
|
+
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
1937
|
+
filePath: row.file_path,
|
|
1938
|
+
language: row.language,
|
|
1939
|
+
rowId: row.id,
|
|
1940
|
+
}));
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Keyset variant of {@link getUnresolvedReferencesBatch} for the batched
|
|
1944
|
+
* resolution loop: seek past the last-seen row id instead of OFFSET-walking.
|
|
1945
|
+
* OFFSET reads re-scan the accumulated failed-row prefix on every batch —
|
|
1946
|
+
* O(failed rows) per read, measured at 54.6s of the kernel-scale batch loop
|
|
1947
|
+
* (§7a.2) — while the seek is O(batch) forever. `id` is the rowid alias, so
|
|
1948
|
+
* the enumeration order is identical to the OFFSET reader's.
|
|
1949
|
+
*/
|
|
1950
|
+
getUnresolvedReferencesBatchAfter(afterRowId, limit) {
|
|
1951
|
+
if (!this.stmts.getUnresolvedBatchAfter) {
|
|
1952
|
+
this.stmts.getUnresolvedBatchAfter = this.db.prepare("SELECT * FROM unresolved_refs WHERE status = 'pending' AND id > ? ORDER BY id LIMIT ?");
|
|
1953
|
+
}
|
|
1954
|
+
const rows = this.stmts.getUnresolvedBatchAfter.all(afterRowId, limit);
|
|
1955
|
+
return rows.map((row) => ({
|
|
1956
|
+
fromNodeId: row.from_node_id,
|
|
1957
|
+
referenceName: row.reference_name,
|
|
1958
|
+
referenceKind: row.reference_kind,
|
|
1959
|
+
line: row.line,
|
|
1960
|
+
column: row.col,
|
|
1961
|
+
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
1962
|
+
filePath: row.file_path,
|
|
1963
|
+
language: row.language,
|
|
1964
|
+
rowId: row.id,
|
|
1965
|
+
}));
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Get all tracked file paths (lightweight — no full FileRecord objects)
|
|
1969
|
+
*/
|
|
1970
|
+
getAllFilePaths() {
|
|
1971
|
+
if (!this.stmts.getAllFilePaths) {
|
|
1972
|
+
this.stmts.getAllFilePaths = this.db.prepare('SELECT path FROM files ORDER BY path');
|
|
1973
|
+
}
|
|
1974
|
+
const rows = this.stmts.getAllFilePaths.all();
|
|
1975
|
+
return rows.map((r) => r.path);
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Get all distinct node names (lightweight — just name strings for pre-filtering)
|
|
1979
|
+
*/
|
|
1980
|
+
getAllNodeNames() {
|
|
1981
|
+
if (!this.stmts.getAllNodeNames) {
|
|
1982
|
+
this.stmts.getAllNodeNames = this.db.prepare('SELECT DISTINCT name FROM nodes');
|
|
1983
|
+
}
|
|
1984
|
+
const rows = this.stmts.getAllNodeNames.all();
|
|
1985
|
+
return rows.map((r) => r.name);
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Stream the distinct node names one row at a time — the incremental
|
|
1989
|
+
* counterpart to {@link getAllNodeNames} for callers that need to yield
|
|
1990
|
+
* to the event loop mid-scan (resolver cache warm-up on multi-million-node
|
|
1991
|
+
* indexes). Fresh statement per call: the iterator holds an open cursor.
|
|
1992
|
+
*/
|
|
1993
|
+
*iterateNodeNames() {
|
|
1994
|
+
const stmt = this.db.prepare('SELECT DISTINCT name FROM nodes');
|
|
1995
|
+
for (const row of stmt.iterate()) {
|
|
1996
|
+
yield row.name;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Get unresolved references scoped to specific file paths.
|
|
2001
|
+
* Uses the idx_unresolved_file_path index for efficient lookup.
|
|
2002
|
+
*/
|
|
2003
|
+
getUnresolvedReferencesByFiles(filePaths) {
|
|
2004
|
+
if (filePaths.length === 0)
|
|
2005
|
+
return [];
|
|
2006
|
+
// Chunk under SQLite's parameter limit: the first sync of a very large repo
|
|
2007
|
+
// passes every changed file here, which an unbounded `IN (...)` would bind
|
|
2008
|
+
// as one parameter each — exceeding MAX_VARIABLE_NUMBER and aborting with
|
|
2009
|
+
// "too many SQL variables". (#540)
|
|
2010
|
+
const rows = [];
|
|
2011
|
+
for (let i = 0; i < filePaths.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2012
|
+
const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2013
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2014
|
+
const chunkRows = this.db
|
|
2015
|
+
.prepare(`SELECT * FROM unresolved_refs WHERE status = 'pending' AND file_path IN (${placeholders})`)
|
|
2016
|
+
.all(...chunk);
|
|
2017
|
+
rows.push(...chunkRows);
|
|
2018
|
+
}
|
|
2019
|
+
return rows.map((row) => ({
|
|
2020
|
+
fromNodeId: row.from_node_id,
|
|
2021
|
+
referenceName: row.reference_name,
|
|
2022
|
+
referenceKind: row.reference_kind,
|
|
2023
|
+
line: row.line,
|
|
2024
|
+
column: row.col,
|
|
2025
|
+
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
2026
|
+
filePath: row.file_path,
|
|
2027
|
+
language: row.language,
|
|
2028
|
+
rowId: row.id,
|
|
2029
|
+
}));
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Delete all unresolved references (after resolution)
|
|
2033
|
+
*/
|
|
2034
|
+
clearUnresolvedReferences() {
|
|
2035
|
+
this.db.exec('DELETE FROM unresolved_refs');
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Delete resolved references by their IDs
|
|
2039
|
+
*/
|
|
2040
|
+
deleteResolvedReferences(fromNodeIds) {
|
|
2041
|
+
if (fromNodeIds.length === 0)
|
|
2042
|
+
return;
|
|
2043
|
+
// Chunk under SQLite's parameter limit, matching every other IN-list in
|
|
2044
|
+
// this file. The internal resolution path uses deleteSpecificResolvedReferences
|
|
2045
|
+
// instead, but QueryBuilder is part of the public API, so a library consumer
|
|
2046
|
+
// passing more ids than SQLITE_MAX_VARIABLE_NUMBER (32766 on the bundled
|
|
2047
|
+
// node:sqlite) would otherwise hit "too many SQL variables". (#540, #1001)
|
|
2048
|
+
for (let i = 0; i < fromNodeIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2049
|
+
const chunk = fromNodeIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2050
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2051
|
+
this.db.prepare(`DELETE FROM unresolved_refs WHERE from_node_id IN (${placeholders})`).run(...chunk);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Delete specific resolved references by (fromNodeId, referenceName, referenceKind) tuples.
|
|
2056
|
+
* More precise than deleteResolvedReferences — only removes refs that were actually resolved.
|
|
2057
|
+
*/
|
|
2058
|
+
deleteSpecificResolvedReferences(refs) {
|
|
2059
|
+
if (refs.length === 0)
|
|
2060
|
+
return 0;
|
|
2061
|
+
const stmt = this.db.prepare('DELETE FROM unresolved_refs WHERE from_node_id = ? AND reference_name = ? AND reference_kind = ?');
|
|
2062
|
+
// Returns rows actually removed (SQLite `changes`, summed): the batched
|
|
2063
|
+
// resolution loop's non-progress guard keys on this — zero removals from
|
|
2064
|
+
// a batch that claimed work is the direct runaway signal (§7a.2).
|
|
2065
|
+
let changed = 0;
|
|
2066
|
+
const deleteMany = this.db.transaction((items) => {
|
|
2067
|
+
for (const ref of items) {
|
|
2068
|
+
changed += stmt.run(ref.fromNodeId, ref.referenceName, ref.referenceKind).changes;
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
deleteMany(refs);
|
|
2072
|
+
return changed;
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Delete unresolved-ref rows by row id — the precise cleanup for refs a
|
|
2076
|
+
* resolution pass actually processed. The key-tuple variant above also
|
|
2077
|
+
* deletes SIBLING rows (same caller calling the same callee at other lines)
|
|
2078
|
+
* that a later batch hasn't attempted yet, so when a batch boundary split a
|
|
2079
|
+
* caller's same-named call sites, the later sites' edges were silently never
|
|
2080
|
+
* created (#1269).
|
|
2081
|
+
*/
|
|
2082
|
+
deleteReferencesByRowIds(rowIds) {
|
|
2083
|
+
if (rowIds.length === 0)
|
|
2084
|
+
return 0;
|
|
2085
|
+
// One transaction for all chunks (each chunk was previously its own
|
|
2086
|
+
// implicit transaction = its own WAL commit — measurable on 100k+-ref
|
|
2087
|
+
// resolution persists), and the full-size chunk statement is cached so
|
|
2088
|
+
// repeat calls skip the re-prepare; only the final partial chunk (if any)
|
|
2089
|
+
// prepares ad hoc. Returns rows actually removed (summed `changes`) for
|
|
2090
|
+
// the batched loop's non-progress guard (§7a.2).
|
|
2091
|
+
let changed = 0;
|
|
2092
|
+
this.db.transaction(() => {
|
|
2093
|
+
for (let i = 0; i < rowIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2094
|
+
const chunk = rowIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2095
|
+
if (chunk.length === SQLITE_PARAM_CHUNK_SIZE) {
|
|
2096
|
+
if (!this.stmts.deleteRefsByRowIdsFull) {
|
|
2097
|
+
const placeholders = new Array(SQLITE_PARAM_CHUNK_SIZE).fill('?').join(',');
|
|
2098
|
+
this.stmts.deleteRefsByRowIdsFull = this.db.prepare(`DELETE FROM unresolved_refs WHERE id IN (${placeholders})`);
|
|
2099
|
+
}
|
|
2100
|
+
changed += this.stmts.deleteRefsByRowIdsFull.run(...chunk).changes;
|
|
2101
|
+
}
|
|
2102
|
+
else {
|
|
2103
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2104
|
+
changed += this.db.prepare(`DELETE FROM unresolved_refs WHERE id IN (${placeholders})`).run(...chunk).changes;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
})();
|
|
2108
|
+
return changed;
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Mark refs a completed resolution pass could not resolve as status='failed'
|
|
2112
|
+
* instead of deleting them (#1240). Failed rows are invisible to the pending
|
|
2113
|
+
* count/batch readers (so drain loops and the #1187 orphan sweep still
|
|
2114
|
+
* terminate) but stay queryable by name_tail so a later sync can retry them
|
|
2115
|
+
* when a changed file introduces a symbol that could satisfy them. name_tail
|
|
2116
|
+
* is (re)written here so rows inserted before the v8 migration get their
|
|
2117
|
+
* tail the first time they're attempted.
|
|
2118
|
+
*/
|
|
2119
|
+
markReferencesFailed(refs) {
|
|
2120
|
+
if (refs.length === 0)
|
|
2121
|
+
return 0;
|
|
2122
|
+
const stmt = this.db.prepare("UPDATE unresolved_refs SET status = 'failed', name_tail = ? WHERE from_node_id = ? AND reference_name = ? AND reference_kind = ?");
|
|
2123
|
+
let changed = 0;
|
|
2124
|
+
const markMany = this.db.transaction((items) => {
|
|
2125
|
+
for (const ref of items) {
|
|
2126
|
+
changed += stmt.run(referenceNameTail(ref.referenceName), ref.fromNodeId, ref.referenceName, ref.referenceKind).changes;
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
markMany(refs);
|
|
2130
|
+
return changed;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Park refs as status='failed' by row id — the precise counterpart of
|
|
2134
|
+
* markReferencesFailed, for the same reason as deleteReferencesByRowIds:
|
|
2135
|
+
* the key-tuple variant also flips same-key sibling rows in later batches
|
|
2136
|
+
* to 'failed' before they were ever attempted (#1269). Resolution outcome
|
|
2137
|
+
* can differ per call site (receiver-type inference reads the ref's line),
|
|
2138
|
+
* so a sibling must not inherit this row's failure.
|
|
2139
|
+
*/
|
|
2140
|
+
markReferencesFailedByRowIds(refs) {
|
|
2141
|
+
if (refs.length === 0)
|
|
2142
|
+
return 0;
|
|
2143
|
+
const stmt = this.db.prepare("UPDATE unresolved_refs SET status = 'failed', name_tail = ? WHERE id = ?");
|
|
2144
|
+
let changed = 0;
|
|
2145
|
+
const markMany = this.db.transaction((items) => {
|
|
2146
|
+
for (const ref of items) {
|
|
2147
|
+
changed += stmt.run(referenceNameTail(ref.referenceName), ref.rowId).changes;
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
markMany(refs);
|
|
2151
|
+
return changed;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Failed refs whose name tail matches one of the given symbol names — the
|
|
2155
|
+
* candidates a sync should retry after files carrying those names changed
|
|
2156
|
+
* (#1240). Names matching more than `perNameCeiling` failed refs are
|
|
2157
|
+
* skipped entirely: at that population a name is external/builtin noise
|
|
2158
|
+
* (`get`, `map`, …) that one new definition won't resolve — the same
|
|
2159
|
+
* rationale as resolution's AMBIGUOUS_NAME_CEILING (#999) — and retrying an
|
|
2160
|
+
* arbitrary subset would be both wasted work and incoherent coverage.
|
|
2161
|
+
*/
|
|
2162
|
+
getRetryableFailedReferences(names, perNameCeiling = 500) {
|
|
2163
|
+
if (names.length === 0)
|
|
2164
|
+
return [];
|
|
2165
|
+
// Pass 1: per-tail counts, chunked under the SQLite parameter limit.
|
|
2166
|
+
const retryNames = [];
|
|
2167
|
+
for (let i = 0; i < names.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2168
|
+
const chunk = names.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2169
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2170
|
+
const counts = this.db
|
|
2171
|
+
.prepare(`SELECT name_tail, COUNT(*) as count FROM unresolved_refs WHERE status = 'failed' AND name_tail IN (${placeholders}) GROUP BY name_tail`)
|
|
2172
|
+
.all(...chunk);
|
|
2173
|
+
for (const row of counts) {
|
|
2174
|
+
if (row.count <= perNameCeiling)
|
|
2175
|
+
retryNames.push(row.name_tail);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
if (retryNames.length === 0)
|
|
2179
|
+
return [];
|
|
2180
|
+
// Pass 2: load the surviving rows.
|
|
2181
|
+
const rows = [];
|
|
2182
|
+
for (let i = 0; i < retryNames.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2183
|
+
const chunk = retryNames.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2184
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2185
|
+
const chunkRows = this.db
|
|
2186
|
+
.prepare(`SELECT * FROM unresolved_refs WHERE status = 'failed' AND name_tail IN (${placeholders})`)
|
|
2187
|
+
.all(...chunk);
|
|
2188
|
+
rows.push(...chunkRows);
|
|
2189
|
+
}
|
|
2190
|
+
return rows.map((row) => ({
|
|
2191
|
+
fromNodeId: row.from_node_id,
|
|
2192
|
+
referenceName: row.reference_name,
|
|
2193
|
+
referenceKind: row.reference_kind,
|
|
2194
|
+
line: row.line,
|
|
2195
|
+
column: row.col,
|
|
2196
|
+
candidates: row.candidates ? (0, utils_1.safeJsonParse)(row.candidates, undefined) : undefined,
|
|
2197
|
+
filePath: row.file_path,
|
|
2198
|
+
language: row.language,
|
|
2199
|
+
rowId: row.id,
|
|
2200
|
+
}));
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Resolution edges whose TARGET symbol is named one of `names` — the edges a
|
|
2204
|
+
* sync must re-resolve after `names` gained or lost a definition (CG-33).
|
|
2205
|
+
*
|
|
2206
|
+
* Resolution binds a reference to a node whose name matches the reference's
|
|
2207
|
+
* tail, and it picks among ALL same-named definitions project-wide. So adding
|
|
2208
|
+
* or removing one definition of `pct` changes the answer for every `pct(...)`
|
|
2209
|
+
* reference in the repo — including references in files this sync never
|
|
2210
|
+
* touches, whose edges nothing else revisits. Those edges' current target is,
|
|
2211
|
+
* by that same rule, a node named `pct`, which is why the target's name is a
|
|
2212
|
+
* sufficient (and index-backed, via idx_nodes_name) way to find them without
|
|
2213
|
+
* a schema change or a scan of edge metadata.
|
|
2214
|
+
*
|
|
2215
|
+
* Returns the source file/language alongside each edge so the caller can
|
|
2216
|
+
* resurrect it as its original reference. Excludes `provenance='heuristic'`
|
|
2217
|
+
* (synthesized dispatch edges are not resolution output and carry no refName
|
|
2218
|
+
* stamp to resurrect from — deleting one would be a permanent loss).
|
|
2219
|
+
*
|
|
2220
|
+
* Names matching more than `perNameCeiling` edges are skipped entirely, same
|
|
2221
|
+
* rationale and same default as {@link getRetryableFailedReferences}: at that
|
|
2222
|
+
* population the name is generic (`get`, `clear`, …), one definition changing
|
|
2223
|
+
* won't flip most of them, and rebinding an arbitrary subset is both wasted
|
|
2224
|
+
* work and incoherent coverage.
|
|
2225
|
+
*/
|
|
2226
|
+
getResolutionEdgesByTargetName(names, perNameCeiling = 500) {
|
|
2227
|
+
if (names.length === 0)
|
|
2228
|
+
return [];
|
|
2229
|
+
// Pass 1: per-name edge counts, chunked under the SQLite parameter limit.
|
|
2230
|
+
const keep = [];
|
|
2231
|
+
for (let i = 0; i < names.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2232
|
+
const chunk = names.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2233
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2234
|
+
const counts = this.db
|
|
2235
|
+
.prepare(`SELECT tgt.name AS name, COUNT(*) AS count
|
|
2236
|
+
FROM edges e
|
|
2237
|
+
JOIN nodes tgt ON tgt.id = e.target
|
|
2238
|
+
WHERE tgt.name IN (${placeholders})
|
|
2239
|
+
AND (e.provenance IS NULL OR e.provenance != 'heuristic')
|
|
2240
|
+
GROUP BY tgt.name`)
|
|
2241
|
+
.all(...chunk);
|
|
2242
|
+
for (const row of counts) {
|
|
2243
|
+
if (row.count <= perNameCeiling)
|
|
2244
|
+
keep.push(row.name);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
if (keep.length === 0)
|
|
2248
|
+
return [];
|
|
2249
|
+
// Pass 2: load the surviving edges with the source file context a
|
|
2250
|
+
// resurrection needs.
|
|
2251
|
+
const out = [];
|
|
2252
|
+
for (let i = 0; i < keep.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2253
|
+
const chunk = keep.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2254
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2255
|
+
const rows = this.db
|
|
2256
|
+
.prepare(`SELECT e.*, src.file_path AS source_file_path, src.language AS source_language
|
|
2257
|
+
FROM edges e
|
|
2258
|
+
JOIN nodes tgt ON tgt.id = e.target
|
|
2259
|
+
JOIN nodes src ON src.id = e.source
|
|
2260
|
+
WHERE tgt.name IN (${placeholders})
|
|
2261
|
+
AND (e.provenance IS NULL OR e.provenance != 'heuristic')`)
|
|
2262
|
+
.all(...chunk);
|
|
2263
|
+
for (const row of rows) {
|
|
2264
|
+
out.push({
|
|
2265
|
+
...rowToEdge(row),
|
|
2266
|
+
edgeId: row.id,
|
|
2267
|
+
sourceFilePath: row.source_file_path,
|
|
2268
|
+
sourceLanguage: row.source_language,
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return out;
|
|
2273
|
+
}
|
|
2274
|
+
/** Delete edges by primary key — the rebind pass's half of a re-resolution. */
|
|
2275
|
+
deleteEdgesByIds(edgeIds) {
|
|
2276
|
+
if (edgeIds.length === 0)
|
|
2277
|
+
return 0;
|
|
2278
|
+
let changed = 0;
|
|
2279
|
+
this.db.transaction(() => {
|
|
2280
|
+
for (let i = 0; i < edgeIds.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2281
|
+
const chunk = edgeIds.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2282
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2283
|
+
changed += this.db.prepare(`DELETE FROM edges WHERE id IN (${placeholders})`).run(...chunk).changes;
|
|
2284
|
+
}
|
|
2285
|
+
})();
|
|
2286
|
+
return changed;
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Distinct node names present in the given files — the symbol names a sync
|
|
2290
|
+
* pass uses to look up retryable failed refs after those files changed.
|
|
2291
|
+
*/
|
|
2292
|
+
getNodeNamesByFiles(filePaths) {
|
|
2293
|
+
if (filePaths.length === 0)
|
|
2294
|
+
return [];
|
|
2295
|
+
const names = new Set();
|
|
2296
|
+
for (let i = 0; i < filePaths.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2297
|
+
const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2298
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2299
|
+
const rows = this.db
|
|
2300
|
+
.prepare(`SELECT DISTINCT name FROM nodes WHERE file_path IN (${placeholders})`)
|
|
2301
|
+
.all(...chunk);
|
|
2302
|
+
for (const row of rows)
|
|
2303
|
+
names.add(row.name);
|
|
2304
|
+
}
|
|
2305
|
+
return [...names];
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Distinct `file\0name` pairs defined by the given files — the shape sync's
|
|
2309
|
+
* definition delta needs (CG-33).
|
|
2310
|
+
*
|
|
2311
|
+
* Deliberately NOT `getNodeNamesByFiles`: a bare name set is taken over the
|
|
2312
|
+
* WHOLE changed batch, so a name that moves between two files in one commit
|
|
2313
|
+
* (or exists in one changed file and is newly added to another) appears on
|
|
2314
|
+
* both sides and cancels out of the symmetric difference — even though a
|
|
2315
|
+
* definition genuinely appeared or vanished and every reference to that name
|
|
2316
|
+
* repo-wide may now bind elsewhere. Keying by file makes each definition its
|
|
2317
|
+
* own fact, so the move is seen as one removal plus one addition.
|
|
2318
|
+
*/
|
|
2319
|
+
getNodeNamePairsByFiles(filePaths) {
|
|
2320
|
+
const pairs = new Set();
|
|
2321
|
+
if (filePaths.length === 0)
|
|
2322
|
+
return pairs;
|
|
2323
|
+
for (let i = 0; i < filePaths.length; i += SQLITE_PARAM_CHUNK_SIZE) {
|
|
2324
|
+
const chunk = filePaths.slice(i, i + SQLITE_PARAM_CHUNK_SIZE);
|
|
2325
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
2326
|
+
const rows = this.db
|
|
2327
|
+
.prepare(`SELECT DISTINCT file_path, name FROM nodes WHERE file_path IN (${placeholders})`)
|
|
2328
|
+
.all(...chunk);
|
|
2329
|
+
// NUL-joined: a path or a symbol name can contain a space, never a NUL.
|
|
2330
|
+
for (const row of rows)
|
|
2331
|
+
pairs.add(`${row.file_path}\0${row.name}`);
|
|
2332
|
+
}
|
|
2333
|
+
return pairs;
|
|
2334
|
+
}
|
|
2335
|
+
// ===========================================================================
|
|
2336
|
+
// Statistics
|
|
2337
|
+
// ===========================================================================
|
|
2338
|
+
/**
|
|
2339
|
+
* Lightweight (nodes, edges) count snapshot. Used around an index/sync
|
|
2340
|
+
* run to compute true additions across extraction + resolution +
|
|
2341
|
+
* synthesis — the per-phase counter in the orchestrator only sees
|
|
2342
|
+
* extraction's contribution, which is why the CLI summary under-reported
|
|
2343
|
+
* the edge count (resolution + synthesizer edges were invisible).
|
|
2344
|
+
*/
|
|
2345
|
+
getNodeAndEdgeCount() {
|
|
2346
|
+
return this.db
|
|
2347
|
+
.prepare('SELECT (SELECT COUNT(*) FROM nodes) AS nodes, (SELECT COUNT(*) FROM edges) AS edges')
|
|
2348
|
+
.get();
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Get graph statistics
|
|
2352
|
+
*/
|
|
2353
|
+
getStats() {
|
|
2354
|
+
// Single query for all three aggregate counts
|
|
2355
|
+
const counts = this.db.prepare(`
|
|
2356
|
+
SELECT
|
|
2357
|
+
(SELECT COUNT(*) FROM nodes) AS node_count,
|
|
2358
|
+
(SELECT COUNT(*) FROM edges) AS edge_count,
|
|
2359
|
+
(SELECT COUNT(*) FROM files) AS file_count
|
|
2360
|
+
`).get();
|
|
2361
|
+
const nodesByKind = {};
|
|
2362
|
+
const nodeKindRows = this.db
|
|
2363
|
+
.prepare('SELECT kind, COUNT(*) as count FROM nodes GROUP BY kind')
|
|
2364
|
+
.all();
|
|
2365
|
+
for (const row of nodeKindRows) {
|
|
2366
|
+
nodesByKind[row.kind] = row.count;
|
|
2367
|
+
}
|
|
2368
|
+
const edgesByKind = {};
|
|
2369
|
+
const edgeKindRows = this.db
|
|
2370
|
+
.prepare('SELECT kind, COUNT(*) as count FROM edges GROUP BY kind')
|
|
2371
|
+
.all();
|
|
2372
|
+
for (const row of edgeKindRows) {
|
|
2373
|
+
edgesByKind[row.kind] = row.count;
|
|
2374
|
+
}
|
|
2375
|
+
const filesByLanguage = {};
|
|
2376
|
+
const languageRows = this.db
|
|
2377
|
+
.prepare('SELECT language, COUNT(*) as count FROM files GROUP BY language')
|
|
2378
|
+
.all();
|
|
2379
|
+
for (const row of languageRows) {
|
|
2380
|
+
filesByLanguage[row.language] = row.count;
|
|
2381
|
+
}
|
|
2382
|
+
return {
|
|
2383
|
+
nodeCount: counts.node_count,
|
|
2384
|
+
edgeCount: counts.edge_count,
|
|
2385
|
+
fileCount: counts.file_count,
|
|
2386
|
+
nodesByKind,
|
|
2387
|
+
edgesByKind,
|
|
2388
|
+
filesByLanguage,
|
|
2389
|
+
dbSizeBytes: 0, // Set by caller using DatabaseConnection.getSize()
|
|
2390
|
+
walSizeBytes: 0, // Set by caller using DatabaseConnection.getWalSizeBytes()
|
|
2391
|
+
lastUpdated: Date.now(),
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
// ===========================================================================
|
|
2395
|
+
// Project Metadata
|
|
2396
|
+
// ===========================================================================
|
|
2397
|
+
/**
|
|
2398
|
+
* Get a metadata value by key
|
|
2399
|
+
*/
|
|
2400
|
+
getMetadata(key) {
|
|
2401
|
+
const row = this.db.prepare('SELECT value FROM project_metadata WHERE key = ?').get(key);
|
|
2402
|
+
return row?.value ?? null;
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Set a metadata key-value pair (upsert)
|
|
2406
|
+
*/
|
|
2407
|
+
setMetadata(key, value) {
|
|
2408
|
+
this.db.prepare('INSERT INTO project_metadata (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at').run(key, value, Date.now());
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Get all metadata as a key-value record
|
|
2412
|
+
*/
|
|
2413
|
+
getAllMetadata() {
|
|
2414
|
+
const rows = this.db.prepare('SELECT key, value FROM project_metadata').all();
|
|
2415
|
+
const result = {};
|
|
2416
|
+
for (const row of rows) {
|
|
2417
|
+
result[row.key] = row.value;
|
|
2418
|
+
}
|
|
2419
|
+
return result;
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Clear all data from the database
|
|
2423
|
+
*/
|
|
2424
|
+
clear() {
|
|
2425
|
+
this.nodeCache.clear();
|
|
2426
|
+
this.db.transaction(() => {
|
|
2427
|
+
this.db.exec('DELETE FROM unresolved_refs');
|
|
2428
|
+
this.db.exec('DELETE FROM edges');
|
|
2429
|
+
this.db.exec('DELETE FROM nodes');
|
|
2430
|
+
this.db.exec('DELETE FROM files');
|
|
2431
|
+
})();
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
exports.QueryBuilder = QueryBuilder;
|
|
2435
|
+
//# sourceMappingURL=queries.js.map
|