@saluzi/codegraph 0.1.0 → 0.2.1
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 +885 -0
- package/dist/bin/codegraph.d.ts +2 -1
- package/dist/bin/codegraph.d.ts.map +1 -0
- package/dist/bin/codegraph.js +2248 -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.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.map +1 -0
- package/dist/bin/uninstall.js +36 -0
- package/dist/bin/uninstall.js.map +1 -0
- package/dist/context/formatter.d.ts +6 -9
- package/dist/context/formatter.d.ts.map +1 -0
- package/dist/context/formatter.js +25 -6
- package/dist/context/formatter.js.map +1 -0
- package/dist/context/index.d.ts +102 -100
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +252 -6
- 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 +185 -85
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +257 -14
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrations.d.ts +15 -18
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +82 -1
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/queries.d.ts +503 -281
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +644 -107
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.sql +69 -26
- package/dist/db/sqlite-adapter.d.ts +26 -22
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/sqlite-adapter.js +11 -35
- package/dist/db/sqlite-adapter.js.map +1 -0
- package/dist/db/wal-valve.d.ts +106 -0
- package/dist/db/wal-valve.d.ts.map +1 -0
- package/dist/db/wal-valve.js +208 -0
- package/dist/db/wal-valve.js.map +1 -0
- package/dist/directory.d.ts +140 -19
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +568 -35
- package/dist/directory.js.map +1 -0
- package/dist/errors.d.ts +32 -45
- package/dist/errors.d.ts.map +1 -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 +17 -17
- package/dist/extraction/dfm-extractor.d.ts.map +1 -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 +727 -0
- package/dist/extraction/function-ref.js.map +1 -0
- package/dist/extraction/generated-detection.d.ts +30 -0
- package/dist/extraction/generated-detection.d.ts.map +1 -0
- package/dist/extraction/generated-detection.js +83 -0
- package/dist/extraction/generated-detection.js.map +1 -0
- package/dist/extraction/grammars.d.ts +74 -30
- package/dist/extraction/grammars.d.ts.map +1 -0
- package/dist/extraction/grammars.js +289 -15
- package/dist/extraction/grammars.js.map +1 -0
- package/dist/extraction/index.d.ts +193 -109
- package/dist/extraction/index.d.ts.map +1 -0
- package/dist/extraction/index.js +1485 -362
- package/dist/extraction/index.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 +110 -4
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
- package/dist/extraction/languages/c-cpp.js +713 -2
- 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 -3
- package/dist/extraction/languages/csharp.d.ts.map +1 -0
- package/dist/extraction/languages/csharp.js +106 -3
- package/dist/extraction/languages/csharp.js.map +1 -0
- package/dist/extraction/languages/dart.d.ts +3 -3
- package/dist/extraction/languages/dart.d.ts.map +1 -0
- package/dist/extraction/languages/dart.js +183 -1
- 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 -3
- package/dist/extraction/languages/go.d.ts.map +1 -0
- package/dist/extraction/languages/go.js +55 -2
- package/dist/extraction/languages/go.js.map +1 -0
- package/dist/extraction/languages/index.d.ts +4 -4
- package/dist/extraction/languages/index.d.ts.map +1 -0
- package/dist/extraction/languages/index.js +22 -0
- package/dist/extraction/languages/index.js.map +1 -0
- package/dist/extraction/languages/java.d.ts +3 -3
- package/dist/extraction/languages/java.d.ts.map +1 -0
- package/dist/extraction/languages/java.js +252 -1
- package/dist/extraction/languages/java.js.map +1 -0
- package/dist/extraction/languages/javascript.d.ts +3 -3
- package/dist/extraction/languages/javascript.d.ts.map +1 -0
- package/dist/extraction/languages/javascript.js +16 -0
- package/dist/extraction/languages/javascript.js.map +1 -0
- package/dist/extraction/languages/kotlin.d.ts +3 -3
- package/dist/extraction/languages/kotlin.d.ts.map +1 -0
- package/dist/extraction/languages/kotlin.js +126 -0
- package/dist/extraction/languages/kotlin.js.map +1 -0
- package/dist/extraction/languages/lua.d.ts +3 -3
- package/dist/extraction/languages/lua.d.ts.map +1 -0
- package/dist/extraction/languages/lua.js.map +1 -0
- package/dist/extraction/languages/luau.d.ts +3 -3
- package/dist/extraction/languages/luau.d.ts.map +1 -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 +175 -0
- package/dist/extraction/languages/objc.js.map +1 -0
- package/dist/extraction/languages/pascal.d.ts +3 -3
- package/dist/extraction/languages/pascal.d.ts.map +1 -0
- package/dist/extraction/languages/pascal.js +11 -0
- package/dist/extraction/languages/pascal.js.map +1 -0
- package/dist/extraction/languages/php.d.ts +3 -3
- package/dist/extraction/languages/php.d.ts.map +1 -0
- package/dist/extraction/languages/php.js +90 -1
- package/dist/extraction/languages/php.js.map +1 -0
- package/dist/extraction/languages/python.d.ts +3 -3
- package/dist/extraction/languages/python.d.ts.map +1 -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 -3
- package/dist/extraction/languages/ruby.d.ts.map +1 -0
- package/dist/extraction/languages/ruby.js +35 -0
- package/dist/extraction/languages/ruby.js.map +1 -0
- package/dist/extraction/languages/rust.d.ts +3 -3
- package/dist/extraction/languages/rust.d.ts.map +1 -0
- package/dist/extraction/languages/rust.js +35 -2
- package/dist/extraction/languages/rust.js.map +1 -0
- package/dist/extraction/languages/scala.d.ts +3 -3
- package/dist/extraction/languages/scala.d.ts.map +1 -0
- package/dist/extraction/languages/scala.js +80 -10
- 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 -3
- package/dist/extraction/languages/swift.d.ts.map +1 -0
- package/dist/extraction/languages/swift.js +61 -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 -3
- package/dist/extraction/languages/typescript.d.ts.map +1 -0
- package/dist/extraction/languages/typescript.js +38 -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 +48 -41
- package/dist/extraction/liquid-extractor.d.ts.map +1 -0
- package/dist/extraction/liquid-extractor.js +53 -9
- 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 +152 -0
- package/dist/extraction/parse-pool.d.ts.map +1 -0
- package/dist/extraction/parse-pool.js +387 -0
- package/dist/extraction/parse-pool.js.map +1 -0
- package/dist/extraction/parse-worker.d.ts +2 -2
- package/dist/extraction/parse-worker.d.ts.map +1 -0
- package/dist/extraction/parse-worker.js +13 -3
- 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/svelte-extractor.d.ts +42 -42
- package/dist/extraction/svelte-extractor.d.ts.map +1 -0
- package/dist/extraction/svelte-extractor.js +6 -3
- package/dist/extraction/svelte-extractor.js.map +1 -0
- package/dist/extraction/tree-sitter-helpers.d.ts +7 -18
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
- package/dist/extraction/tree-sitter-helpers.js +60 -10
- package/dist/extraction/tree-sitter-helpers.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +224 -157
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
- package/dist/extraction/tree-sitter-types.js.map +1 -0
- package/dist/extraction/tree-sitter.d.ts +689 -221
- package/dist/extraction/tree-sitter.d.ts.map +1 -0
- package/dist/extraction/tree-sitter.js +4193 -117
- package/dist/extraction/tree-sitter.js.map +1 -0
- package/dist/extraction/vue-extractor.d.ts +40 -25
- package/dist/extraction/vue-extractor.d.ts.map +1 -0
- package/dist/extraction/vue-extractor.js +94 -3
- 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_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-erlang.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +6 -14
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
- package/dist/extraction/wasm-runtime-flags.js +1 -0
- package/dist/extraction/wasm-runtime-flags.js.map +1 -0
- package/dist/graph/index.d.ts +3 -3
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/queries.d.ts +96 -99
- package/dist/graph/queries.d.ts.map +1 -0
- package/dist/graph/queries.js +13 -40
- package/dist/graph/queries.js.map +1 -0
- package/dist/graph/traversal.d.ts +117 -127
- package/dist/graph/traversal.d.ts.map +1 -0
- package/dist/graph/traversal.js +92 -21
- package/dist/graph/traversal.js.map +1 -0
- package/dist/index.d.ts +615 -481
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +709 -18
- package/dist/index.js.map +1 -0
- package/dist/installer/config-writer.d.ts +13 -14
- 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 +84 -82
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +619 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/instructions-template.d.ts +35 -24
- 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 +34 -27
- package/dist/installer/targets/claude.d.ts.map +1 -0
- package/dist/installer/targets/claude.js +454 -0
- package/dist/installer/targets/claude.js.map +1 -0
- package/dist/installer/targets/codex.d.ts +3 -3
- 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/cursor.d.ts +3 -3
- 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/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 +3 -3
- 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 +12 -4
- 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 +9 -12
- package/dist/installer/targets/registry.d.ts.map +1 -0
- package/dist/installer/targets/registry.js +91 -0
- package/dist/installer/targets/registry.js.map +1 -0
- package/dist/installer/targets/shared.d.ts +38 -29
- package/dist/installer/targets/shared.d.ts.map +1 -0
- package/dist/installer/targets/shared.js +264 -0
- package/dist/installer/targets/shared.js.map +1 -0
- package/dist/installer/targets/toml.d.ts +11 -23
- package/dist/installer/targets/toml.d.ts.map +1 -0
- package/dist/installer/targets/toml.js +147 -0
- package/dist/installer/targets/toml.js.map +1 -0
- package/dist/installer/targets/types.d.ts +62 -76
- 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/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/index.d.ts +89 -74
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +506 -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 +267 -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 +667 -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 +22 -8
- package/dist/mcp/server-instructions.d.ts.map +1 -0
- package/dist/mcp/server-instructions.js +106 -0
- package/dist/mcp/server-instructions.js.map +1 -0
- package/dist/mcp/session.d.ts +93 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +357 -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 +557 -213
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +4477 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/transport.d.ts +158 -87
- 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/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 +1062 -0
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -0
- package/dist/resolution/callback-synthesizer.d.ts +15 -0
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
- package/dist/resolution/callback-synthesizer.js +3938 -0
- package/dist/resolution/callback-synthesizer.js.map +1 -0
- package/dist/resolution/cooperative-yield.d.ts +32 -0
- package/dist/resolution/cooperative-yield.d.ts.map +1 -0
- package/dist/resolution/cooperative-yield.js +42 -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 +3 -5
- package/dist/resolution/frameworks/cargo-workspace.d.ts.map +1 -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 +3 -3
- package/dist/resolution/frameworks/csharp.d.ts.map +1 -0
- package/dist/resolution/frameworks/csharp.js +36 -8
- package/dist/resolution/frameworks/csharp.js.map +1 -0
- package/dist/resolution/frameworks/drupal.d.ts +3 -3
- package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
- package/dist/resolution/frameworks/drupal.js +44 -12
- 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 +3 -3
- package/dist/resolution/frameworks/express.d.ts.map +1 -0
- package/dist/resolution/frameworks/express.js +102 -19
- 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 +3 -3
- package/dist/resolution/frameworks/go.d.ts.map +1 -0
- package/dist/resolution/frameworks/go.js +6 -3
- 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 +29 -31
- package/dist/resolution/frameworks/index.d.ts.map +1 -0
- package/dist/resolution/frameworks/index.js +39 -1
- package/dist/resolution/frameworks/index.js.map +1 -0
- package/dist/resolution/frameworks/java.d.ts +3 -3
- package/dist/resolution/frameworks/java.d.ts.map +1 -0
- package/dist/resolution/frameworks/java.js +352 -12
- package/dist/resolution/frameworks/java.js.map +1 -0
- package/dist/resolution/frameworks/laravel.d.ts +4 -4
- package/dist/resolution/frameworks/laravel.d.ts.map +1 -0
- package/dist/resolution/frameworks/laravel.js +17 -8
- package/dist/resolution/frameworks/laravel.js.map +1 -0
- package/dist/resolution/frameworks/nestjs.d.ts +3 -3
- package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
- package/dist/resolution/frameworks/nestjs.js +324 -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 +5 -5
- package/dist/resolution/frameworks/python.d.ts.map +1 -0
- package/dist/resolution/frameworks/python.js +138 -16
- 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 +3 -3
- package/dist/resolution/frameworks/react.d.ts.map +1 -0
- package/dist/resolution/frameworks/react.js +122 -60
- package/dist/resolution/frameworks/react.js.map +1 -0
- package/dist/resolution/frameworks/ruby.d.ts +3 -3
- package/dist/resolution/frameworks/ruby.d.ts.map +1 -0
- package/dist/resolution/frameworks/ruby.js +106 -2
- package/dist/resolution/frameworks/ruby.js.map +1 -0
- package/dist/resolution/frameworks/rust.d.ts +3 -3
- package/dist/resolution/frameworks/rust.d.ts.map +1 -0
- package/dist/resolution/frameworks/rust.js +102 -5
- package/dist/resolution/frameworks/rust.js.map +1 -0
- package/dist/resolution/frameworks/svelte.d.ts +3 -3
- package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
- package/dist/resolution/frameworks/svelte.js +5 -1
- 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 +5 -5
- package/dist/resolution/frameworks/swift.d.ts.map +1 -0
- package/dist/resolution/frameworks/swift.js +30 -6
- 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 +3 -3
- package/dist/resolution/frameworks/vue.d.ts.map +1 -0
- package/dist/resolution/frameworks/vue.js +24 -27
- 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 +53 -29
- package/dist/resolution/import-resolver.d.ts.map +1 -0
- package/dist/resolution/import-resolver.js +1319 -8
- package/dist/resolution/import-resolver.js.map +1 -0
- package/dist/resolution/index.d.ts +243 -103
- package/dist/resolution/index.d.ts.map +1 -0
- package/dist/resolution/index.js +1055 -53
- package/dist/resolution/index.js.map +1 -0
- package/dist/resolution/lru-cache.d.ts +9 -9
- package/dist/resolution/lru-cache.d.ts.map +1 -0
- package/dist/resolution/lru-cache.js.map +1 -0
- package/dist/resolution/name-matcher.d.ts +91 -29
- package/dist/resolution/name-matcher.d.ts.map +1 -0
- package/dist/resolution/name-matcher.js +1478 -32
- package/dist/resolution/name-matcher.js.map +1 -0
- package/dist/resolution/path-aliases.d.ts +23 -27
- package/dist/resolution/path-aliases.d.ts.map +1 -0
- package/dist/resolution/path-aliases.js.map +1 -0
- package/dist/resolution/strip-comments.d.ts +3 -16
- package/dist/resolution/strip-comments.d.ts.map +1 -0
- package/dist/resolution/strip-comments.js +51 -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 +217 -136
- package/dist/resolution/types.d.ts.map +1 -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 +14 -18
- package/dist/search/query-parser.d.ts.map +1 -0
- package/dist/search/query-parser.js.map +1 -0
- package/dist/search/query-utils.d.ts +45 -17
- package/dist/search/query-utils.d.ts.map +1 -0
- package/dist/search/query-utils.js +113 -47
- package/dist/search/query-utils.js.map +1 -0
- package/dist/sync/git-hooks.d.ts +13 -22
- package/dist/sync/git-hooks.d.ts.map +1 -0
- package/dist/sync/git-hooks.js +4 -0
- package/dist/sync/git-hooks.js.map +1 -0
- package/dist/sync/index.d.ts +6 -12
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +8 -1
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/watch-policy.d.ts +8 -11
- package/dist/sync/watch-policy.d.ts.map +1 -0
- package/dist/sync/watch-policy.js.map +1 -0
- package/dist/sync/watcher.d.ts +335 -63
- package/dist/sync/watcher.d.ts.map +1 -0
- package/dist/sync/watcher.js +709 -53
- 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 +308 -326
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/glyphs.d.ts +20 -20
- package/dist/ui/glyphs.d.ts.map +1 -0
- package/dist/ui/glyphs.js +78 -0
- package/dist/ui/glyphs.js.map +1 -0
- package/dist/ui/shimmer-progress.d.ts +7 -7
- package/dist/ui/shimmer-progress.d.ts.map +1 -0
- package/dist/ui/shimmer-progress.js +90 -0
- package/dist/ui/shimmer-progress.js.map +1 -0
- package/dist/ui/shimmer-worker.d.ts +2 -2
- package/dist/ui/shimmer-worker.d.ts.map +1 -0
- package/dist/ui/shimmer-worker.js +118 -0
- package/dist/ui/shimmer-worker.js.map +1 -0
- package/dist/ui/types.d.ts +14 -17
- 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 +164 -0
- package/dist/upgrade/index.d.ts.map +1 -0
- package/dist/upgrade/index.js +646 -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 +116 -123
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +83 -49
- package/dist/utils.js.map +1 -0
- package/package.json +42 -10
- 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 +102 -0
- package/scripts/agent-eval/ab-sufficiency.sh +78 -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 +28 -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/hook-settings.json +15 -0
- package/scripts/agent-eval/itrun.sh +120 -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 +84 -0
- package/scripts/agent-eval/parse-run.mjs +45 -0
- package/scripts/agent-eval/parse-session.mjs +93 -0
- package/scripts/agent-eval/probe-context.mjs +21 -0
- package/scripts/agent-eval/probe-explore.mjs +40 -0
- package/scripts/agent-eval/probe-node.mjs +20 -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 +75 -0
- package/scripts/agent-eval/run-arms.sh +56 -0
- package/scripts/agent-eval/seq-matrix.mjs +137 -0
- package/scripts/build-bundle.sh +123 -0
- package/scripts/extract-release-notes.mjs +130 -0
- package/scripts/local-install.sh +41 -0
- package/scripts/npm-sdk.js +75 -0
- package/scripts/npm-shim.js +275 -0
- package/scripts/pack-npm.sh +119 -0
- package/scripts/prepare-release.mjs +270 -0
- package/dist/extraction/wasm/tree-sitter-c.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-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-php.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-python.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-swift.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/installer/claude-md-template.d.ts +0 -19
package/dist/extraction/index.js
CHANGED
|
@@ -41,33 +41,54 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
41
41
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
42
|
};
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
-
exports.loadAllGrammars = exports.loadGrammarsForLanguages = exports.initGrammars = exports.getSupportedLanguages = exports.isGrammarLoaded = exports.isLanguageSupported = exports.isSourceFile = exports.detectLanguage = exports.extractFromSource = exports.ExtractionOrchestrator = void 0;
|
|
44
|
+
exports.loadAllGrammars = exports.loadGrammarsForLanguages = exports.initGrammars = exports.getSupportedLanguages = exports.isGrammarLoaded = exports.isLanguageSupported = exports.isSourceFile = exports.detectLanguage = exports.extractFromSource = exports.ExtractionOrchestrator = exports.ScopeIgnore = void 0;
|
|
45
45
|
exports.hashContent = hashContent;
|
|
46
|
+
exports.buildDefaultIgnore = buildDefaultIgnore;
|
|
47
|
+
exports.buildScopeIgnore = buildScopeIgnore;
|
|
48
|
+
exports.discoverEmbeddedRepoRoots = discoverEmbeddedRepoRoots;
|
|
49
|
+
exports.findUnindexedIgnoredRepos = findUnindexedIgnoredRepos;
|
|
46
50
|
exports.scanDirectory = scanDirectory;
|
|
47
51
|
exports.scanDirectoryAsync = scanDirectoryAsync;
|
|
48
52
|
const fs = __importStar(require("fs"));
|
|
49
53
|
const fsp = __importStar(require("fs/promises"));
|
|
50
54
|
const path = __importStar(require("path"));
|
|
55
|
+
const os = __importStar(require("os"));
|
|
51
56
|
const crypto = __importStar(require("crypto"));
|
|
52
57
|
const child_process_1 = require("child_process");
|
|
53
58
|
const tree_sitter_1 = require("./tree-sitter");
|
|
59
|
+
const parse_pool_1 = require("./parse-pool");
|
|
54
60
|
const grammars_1 = require("./grammars");
|
|
61
|
+
const project_config_1 = require("../project-config");
|
|
62
|
+
const directory_1 = require("../directory");
|
|
55
63
|
const errors_1 = require("../errors");
|
|
56
64
|
const utils_1 = require("../utils");
|
|
57
65
|
const ignore_1 = __importDefault(require("ignore"));
|
|
58
66
|
const frameworks_1 = require("../resolution/frameworks");
|
|
67
|
+
const cooperative_yield_1 = require("../resolution/cooperative-yield");
|
|
59
68
|
/**
|
|
60
69
|
* Number of files to read in parallel during indexing.
|
|
61
70
|
* File reads are I/O-bound; batching overlaps I/O wait with CPU parse work.
|
|
62
71
|
*/
|
|
63
72
|
const FILE_IO_BATCH_SIZE = 10;
|
|
73
|
+
/**
|
|
74
|
+
* How many files the `sync()` reconcile processes between cooperative yields to
|
|
75
|
+
* the event loop. The reconcile runs two O(files) loops of synchronous `fs`
|
|
76
|
+
* calls (existsSync for removals, statSync for adds/mods); on a very large repo
|
|
77
|
+
* (~100k files) an un-yielded run wedges the main thread for minutes, which both
|
|
78
|
+
* trips the liveness watchdog (it SIGKILLs a process whose loop stops turning)
|
|
79
|
+
* and blocks the first MCP tool call behind the catch-up gate (issue #905).
|
|
80
|
+
* Yielding every N files keeps the socket, the watchdog heartbeat, and any
|
|
81
|
+
* concurrent read query responsive while the reconcile runs.
|
|
82
|
+
*/
|
|
83
|
+
const SYNC_RECONCILE_YIELD_INTERVAL = 1000;
|
|
64
84
|
// PARSER_RESET_INTERVAL moved to parse-worker.ts (runs in worker thread)
|
|
65
85
|
/**
|
|
66
86
|
* Maximum time (ms) to wait for a single file to parse in the worker thread.
|
|
67
87
|
* If tree-sitter hangs or WASM runs out of memory, this prevents the entire
|
|
68
|
-
* indexing run from freezing. The worker is restarted after a timeout.
|
|
88
|
+
* indexing run from freezing. The worker is restarted after a (hard) timeout.
|
|
89
|
+
* Env-overridable via CODEGRAPH_PARSE_TIMEOUT_MS for slow storage (#1231).
|
|
69
90
|
*/
|
|
70
|
-
const PARSE_TIMEOUT_MS =
|
|
91
|
+
const PARSE_TIMEOUT_MS = (0, parse_pool_1.resolveParseTimeoutMs)(process.env.CODEGRAPH_PARSE_TIMEOUT_MS);
|
|
71
92
|
/**
|
|
72
93
|
* Number of files to parse before recycling the worker thread.
|
|
73
94
|
* WASM linear memory can grow but NEVER shrink (WebAssembly spec limitation).
|
|
@@ -88,6 +109,740 @@ function hashContent(content) {
|
|
|
88
109
|
* symbols. 1 MB covers essentially all hand-written source.
|
|
89
110
|
*/
|
|
90
111
|
const MAX_FILE_SIZE = 1024 * 1024;
|
|
112
|
+
/**
|
|
113
|
+
* Directory names that are dependency, build, cache, or tooling output across the
|
|
114
|
+
* languages/frameworks CodeGraph supports — curated from the canonical
|
|
115
|
+
* github/gitignore templates. Excluded by default so the graph reflects your code,
|
|
116
|
+
* not third-party noise, without requiring a `.gitignore` (issue #407). The
|
|
117
|
+
* exclusion applies uniformly (git or not, tracked or not); the only opt-in is an
|
|
118
|
+
* explicit `.gitignore` negation (e.g. `!vendor/`). First-party-prone or generic
|
|
119
|
+
* names (`packages`, `lib`, `app`, `bin`, `src`, `deps`, `env`, `tmp`, `storage`,
|
|
120
|
+
* `Library`) are deliberately NOT listed, to avoid ever hiding real source.
|
|
121
|
+
*
|
|
122
|
+
* Only dirs that actually contain *indexable source* (or are enormous) earn a slot
|
|
123
|
+
* — IDE/state dirs like `.idea`/`.vs` are omitted because CodeGraph indexes only
|
|
124
|
+
* recognized source extensions, so they produce no symbols regardless.
|
|
125
|
+
*/
|
|
126
|
+
const DEFAULT_IGNORE_DIRS = new Set([
|
|
127
|
+
// JS / TS — dependency directories
|
|
128
|
+
'node_modules', 'bower_components', 'jspm_packages', 'web_modules',
|
|
129
|
+
'.yarn', '.pnpm-store',
|
|
130
|
+
// JS / TS — framework & bundler build / cache / deploy output
|
|
131
|
+
'.next', '.nuxt', '.svelte-kit', '.turbo', '.vite', '.parcel-cache', '.angular',
|
|
132
|
+
'.docusaurus', 'storybook-static', '.vinxi', '.nitro', 'out-tsc',
|
|
133
|
+
'.vercel', '.netlify', '.wrangler',
|
|
134
|
+
// Build output (common across ecosystems)
|
|
135
|
+
'dist', 'build', 'out', '.output',
|
|
136
|
+
// Test / coverage
|
|
137
|
+
'coverage', '.nyc_output',
|
|
138
|
+
// Python
|
|
139
|
+
'__pycache__', '__pypackages__', '.venv', 'venv', '.pixi', '.pdm-build',
|
|
140
|
+
'.mypy_cache', '.pytest_cache', '.ruff_cache', '.tox', '.nox', '.hypothesis',
|
|
141
|
+
'.ipynb_checkpoints', '.eggs',
|
|
142
|
+
// Rust / JVM (Maven, Gradle, Scala)
|
|
143
|
+
'target', '.gradle',
|
|
144
|
+
// .NET
|
|
145
|
+
'obj',
|
|
146
|
+
// Vendored deps (Go, PHP/Composer, Ruby/Bundler)
|
|
147
|
+
'vendor',
|
|
148
|
+
// Swift / iOS
|
|
149
|
+
'.build', 'Pods', 'Carthage', 'DerivedData', '.swiftpm',
|
|
150
|
+
// Dart / Flutter
|
|
151
|
+
'.dart_tool', '.pub-cache',
|
|
152
|
+
// Native (Android NDK, C/C++ deps)
|
|
153
|
+
'.cxx', '.externalNativeBuild', 'vcpkg_installed',
|
|
154
|
+
// Scala tooling
|
|
155
|
+
'.bloop', '.metals',
|
|
156
|
+
// Lua / Luau (LuaRocks)
|
|
157
|
+
'lua_modules', '.luarocks',
|
|
158
|
+
// Delphi / RAD Studio IDE backups (duplicate .pas source — would double-count)
|
|
159
|
+
'__history', '__recovery',
|
|
160
|
+
// Generic cache
|
|
161
|
+
'.cache',
|
|
162
|
+
]);
|
|
163
|
+
/**
|
|
164
|
+
* Android resource directory types. A `res/` tree holds ONLY non-code resources —
|
|
165
|
+
* layouts, drawables, value bags (strings/colors/styles), menus, navigation
|
|
166
|
+
* graphs — split into one typed subdirectory per kind, optionally density/locale/
|
|
167
|
+
* version-qualified (`values-es`, `drawable-hdpi`, `layout-v21`, …). None of it
|
|
168
|
+
* yields an extractable code symbol, yet on an Android app it DOMINATES the tree
|
|
169
|
+
* (one report: 26k XML files = 97% of the project, 0 symbols), bloating the DB,
|
|
170
|
+
* slowing indexing, and skewing both the file count and `codegraph_explore`
|
|
171
|
+
* results (#1047). So these are excluded by default. The structure is
|
|
172
|
+
* self-identifying — a non-Android project has no `res/layout/` etc., so it's
|
|
173
|
+
* untouched — and the only XML that DOES produce symbols (MyBatis mappers) lives
|
|
174
|
+
* under `src/main/resources/`, never `res/`, so nothing useful is dropped.
|
|
175
|
+
* `res/raw/` is deliberately NOT here: it holds arbitrary bundled assets that can
|
|
176
|
+
* be code-ish (a `.sql` schema, a `.js`), so we leave it indexed. Override any of
|
|
177
|
+
* these with a `.gitignore` negation (e.g. `!res/values/`).
|
|
178
|
+
*/
|
|
179
|
+
const ANDROID_RES_TYPES = [
|
|
180
|
+
'anim', 'animator', 'color', 'drawable', 'font', 'layout',
|
|
181
|
+
'menu', 'mipmap', 'navigation', 'transition', 'values', 'xml',
|
|
182
|
+
];
|
|
183
|
+
/** Gitignore-style patterns for the `ignore` matcher: the dirs above plus a few globs. */
|
|
184
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
185
|
+
...Array.from(DEFAULT_IGNORE_DIRS, (d) => `${d}/`),
|
|
186
|
+
'*.egg-info/', // Python packaging metadata
|
|
187
|
+
'cmake-build-*/', // CLion / CMake build trees
|
|
188
|
+
'bazel-*/', // Bazel output symlink trees
|
|
189
|
+
// Android resource dirs at any depth, with their qualifier variants (#1047).
|
|
190
|
+
...ANDROID_RES_TYPES.map((t) => `**/res/${t}*/`),
|
|
191
|
+
];
|
|
192
|
+
/** True if `buf` decodes as strict UTF-8 (no invalid byte sequences). */
|
|
193
|
+
function isValidUtf8(buf) {
|
|
194
|
+
try {
|
|
195
|
+
new TextDecoder('utf-8', { fatal: true }).decode(buf);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Read a `.gitignore` and return patterns safe to hand to the `ignore` matcher —
|
|
204
|
+
* never throwing, even when the file isn't real gitignore text. Two failure
|
|
205
|
+
* modes, both seen in the wild (issue #682):
|
|
206
|
+
*
|
|
207
|
+
* - The file isn't valid UTF-8 — e.g. transparently encrypted in place by
|
|
208
|
+
* corporate DLP / endpoint-security software, leaving a UTF-16 header plus
|
|
209
|
+
* ciphertext. None of it is meaningful patterns, so the whole file is skipped.
|
|
210
|
+
* - The file is text but a single line can't be compiled to a regex by the
|
|
211
|
+
* `ignore` library — `\\[` and friends throw "Unterminated character class".
|
|
212
|
+
* Crucially the throw is LAZY (at match time, not `.add()`), so it would
|
|
213
|
+
* otherwise escape mid-scan. That one pattern is dropped; the rest are kept.
|
|
214
|
+
*
|
|
215
|
+
* Either way a warning that NAMES the file is logged (the reporter couldn't tell
|
|
216
|
+
* which `.gitignore` was at fault) and indexing continues instead of aborting.
|
|
217
|
+
* Returns '' when there's nothing usable.
|
|
218
|
+
*/
|
|
219
|
+
function readGitignorePatterns(giPath) {
|
|
220
|
+
let buf;
|
|
221
|
+
try {
|
|
222
|
+
buf = fs.readFileSync(giPath);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return ''; // unreadable (permissions / race) — treat as absent
|
|
226
|
+
}
|
|
227
|
+
// A NUL byte never appears in real gitignore text, and a fatal UTF-8 decode
|
|
228
|
+
// catches the rest. Such a file isn't ignore patterns at all.
|
|
229
|
+
if (buf.includes(0) || !isValidUtf8(buf)) {
|
|
230
|
+
(0, errors_1.logWarn)('Ignoring a .gitignore that is not valid UTF-8 text — it may have been encrypted ' +
|
|
231
|
+
'in place by endpoint-security software. Indexing continues without it.', { file: giPath });
|
|
232
|
+
return '';
|
|
233
|
+
}
|
|
234
|
+
const content = buf.toString('utf-8');
|
|
235
|
+
// Fast path: one `.ignores()` call forces the library to compile EVERY rule,
|
|
236
|
+
// so if it doesn't throw, the whole file is safe to use verbatim.
|
|
237
|
+
try {
|
|
238
|
+
(0, ignore_1.default)().add(content).ignores('.codegraph-probe');
|
|
239
|
+
return content;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Fall through: a line is uncompilable — keep the good ones, drop the bad.
|
|
243
|
+
}
|
|
244
|
+
const kept = [];
|
|
245
|
+
let dropped = 0;
|
|
246
|
+
for (const line of content.split(/\r?\n/)) {
|
|
247
|
+
try {
|
|
248
|
+
(0, ignore_1.default)().add(line).ignores('.codegraph-probe');
|
|
249
|
+
kept.push(line);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
dropped++;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (dropped > 0) {
|
|
256
|
+
(0, errors_1.logWarn)(`Skipped ${dropped} unparseable pattern(s) in a .gitignore; the rest are applied.`, { file: giPath });
|
|
257
|
+
}
|
|
258
|
+
return kept.join('\n');
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* An `ignore` matcher seeded with the built-in defaults, merged with the project's
|
|
262
|
+
* root .gitignore so a negation there (e.g. `!vendor/`) overrides a default. Shared
|
|
263
|
+
* by both enumeration paths so behavior is identical with or without git — and so
|
|
264
|
+
* the defaults apply to tracked files too (committing a dependency dir doesn't make
|
|
265
|
+
* it project code; the explicit `.gitignore` negation is the only opt-in).
|
|
266
|
+
*/
|
|
267
|
+
function buildDefaultIgnore(rootDir) {
|
|
268
|
+
const ig = (0, ignore_1.default)().add(DEFAULT_IGNORE_PATTERNS);
|
|
269
|
+
const rootGitignore = path.join(rootDir, '.gitignore');
|
|
270
|
+
if (fs.existsSync(rootGitignore))
|
|
271
|
+
ig.add(readGitignorePatterns(rootGitignore));
|
|
272
|
+
return ig;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Defaults-only ignore matcher (no root `.gitignore` merged). Used wherever the
|
|
276
|
+
* parent repo's own ignore rules must NOT apply — inside embedded child repos,
|
|
277
|
+
* whose gitignore semantics their own `git ls-files` already enforced (#514).
|
|
278
|
+
*/
|
|
279
|
+
function defaultsOnlyIgnore() {
|
|
280
|
+
return (0, ignore_1.default)().add(DEFAULT_IGNORE_PATTERNS);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Matcher for the project's `codegraph.json` `includeIgnored` patterns — the
|
|
284
|
+
* explicit opt-in to index embedded git repos living inside gitignored
|
|
285
|
+
* directories (#622, #699). Returns `null` when the project opted in nothing,
|
|
286
|
+
* which is the zero-config DEFAULT: `.gitignore` is then fully respected and a
|
|
287
|
+
* gitignored directory (even one holding nested repos) is never walked or
|
|
288
|
+
* indexed (#970, #976). Built once per scan/sync/scope operation from the scan
|
|
289
|
+
* root and threaded down — never global, so multi-project daemons stay isolated.
|
|
290
|
+
*/
|
|
291
|
+
function loadIncludeIgnoredMatcher(rootDir) {
|
|
292
|
+
const patterns = (0, project_config_1.loadIncludeIgnoredPatterns)(rootDir);
|
|
293
|
+
return patterns.length > 0 ? (0, ignore_1.default)().add(patterns) : null;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Matcher for the project's `codegraph.json` `exclude` patterns — paths to keep
|
|
297
|
+
* OUT of the index even when git-tracked, which `.gitignore` cannot do (#999).
|
|
298
|
+
* The escape hatch for a committed vendor/theme/SDK directory. Returns `null`
|
|
299
|
+
* when nothing is excluded (the zero-config default → no overhead). Matched
|
|
300
|
+
* against project-root-relative paths, so it applies uniformly across the whole
|
|
301
|
+
* workspace, including inside embedded repos (excluding `static/` means gone
|
|
302
|
+
* everywhere). Built once per scan/sync/scope operation from the scan root.
|
|
303
|
+
*/
|
|
304
|
+
function loadExcludeMatcher(rootDir) {
|
|
305
|
+
const patterns = (0, project_config_1.loadExcludePatterns)(rootDir);
|
|
306
|
+
return patterns.length > 0 ? (0, ignore_1.default)().add(patterns) : null;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Matcher for the project's `codegraph.json` `include` patterns — first-party
|
|
310
|
+
* source to force INTO the index even when `.gitignore` drops it (the general
|
|
311
|
+
* whitelist `includeIgnored` never was — that one only revives *embedded git
|
|
312
|
+
* repos*). The case it exists for: a project under a second VCS (SVN/Perforce)
|
|
313
|
+
* `.gitignore`s its own real source so it stays out of Git, yet we still want it
|
|
314
|
+
* indexed. Returns `null` when nothing is force-included (the zero-config
|
|
315
|
+
* default → no overhead, no extra walk). Built once per scan/sync/scope
|
|
316
|
+
* operation from the scan root.
|
|
317
|
+
*/
|
|
318
|
+
function loadIncludeMatcher(rootDir) {
|
|
319
|
+
const patterns = (0, project_config_1.loadIncludePatterns)(rootDir);
|
|
320
|
+
return patterns.length > 0 ? (0, ignore_1.default)().add(patterns) : null;
|
|
321
|
+
}
|
|
322
|
+
/** Glob metacharacters that end the static (literal) prefix of an `include` pattern. */
|
|
323
|
+
const GLOB_META = /[*?[\]{}!]/;
|
|
324
|
+
/**
|
|
325
|
+
* The static directory prefix of each `include` pattern — the literal leading
|
|
326
|
+
* path up to the first glob segment — trailing-slashed, used to (a) walk only
|
|
327
|
+
* the opted-in subtrees in `collectIncludedFiles` and (b) let `ScopeIgnore` keep
|
|
328
|
+
* the watcher descending toward them. `Tools/` stays `Tools/`; a recursive
|
|
329
|
+
* `Tools/**` glob yields `Tools/`; `src/local/file.ts` yields `src/local/` (the
|
|
330
|
+
* file's dir); a pattern that starts with a glob (like a leading `**`) yields
|
|
331
|
+
* `''`, meaning "no static root — walk the whole tree". Duplicates and roots
|
|
332
|
+
* nested under a broader root are collapsed so each subtree is walked once.
|
|
333
|
+
*/
|
|
334
|
+
function includeStaticRoots(patterns) {
|
|
335
|
+
const roots = new Set();
|
|
336
|
+
for (const pattern of patterns) {
|
|
337
|
+
let p = pattern.replace(/^\/+/, '');
|
|
338
|
+
const trailingSlash = p.endsWith('/');
|
|
339
|
+
if (trailingSlash)
|
|
340
|
+
p = p.slice(0, -1);
|
|
341
|
+
const segs = p.split('/').filter(Boolean);
|
|
342
|
+
const lead = [];
|
|
343
|
+
for (const s of segs) {
|
|
344
|
+
if (GLOB_META.test(s))
|
|
345
|
+
break;
|
|
346
|
+
lead.push(s);
|
|
347
|
+
}
|
|
348
|
+
const hadWildcard = lead.length < segs.length;
|
|
349
|
+
// A wholly-literal pattern with no trailing slash names a file (or a dir we
|
|
350
|
+
// can't tell apart) — drop its last segment so we walk the containing dir
|
|
351
|
+
// and let the matcher pick the file. A trailing slash or a glob means the
|
|
352
|
+
// remaining `lead` is already the directory to walk.
|
|
353
|
+
if (!hadWildcard && !trailingSlash && lead.length > 0)
|
|
354
|
+
lead.pop();
|
|
355
|
+
if (lead.length === 0) {
|
|
356
|
+
roots.clear();
|
|
357
|
+
roots.add('');
|
|
358
|
+
return ['']; // a top-level glob forces a whole-tree walk; nothing narrower matters
|
|
359
|
+
}
|
|
360
|
+
roots.add(lead.join('/') + '/');
|
|
361
|
+
}
|
|
362
|
+
// Collapse roots nested under a broader one (e.g. drop `a/b/` if `a/` is present).
|
|
363
|
+
const all = [...roots];
|
|
364
|
+
return all.filter((r) => !all.some((other) => other !== r && r.startsWith(other)));
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Actively discover the source files an `include` whitelist forces in. `git
|
|
368
|
+
* ls-files` never lists gitignored files, so a filtered filesystem walk of just
|
|
369
|
+
* the opted-in subtrees (`includeStaticRoots`) is the only way to find them.
|
|
370
|
+
* Returns project-root-relative, normalized source-file paths.
|
|
371
|
+
*
|
|
372
|
+
* A file is collected when it MATCHES `include`, is NOT hit by `exclude` (an
|
|
373
|
+
* explicit exclude always wins), is a recognized source file, and does not live
|
|
374
|
+
* under a built-in default-ignored dir (`node_modules`, `dist`, …), `.git`, or
|
|
375
|
+
* CodeGraph's data dir — those are never resurfaced, mirroring `ScopeIgnore`.
|
|
376
|
+
* `.gitignore` is deliberately NOT consulted: overriding it is the whole point.
|
|
377
|
+
*/
|
|
378
|
+
function collectIncludedFiles(rootDir, include, exclude, roots, overrides) {
|
|
379
|
+
const out = new Set();
|
|
380
|
+
const defaults = defaultsOnlyIgnore();
|
|
381
|
+
const visited = new Set();
|
|
382
|
+
const consider = (abs, rel, isDir) => {
|
|
383
|
+
if (isDir) {
|
|
384
|
+
if (defaults.ignores(rel + '/'))
|
|
385
|
+
return; // never node_modules/dist/… via include
|
|
386
|
+
// An explicit `exclude` always wins over `include`; prune the whole subtree
|
|
387
|
+
// here so a large excluded dir (a committed frontend's own vendored deps,
|
|
388
|
+
// build output, …) is never walked — the per-file guard below still catches
|
|
389
|
+
// anything a directory pattern doesn't, so this is a pure efficiency win.
|
|
390
|
+
if (exclude && exclude.ignores(rel + '/'))
|
|
391
|
+
return;
|
|
392
|
+
walk(abs);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
if (defaults.ignores(rel))
|
|
396
|
+
return;
|
|
397
|
+
if (!include.ignores(rel))
|
|
398
|
+
return;
|
|
399
|
+
if (exclude && exclude.ignores(rel))
|
|
400
|
+
return;
|
|
401
|
+
if (!(0, grammars_1.isSourceFile)(rel, overrides))
|
|
402
|
+
return;
|
|
403
|
+
out.add(rel);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
function walk(absDir) {
|
|
407
|
+
let realDir;
|
|
408
|
+
try {
|
|
409
|
+
realDir = fs.realpathSync(absDir);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (visited.has(realDir))
|
|
415
|
+
return; // symlink-cycle guard
|
|
416
|
+
visited.add(realDir);
|
|
417
|
+
let entries;
|
|
418
|
+
try {
|
|
419
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
if (entry.name === '.git' || (0, directory_1.isCodeGraphDataDir)(entry.name))
|
|
426
|
+
continue;
|
|
427
|
+
const abs = path.join(absDir, entry.name);
|
|
428
|
+
const rel = (0, utils_1.normalizePath)(path.relative(rootDir, abs));
|
|
429
|
+
if (!rel || rel.startsWith('..'))
|
|
430
|
+
continue;
|
|
431
|
+
if (entry.isSymbolicLink()) {
|
|
432
|
+
try {
|
|
433
|
+
const st = fs.statSync(fs.realpathSync(abs));
|
|
434
|
+
consider(abs, rel, st.isDirectory());
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// broken symlink — skip
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
consider(abs, rel, entry.isDirectory());
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const root of roots) {
|
|
445
|
+
walk(root === '' ? rootDir : path.join(rootDir, root));
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The included source files (`codegraph.json` `include`) for a scan root, or an
|
|
451
|
+
* empty set when nothing is force-included. Centralizes loading the matcher,
|
|
452
|
+
* roots, exclude, and overrides so both enumeration paths (git and filesystem
|
|
453
|
+
* walk) add the same files.
|
|
454
|
+
*/
|
|
455
|
+
function collectIncludedFilesForRoot(rootDir) {
|
|
456
|
+
const include = loadIncludeMatcher(rootDir);
|
|
457
|
+
if (!include)
|
|
458
|
+
return new Set();
|
|
459
|
+
const roots = includeStaticRoots((0, project_config_1.loadIncludePatterns)(rootDir));
|
|
460
|
+
return collectIncludedFiles(rootDir, include, loadExcludeMatcher(rootDir), roots, (0, project_config_1.loadExtensionOverrides)(rootDir));
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* `git ls-files --directory` collapses a wholly-untracked/ignored directory into
|
|
464
|
+
* one entry — and when the command's own cwd is such a directory (the indexed
|
|
465
|
+
* root is itself a git-ignored subdir of an enclosing repo), git emits the
|
|
466
|
+
* literal `./` meaning "this entire directory". That sentinel is not a real
|
|
467
|
+
* nested path: feeding it to the `ignore` matcher throws ("path should be a
|
|
468
|
+
* `path.relative()`d string, but got "./""), which used to abort `buildScopeIgnore`
|
|
469
|
+
* and so break the MCP daemon's watcher/auto-sync on connect; and joining it back
|
|
470
|
+
* onto `repoDir` would just re-point at the cwd. Drop it wherever we consume
|
|
471
|
+
* `--directory` output. (#936)
|
|
472
|
+
*/
|
|
473
|
+
function isWholeCwdEntry(entry) {
|
|
474
|
+
return entry === './' || entry === '.' || entry === '';
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* List the gitignored DIRECTORIES of a repo (collapsed, trailing-slash form),
|
|
478
|
+
* relative to `repoDir`. These are invisible to every other `git ls-files` /
|
|
479
|
+
* `git status` mode — and in a multi-repo workspace they are exactly where the
|
|
480
|
+
* nested project repos live (a super-repo `.gitignore`s its child repos to keep
|
|
481
|
+
* `git status` quiet; that does not make them third-party code). (#514)
|
|
482
|
+
*/
|
|
483
|
+
function listIgnoredDirs(repoDir) {
|
|
484
|
+
try {
|
|
485
|
+
const out = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-o', '-i', '--exclude-standard', '--directory'], { cwd: repoDir, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
486
|
+
return out.split('\0').filter((e) => e.endsWith('/') && !isWholeCwdEntry(e));
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** Max directory depth searched below an ignored dir for nested `.git` roots. */
|
|
493
|
+
const EMBEDDED_REPO_SEARCH_DEPTH = 4;
|
|
494
|
+
/** Max directories examined per search — a huge ignored data dir must never stall a scan/sync. */
|
|
495
|
+
const EMBEDDED_REPO_SEARCH_ENTRIES = 2000;
|
|
496
|
+
/**
|
|
497
|
+
* Classify a directory's `.git` entry for embedded-repo discovery.
|
|
498
|
+
*
|
|
499
|
+
* - A `.git` **directory** is an embedded clone — distinct first-party code a
|
|
500
|
+
* super-repo merely hides from git; index it (#193, #514).
|
|
501
|
+
* - A `.git` **file** is a pointer (`gitdir: …`). A git **worktree** points into
|
|
502
|
+
* the host repo's own `.git/worktrees/<name>`, so it is a second working view
|
|
503
|
+
* of a repo CodeGraph already indexes — indexing it just duplicates the whole
|
|
504
|
+
* graph N times; skip it (#848). A **submodule worktree** points into
|
|
505
|
+
* `.git/modules/<module>/worktrees/<name>` — same duplication, so skip it too
|
|
506
|
+
* (#945). A **submodule** checkout points into `.git/modules/<module>` (no
|
|
507
|
+
* `worktrees/` segment) and is distinct code, so index it as before.
|
|
508
|
+
*
|
|
509
|
+
* Returns `'none'` when there is no `.git` entry here.
|
|
510
|
+
*/
|
|
511
|
+
function classifyGitDir(absDir) {
|
|
512
|
+
let st;
|
|
513
|
+
try {
|
|
514
|
+
st = fs.statSync(path.join(absDir, '.git'));
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
return 'none';
|
|
518
|
+
}
|
|
519
|
+
if (st.isDirectory())
|
|
520
|
+
return 'embedded';
|
|
521
|
+
if (!st.isFile())
|
|
522
|
+
return 'none';
|
|
523
|
+
try {
|
|
524
|
+
const gitdir = fs.readFileSync(path.join(absDir, '.git'), 'utf8').match(/^gitdir:\s*(.+)$/m)?.[1]?.trim();
|
|
525
|
+
// A worktree's gitdir lives under some repo's `.git/worktrees/<name>` —
|
|
526
|
+
// either the top-level repo's (`.git/worktrees/`) or, for a worktree of a
|
|
527
|
+
// submodule, that submodule's gitdir (`.git/modules/<module>/worktrees/`).
|
|
528
|
+
// The optional `modules/<module>` segment covers the submodule case (#945).
|
|
529
|
+
// Match both separators so a Windows-style pointer is recognized too.
|
|
530
|
+
if (gitdir && /(^|[\\/])\.git[\\/](modules[\\/][^\\/]+[\\/])?worktrees[\\/]/.test(gitdir))
|
|
531
|
+
return 'worktree';
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// Unreadable `.git` pointer — fall back to the prior "index it" behavior.
|
|
535
|
+
}
|
|
536
|
+
return 'embedded';
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Find git repositories nested under `absDir` (inclusive), shallow bounded BFS.
|
|
540
|
+
* Stops descending at each repo root found — contents belong to that repo's own
|
|
541
|
+
* enumeration. Skips default-ignored dirs (`node_modules` can contain `.git`
|
|
542
|
+
* from npm git-dependencies — that never makes it project code) and CodeGraph
|
|
543
|
+
* data dirs. Depth- and entry-capped so a huge ignored tree can't stall the scan.
|
|
544
|
+
*/
|
|
545
|
+
function findNestedGitRepos(absDir, relPrefix) {
|
|
546
|
+
const found = [];
|
|
547
|
+
const defaults = defaultsOnlyIgnore();
|
|
548
|
+
const queue = [
|
|
549
|
+
{ abs: absDir, rel: relPrefix, depth: 0 },
|
|
550
|
+
];
|
|
551
|
+
let examined = 0;
|
|
552
|
+
while (queue.length > 0) {
|
|
553
|
+
const { abs, rel, depth } = queue.shift();
|
|
554
|
+
if (++examined > EMBEDDED_REPO_SEARCH_ENTRIES) {
|
|
555
|
+
(0, errors_1.logDebug)('Embedded-repo search entry cap hit — deeper repos (if any) not discovered', { under: relPrefix });
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
const cls = classifyGitDir(abs);
|
|
559
|
+
if (cls === 'worktree') {
|
|
560
|
+
continue; // a git worktree duplicates an already-indexed repo (#848) — skip
|
|
561
|
+
}
|
|
562
|
+
if (cls === 'embedded') {
|
|
563
|
+
found.push(rel);
|
|
564
|
+
continue; // its own git handles everything below
|
|
565
|
+
}
|
|
566
|
+
if (depth >= EMBEDDED_REPO_SEARCH_DEPTH)
|
|
567
|
+
continue;
|
|
568
|
+
let entries;
|
|
569
|
+
try {
|
|
570
|
+
entries = fs.readdirSync(abs, { withFileTypes: true });
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
for (const entry of entries) {
|
|
576
|
+
if (!entry.isDirectory())
|
|
577
|
+
continue;
|
|
578
|
+
if (entry.name === '.git' || (0, directory_1.isCodeGraphDataDir)(entry.name))
|
|
579
|
+
continue;
|
|
580
|
+
const childRel = rel + entry.name + '/';
|
|
581
|
+
if (defaults.ignores(childRel))
|
|
582
|
+
continue;
|
|
583
|
+
queue.push({ abs: path.join(abs, entry.name), rel: childRel, depth: depth + 1 });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return found;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Workspace-scope ignore matcher. Ordinary paths get the root's matcher
|
|
590
|
+
* (built-in defaults + root `.gitignore`); paths inside an EMBEDDED repo get
|
|
591
|
+
* that repo's own matcher (defaults + its root `.gitignore`) — the parent's
|
|
592
|
+
* `.gitignore` hides a child repo from git, not from the index (#514). A
|
|
593
|
+
* directory path (trailing slash) that is an ANCESTOR of an embedded root is
|
|
594
|
+
* never ignored, so directory-pruning callers (the Linux per-directory
|
|
595
|
+
* watcher) still descend to reach the embedded repos.
|
|
596
|
+
*
|
|
597
|
+
* Single source of truth for indexer and watcher scope — they must not diverge.
|
|
598
|
+
*/
|
|
599
|
+
class ScopeIgnore {
|
|
600
|
+
rootMatcher;
|
|
601
|
+
exclude;
|
|
602
|
+
include;
|
|
603
|
+
includeRoots;
|
|
604
|
+
embedded;
|
|
605
|
+
defaults = defaultsOnlyIgnore();
|
|
606
|
+
constructor(rootMatcher, embedded,
|
|
607
|
+
/**
|
|
608
|
+
* Project `codegraph.json` `exclude` patterns (#999), matched against the
|
|
609
|
+
* full root-relative path. Wins over everything else — an explicit user
|
|
610
|
+
* exclude applies even to tracked files and even inside embedded repos.
|
|
611
|
+
*/
|
|
612
|
+
exclude = null,
|
|
613
|
+
/**
|
|
614
|
+
* Project `codegraph.json` `include` patterns — first-party source forced
|
|
615
|
+
* INTO the index despite `.gitignore`. When a path matches, it is NOT
|
|
616
|
+
* ignored (so the watcher watches it), overriding `.gitignore`/`rootMatcher`
|
|
617
|
+
* — but never `exclude` (checked first) and never a built-in default-ignored
|
|
618
|
+
* dir. `includeRoots` are the static prefixes so a gitignored ANCESTOR
|
|
619
|
+
* directory of an included subtree still isn't pruned by the directory
|
|
620
|
+
* walker/watcher.
|
|
621
|
+
*/
|
|
622
|
+
include = null, includeRoots = []) {
|
|
623
|
+
this.rootMatcher = rootMatcher;
|
|
624
|
+
this.exclude = exclude;
|
|
625
|
+
this.include = include;
|
|
626
|
+
this.includeRoots = includeRoots;
|
|
627
|
+
// Longest root first so paths in nested embedded repos hit the innermost matcher.
|
|
628
|
+
this.embedded = [...embedded].sort((a, b) => b.root.length - a.root.length);
|
|
629
|
+
}
|
|
630
|
+
ignores(rel) {
|
|
631
|
+
// User `exclude` (#999) is checked first and against the full root-relative
|
|
632
|
+
// path: it must drop git-TRACKED paths (which `.gitignore` can't) and apply
|
|
633
|
+
// everywhere, including ancestors of embedded repos.
|
|
634
|
+
if (this.exclude && this.exclude.ignores(rel))
|
|
635
|
+
return true;
|
|
636
|
+
// User `include`: force first-party source in despite `.gitignore`. Never
|
|
637
|
+
// resurfaces a built-in default-ignored dir (node_modules/dist/…), so an
|
|
638
|
+
// include pattern can't accidentally pull in dependency/build trees.
|
|
639
|
+
if (this.include && !this.defaults.ignores(rel)) {
|
|
640
|
+
if (rel.endsWith('/')) {
|
|
641
|
+
// A directory on (or leading to) an included subtree must stay walkable
|
|
642
|
+
// so the watcher/walker descends to reach the forced-in files.
|
|
643
|
+
if (this.includeRoots.some((r) => r.startsWith(rel) || rel.startsWith(r)))
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
else if (this.include.ignores(rel)) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (const { root, matcher } of this.embedded) {
|
|
651
|
+
if (rel.startsWith(root)) {
|
|
652
|
+
const inner = rel.slice(root.length);
|
|
653
|
+
if (inner === '')
|
|
654
|
+
return false;
|
|
655
|
+
// Built-in defaults apply to the FULL path uniformly (#407) — an
|
|
656
|
+
// embedded repo inside node_modules (an npm git-dependency) must stay
|
|
657
|
+
// excluded even though its own rules wouldn't ignore its files.
|
|
658
|
+
return this.defaults.ignores(rel) || matcher.ignores(inner);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Never prune a directory that leads to an embedded repo.
|
|
662
|
+
if (rel.endsWith('/') && this.embedded.some(({ root }) => root.startsWith(rel))) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
return this.rootMatcher.ignores(rel);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
exports.ScopeIgnore = ScopeIgnore;
|
|
669
|
+
/**
|
|
670
|
+
* Build the workspace-scope matcher. When the caller already knows the
|
|
671
|
+
* embedded roots (the scanner discovers them during collection), pass them to
|
|
672
|
+
* skip rediscovery; otherwise they're discovered here (the watcher path).
|
|
673
|
+
*/
|
|
674
|
+
function buildScopeIgnore(rootDir, embeddedRoots) {
|
|
675
|
+
const roots = embeddedRoots ? [...embeddedRoots] : discoverEmbeddedRepoRoots(rootDir);
|
|
676
|
+
const include = loadIncludeMatcher(rootDir);
|
|
677
|
+
return new ScopeIgnore(buildDefaultIgnore(rootDir), roots.map((root) => ({ root, matcher: buildDefaultIgnore(path.join(rootDir, root)) })), loadExcludeMatcher(rootDir), include, include ? includeStaticRoots((0, project_config_1.loadIncludePatterns)(rootDir)) : []);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Whether an embedded repo found as a tracked gitlink (mode 160000, #1031/#1033)
|
|
681
|
+
* must be SKIPPED rather than indexed. A gitlink is tracked, so `.gitignore`
|
|
682
|
+
* can't untrack it — but the discovery passes for it must still honor the same
|
|
683
|
+
* scope rules as every other path, or a gitignored reference/data dir full of
|
|
684
|
+
* `git add`ed clones gets pulled into the index against the user's stated intent
|
|
685
|
+
* (#1065). Two reasons to skip:
|
|
686
|
+
* 1. It sits in a built-in default-ignored location — an npm git-dependency
|
|
687
|
+
* under `node_modules` is never project code; not even an explicit opt-in
|
|
688
|
+
* revives it (matches `findIgnoredEmbeddedRepos`).
|
|
689
|
+
* 2. The parent repo's own `.gitignore` covers its path and the project did
|
|
690
|
+
* NOT opt that path in via `codegraph.json` `includeIgnored`. The gitignore
|
|
691
|
+
* rule is the user's stated intent to keep that path out of scope, exactly
|
|
692
|
+
* as for an UNtracked embedded repo — respect it by default, opt back in
|
|
693
|
+
* with `includeIgnored` (#514, #970, #976).
|
|
694
|
+
* `relDir` is repoDir-relative (trailing-slashed); `prefix` is repoDir's
|
|
695
|
+
* scan-root-relative path so the `includeIgnored` pattern is matched on the full
|
|
696
|
+
* scan-root-relative path. `defaults` is `defaultsOnlyIgnore()` and `repoIgnore`
|
|
697
|
+
* is `buildDefaultIgnore(repoDir)` (defaults + the repo's own `.gitignore`),
|
|
698
|
+
* both passed in so they're built once per repo level rather than per gitlink.
|
|
699
|
+
*/
|
|
700
|
+
function gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored) {
|
|
701
|
+
if (defaults.ignores(relDir))
|
|
702
|
+
return true; // default-ignored — never index, opt-in can't revive
|
|
703
|
+
if (!repoIgnore.ignores(relDir))
|
|
704
|
+
return false; // not ignored at all — index as before (#1031/#1033)
|
|
705
|
+
// Gitignored by the repo's own rules — skip unless the project opted it in.
|
|
706
|
+
return !includeIgnored?.ignores((0, utils_1.normalizePath)(prefix + relDir));
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Standalone discovery of every embedded repo root under `rootDir` (relative,
|
|
710
|
+
* trailing-slashed) — the untracked kind (#193) always, and the gitignored kind
|
|
711
|
+
* (#514) only for directories the project opted in via `codegraph.json`
|
|
712
|
+
* `includeIgnored` (#622, #699); otherwise `.gitignore` is respected and they
|
|
713
|
+
* are not discovered (#970, #976). Recursive (an embedded repo can embed further
|
|
714
|
+
* repos). Returns [] for non-git roots: the filesystem walk handles nested repos
|
|
715
|
+
* there already.
|
|
716
|
+
*/
|
|
717
|
+
function discoverEmbeddedRepoRoots(rootDir) {
|
|
718
|
+
try {
|
|
719
|
+
(0, child_process_1.execFileSync)('git', ['rev-parse', '--git-dir'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
const out = [];
|
|
725
|
+
const defaults = defaultsOnlyIgnore();
|
|
726
|
+
const includeIgnored = loadIncludeIgnoredMatcher(rootDir);
|
|
727
|
+
const visit = (repoAbs, prefix) => {
|
|
728
|
+
const candidates = [];
|
|
729
|
+
try {
|
|
730
|
+
const o = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-o', '--exclude-standard', '--directory'], { cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
731
|
+
for (const e of o.split('\0')) {
|
|
732
|
+
if (e.endsWith('/') && !isWholeCwdEntry(e) && !defaults.ignores(e)) {
|
|
733
|
+
candidates.push(...findNestedGitRepos(path.join(repoAbs, e), e));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
catch { /* untracked listing failed — ignored-side discovery still runs */ }
|
|
738
|
+
// Unexpanded gitlinks (mode 160000) with a real checkout on disk — embedded
|
|
739
|
+
// repos `git add`ed without `.gitmodules`, or submodules not active here. The
|
|
740
|
+
// untracked listing above can't see them (they're tracked), so find them the
|
|
741
|
+
// same way collectGitFiles does, keeping watcher scope == indexer scope.
|
|
742
|
+
// (#1031, #1033)
|
|
743
|
+
try {
|
|
744
|
+
const staged = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], { cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
745
|
+
const repoIgnore = buildDefaultIgnore(repoAbs);
|
|
746
|
+
for (const entry of staged.split('\0')) {
|
|
747
|
+
if (!entry || entry.slice(0, 6) !== '160000')
|
|
748
|
+
continue;
|
|
749
|
+
const tab = entry.indexOf('\t');
|
|
750
|
+
if (tab === -1)
|
|
751
|
+
continue;
|
|
752
|
+
const rel = entry.slice(tab + 1);
|
|
753
|
+
const relDir = rel.endsWith('/') ? rel : rel + '/';
|
|
754
|
+
// A gitlink under a gitignored path is respected (not indexed) unless the
|
|
755
|
+
// project opted it in — same rule as the untracked-ignored kind (#1065).
|
|
756
|
+
if (gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored))
|
|
757
|
+
continue;
|
|
758
|
+
if (classifyGitDir(path.join(repoAbs, rel)) === 'embedded')
|
|
759
|
+
candidates.push(relDir);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
catch { /* staged listing failed — other discovery still runs */ }
|
|
763
|
+
candidates.push(...findIgnoredEmbeddedRepos(repoAbs, includeIgnored, prefix));
|
|
764
|
+
for (const rel of candidates) {
|
|
765
|
+
const full = (0, utils_1.normalizePath)(prefix + rel);
|
|
766
|
+
out.push(full);
|
|
767
|
+
visit(path.join(repoAbs, rel), full);
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
visit(rootDir, '');
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Cap on how many skipped gitignored repos the CLI hint enumerates — a huge
|
|
775
|
+
* gitignored data dir full of clones must never turn the hint scan into a long
|
|
776
|
+
* walk. Enough to make the point; the caller says "+N more" past this.
|
|
777
|
+
*/
|
|
778
|
+
const UNINDEXED_IGNORED_REPO_HINT_CAP = 100;
|
|
779
|
+
/**
|
|
780
|
+
* The INVERSE of the gitignored side of {@link discoverEmbeddedRepoRoots}:
|
|
781
|
+
* nested git repositories under a gitignored directory that the project has NOT
|
|
782
|
+
* opted into via `codegraph.json` `includeIgnored`. These are real repos the
|
|
783
|
+
* default `init`/`index` deliberately skips because `.gitignore` excludes them
|
|
784
|
+
* (#970, #976) — most visibly the "super-repo `.gitignore`s its child repos"
|
|
785
|
+
* layout (#1156), where `init` at the parent correctly indexes ~nothing while
|
|
786
|
+
* `init` inside each child works. The CLI uses this to turn that silent empty
|
|
787
|
+
* index into an actionable hint: it names the skipped repos and offers to opt
|
|
788
|
+
* them in. Paths are `rootDir`-relative and trailing-slashed (valid
|
|
789
|
+
* `includeIgnored` patterns as-is). Returns `[]` for a non-git root (a
|
|
790
|
+
* filesystem walk already descends into nested repos there), skips built-in
|
|
791
|
+
* default-ignored dirs (`node_modules`, …), and is bounded so it never stalls
|
|
792
|
+
* on a giant ignored tree.
|
|
793
|
+
*/
|
|
794
|
+
function findUnindexedIgnoredRepos(rootDir) {
|
|
795
|
+
try {
|
|
796
|
+
(0, child_process_1.execFileSync)('git', ['rev-parse', '--git-dir'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
const defaults = defaultsOnlyIgnore();
|
|
802
|
+
const includeIgnored = loadIncludeIgnoredMatcher(rootDir);
|
|
803
|
+
const repos = [];
|
|
804
|
+
for (const dir of listIgnoredDirs(rootDir)) {
|
|
805
|
+
if (defaults.ignores(dir))
|
|
806
|
+
continue; // node_modules etc. — never project code
|
|
807
|
+
if (includeIgnored?.ignores((0, utils_1.normalizePath)(dir)))
|
|
808
|
+
continue; // already opted in — nothing to nag about
|
|
809
|
+
for (const repo of findNestedGitRepos(path.join(rootDir, dir), dir)) {
|
|
810
|
+
repos.push(repo);
|
|
811
|
+
if (repos.length >= UNINDEXED_IGNORED_REPO_HINT_CAP)
|
|
812
|
+
return repos;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return repos;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Discover embedded repos hidden by `repoDir`'s OWN gitignore rules: for each
|
|
819
|
+
* gitignored directory, search for nested `.git` roots. Returns repo paths
|
|
820
|
+
* relative to `repoDir`, trailing-slashed.
|
|
821
|
+
*
|
|
822
|
+
* OPT-IN ONLY. Walking into a gitignored directory contradicts what every other
|
|
823
|
+
* tool (and CodeGraph's own `git ls-files` foundation) does — `.gitignore`
|
|
824
|
+
* excludes. So this returns `[]` unless the project opted the directory in via
|
|
825
|
+
* `codegraph.json` `includeIgnored`; without that, a gitignored dir — including
|
|
826
|
+
* a huge reference/data dir full of nested clones — is left untouched (#970,
|
|
827
|
+
* #976). When opted in, it restores the super-repo-of-clones behavior (#622,
|
|
828
|
+
* #699). `prefix` is the scan-root-relative path of `repoDir`, so a pattern like
|
|
829
|
+
* `services/` opts that whole subtree in at any recursion depth. Built-in
|
|
830
|
+
* default excludes (`node_modules`, …) are always skipped.
|
|
831
|
+
*/
|
|
832
|
+
function findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix) {
|
|
833
|
+
if (!includeIgnored)
|
|
834
|
+
return [];
|
|
835
|
+
const defaults = defaultsOnlyIgnore();
|
|
836
|
+
const repos = [];
|
|
837
|
+
for (const dir of listIgnoredDirs(repoDir)) {
|
|
838
|
+
if (defaults.ignores(dir))
|
|
839
|
+
continue;
|
|
840
|
+
if (!includeIgnored.ignores((0, utils_1.normalizePath)(prefix + dir)))
|
|
841
|
+
continue;
|
|
842
|
+
repos.push(...findNestedGitRepos(path.join(repoDir, dir), dir));
|
|
843
|
+
}
|
|
844
|
+
return repos;
|
|
845
|
+
}
|
|
91
846
|
/**
|
|
92
847
|
* Collect git-visible files (tracked + untracked, .gitignore-respected) from the
|
|
93
848
|
* git repository rooted at `repoDir`, adding each to `files` with `prefix`
|
|
@@ -99,41 +854,111 @@ const MAX_FILE_SIZE = 1024 * 1024;
|
|
|
99
854
|
* skips them entirely, and untracked output reports them only as an opaque
|
|
100
855
|
* "subdir/" entry (trailing slash) rather than expanding their files. Each
|
|
101
856
|
* embedded repo is its own git boundary, so we re-run `git ls-files` inside it.
|
|
102
|
-
* (See issue #193.)
|
|
857
|
+
* (See issue #193.) GITIGNORED embedded repos are invisible even to that; they
|
|
858
|
+
* are discovered separately via `findIgnoredEmbeddedRepos` (#514) but ONLY for
|
|
859
|
+
* directories the project opted in through `codegraph.json` `includeIgnored`
|
|
860
|
+
* (`includeIgnored` here, threaded from the scan root) — by default `.gitignore`
|
|
861
|
+
* is respected and they stay out (#970, #976). Every embedded repo root (however
|
|
862
|
+
* found) is recorded in `embeddedRoots` so callers can exempt its files from the
|
|
863
|
+
* parent's own gitignore rules.
|
|
103
864
|
*/
|
|
104
|
-
function collectGitFiles(repoDir, prefix, files) {
|
|
105
|
-
const gitOpts = { cwd: repoDir, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] };
|
|
865
|
+
function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored = null) {
|
|
866
|
+
const gitOpts = { cwd: repoDir, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
|
|
106
867
|
// Tracked files. --recurse-submodules pulls in files from active submodules,
|
|
107
868
|
// which the index would otherwise represent only as a commit pointer.
|
|
108
869
|
// Without this, monorepos using submodules index 0 files. (See issue #147.)
|
|
109
870
|
// Note: --recurse-submodules only supports -c/--cached and --stage modes — it
|
|
110
871
|
// can't be combined with -o, so untracked files are gathered separately below.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
872
|
+
//
|
|
873
|
+
// We use --stage (-s) rather than -c so each entry carries its file mode. That
|
|
874
|
+
// lets us spot gitlink entries (mode 160000) that --recurse-submodules did NOT
|
|
875
|
+
// expand: a nested repo `git add`ed without a `.gitmodules` entry, or a
|
|
876
|
+
// submodule that isn't active/initialized in this checkout. Such a gitlink
|
|
877
|
+
// falls through every pass — it's tracked, so the untracked `-o` listing below
|
|
878
|
+
// never reports it, and --recurse-submodules only expands ACTIVE submodules —
|
|
879
|
+
// so its source would be silently skipped, leaving only the super-repo's own
|
|
880
|
+
// files indexed. We collect those gitlinks here and recurse into them below.
|
|
881
|
+
// (An active submodule is expanded inline by --recurse-submodules and so never
|
|
882
|
+
// surfaces as a 160000 entry — only the unhandled gitlinks do.) (#1031, #1033)
|
|
883
|
+
//
|
|
884
|
+
// -z gives NUL-separated, unquoted output so non-ASCII (e.g. CJK) paths
|
|
885
|
+
// survive verbatim. Without it git octal-escapes and double-quotes such paths
|
|
886
|
+
// (the core.quotepath default), and the quoted form never matches a real file
|
|
887
|
+
// on disk → those files are silently dropped from the index. (#541) With -s the
|
|
888
|
+
// path follows a TAB after the `<mode> <object> <stage>` prefix.
|
|
889
|
+
const gitlinkRels = [];
|
|
890
|
+
const tracked = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], gitOpts);
|
|
891
|
+
for (const entry of tracked.split('\0')) {
|
|
892
|
+
if (!entry)
|
|
893
|
+
continue;
|
|
894
|
+
const tab = entry.indexOf('\t');
|
|
895
|
+
if (tab === -1)
|
|
896
|
+
continue; // --stage always emits "<mode> <object> <stage>\t<path>"
|
|
897
|
+
const rel = entry.slice(tab + 1);
|
|
898
|
+
if (entry.slice(0, 6) === '160000') {
|
|
899
|
+
gitlinkRels.push(rel); // an unexpanded gitlink — recursed into below, not a source file itself
|
|
900
|
+
continue;
|
|
116
901
|
}
|
|
902
|
+
files.add((0, utils_1.normalizePath)(prefix + rel));
|
|
117
903
|
}
|
|
118
904
|
// Untracked files (submodules manage their own untracked state). Embedded git
|
|
119
905
|
// repos surface here as a single "subdir/" entry that git refuses to descend
|
|
120
906
|
// into — recurse into those as their own repos so their source gets indexed.
|
|
121
|
-
const untracked = (0, child_process_1.execFileSync)('git', ['ls-files', '-o', '--exclude-standard'], gitOpts);
|
|
122
|
-
for (const
|
|
123
|
-
|
|
124
|
-
if (!trimmed)
|
|
907
|
+
const untracked = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-o', '--exclude-standard'], gitOpts);
|
|
908
|
+
for (const rel of untracked.split('\0')) {
|
|
909
|
+
if (!rel)
|
|
125
910
|
continue;
|
|
126
|
-
if (
|
|
911
|
+
if (rel.endsWith('/')) {
|
|
127
912
|
// git only emits a trailing-slash directory entry for an embedded repo.
|
|
128
913
|
// Guard with a .git check anyway, and skip anything else exactly as git
|
|
129
|
-
// itself skips it (we never descend into a non-repo opaque dir).
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
914
|
+
// itself skips it (we never descend into a non-repo opaque dir). Never
|
|
915
|
+
// descend into default-ignored locations — an embedded repo inside
|
|
916
|
+
// node_modules is an npm git-dependency, not project code.
|
|
917
|
+
const childDir = path.join(repoDir, rel);
|
|
918
|
+
// A git worktree surfaces here as an opaque untracked dir too — skip it,
|
|
919
|
+
// it's a duplicate working view of an already-indexed repo (#848).
|
|
920
|
+
if (classifyGitDir(childDir) === 'embedded' && !defaultsOnlyIgnore().ignores(rel)) {
|
|
921
|
+
embeddedRoots?.add((0, utils_1.normalizePath)(prefix + rel));
|
|
922
|
+
collectGitFiles(childDir, prefix + rel, files, embeddedRoots, includeIgnored);
|
|
133
923
|
}
|
|
134
924
|
continue;
|
|
135
925
|
}
|
|
136
|
-
files.add((0, utils_1.normalizePath)(prefix +
|
|
926
|
+
files.add((0, utils_1.normalizePath)(prefix + rel));
|
|
927
|
+
}
|
|
928
|
+
// Gitlink entries (mode 160000) that --recurse-submodules left unexpanded —
|
|
929
|
+
// an embedded repo `git add`ed without `.gitmodules`, or a submodule not
|
|
930
|
+
// active/initialized in this checkout. When such a gitlink has a real working
|
|
931
|
+
// tree on disk it is distinct first-party code we must index as its own
|
|
932
|
+
// embedded repo: the tracked pass skipped its contents and the untracked pass
|
|
933
|
+
// never sees it (it's tracked, not "other"). A gitlink with no checkout on disk
|
|
934
|
+
// (an uninitialized submodule — empty dir, no `.git`) has nothing to index and
|
|
935
|
+
// is left alone, as is a submodule worktree (a duplicate view, #945). (#1031, #1033)
|
|
936
|
+
if (gitlinkRels.length > 0) {
|
|
937
|
+
const defaults = defaultsOnlyIgnore();
|
|
938
|
+
const repoIgnore = buildDefaultIgnore(repoDir);
|
|
939
|
+
for (const rel of gitlinkRels) {
|
|
940
|
+
const relDir = rel.endsWith('/') ? rel : rel + '/';
|
|
941
|
+
// A gitlink under a gitignored path is respected (not indexed) unless the
|
|
942
|
+
// project opted it in via `includeIgnored` — keep tracked gitlinks under
|
|
943
|
+
// the same scope rule as the untracked-ignored kind below (#1065).
|
|
944
|
+
if (gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored))
|
|
945
|
+
continue;
|
|
946
|
+
const childDir = path.join(repoDir, rel);
|
|
947
|
+
// 'embedded' = a real .git checkout on disk; 'worktree' and 'none' are skipped.
|
|
948
|
+
if (classifyGitDir(childDir) !== 'embedded')
|
|
949
|
+
continue;
|
|
950
|
+
embeddedRoots?.add((0, utils_1.normalizePath)(prefix + relDir));
|
|
951
|
+
collectGitFiles(childDir, prefix + relDir, files, embeddedRoots, includeIgnored);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// Embedded repos hidden by THIS repo's ignore rules (`/packages/` in a
|
|
955
|
+
// super-repo .gitignore) never appear in any listing above. By default they
|
|
956
|
+
// stay hidden — `.gitignore` is respected (#970, #976). They are recursed into
|
|
957
|
+
// only when the project opted the directory in via `codegraph.json`
|
|
958
|
+
// `includeIgnored` (#622, #699), which `findIgnoredEmbeddedRepos` enforces.
|
|
959
|
+
for (const rel of findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix)) {
|
|
960
|
+
embeddedRoots?.add((0, utils_1.normalizePath)(prefix + rel));
|
|
961
|
+
collectGitFiles(path.join(repoDir, rel), prefix + rel, files, embeddedRoots, includeIgnored);
|
|
137
962
|
}
|
|
138
963
|
}
|
|
139
964
|
/**
|
|
@@ -147,11 +972,11 @@ function getGitVisibleFiles(rootDir) {
|
|
|
147
972
|
// Check if the project directory is gitignored by a parent repo.
|
|
148
973
|
// When rootDir lives inside a parent git repo that ignores it,
|
|
149
974
|
// `git ls-files` returns nothing — fall back to filesystem walk.
|
|
150
|
-
const gitRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
975
|
+
const gitRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }).trim();
|
|
151
976
|
if (path.resolve(gitRoot) !== path.resolve(rootDir)) {
|
|
152
977
|
try {
|
|
153
978
|
// git check-ignore exits 0 if the path IS ignored, 1 if not
|
|
154
|
-
(0, child_process_1.execFileSync)('git', ['check-ignore', '-q', path.resolve(rootDir)], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
979
|
+
(0, child_process_1.execFileSync)('git', ['check-ignore', '-q', path.resolve(rootDir)], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
155
980
|
// Directory is gitignored by parent repo — fall back to filesystem walk
|
|
156
981
|
return null;
|
|
157
982
|
}
|
|
@@ -160,8 +985,24 @@ function getGitVisibleFiles(rootDir) {
|
|
|
160
985
|
}
|
|
161
986
|
}
|
|
162
987
|
const files = new Set();
|
|
163
|
-
|
|
164
|
-
|
|
988
|
+
const embeddedRoots = new Set();
|
|
989
|
+
collectGitFiles(rootDir, '', files, embeddedRoots, loadIncludeIgnoredMatcher(rootDir));
|
|
990
|
+
// Apply built-in default ignores uniformly — to tracked files too, since
|
|
991
|
+
// committing a dependency/build dir doesn't make it project code. A
|
|
992
|
+
// `.gitignore` negation (e.g. `!vendor/`) is the explicit opt-in. (issue #407)
|
|
993
|
+
// Files inside an EMBEDDED repo are matched against that repo's own rules,
|
|
994
|
+
// not the parent's: the parent's .gitignore hides the child repo from git,
|
|
995
|
+
// not from the index. (#514)
|
|
996
|
+
const ig = buildScopeIgnore(rootDir, embeddedRoots);
|
|
997
|
+
const visible = new Set([...files].filter((f) => !ig.ignores(f)));
|
|
998
|
+
// Force-include first-party source the project whitelisted in
|
|
999
|
+
// `codegraph.json` `include`. These are gitignored, so `git ls-files` never
|
|
1000
|
+
// listed them above — discover them directly off disk and add them. (The
|
|
1001
|
+
// common SVN+Git dual-VCS case: source committed to SVN, gitignored out of
|
|
1002
|
+
// Git, but still wanted in the graph.)
|
|
1003
|
+
for (const f of collectIncludedFilesForRoot(rootDir))
|
|
1004
|
+
visible.add(f);
|
|
1005
|
+
return visible;
|
|
165
1006
|
}
|
|
166
1007
|
catch {
|
|
167
1008
|
return null;
|
|
@@ -170,38 +1011,96 @@ function getGitVisibleFiles(rootDir) {
|
|
|
170
1011
|
/**
|
|
171
1012
|
* Use `git status` to detect changed files instead of scanning every file.
|
|
172
1013
|
* Returns null on failure so callers fall back to full scan.
|
|
1014
|
+
*
|
|
1015
|
+
* Recurses into embedded repos — the untracked kind (#193: the parent's status
|
|
1016
|
+
* collapses them to an opaque `?? subdir/` entry) always, and the gitignored
|
|
1017
|
+
* kind (#514: they never appear in the parent's status at all) only for
|
|
1018
|
+
* directories opted in via `codegraph.json` `includeIgnored` (#622, #699) —
|
|
1019
|
+
* running `git status` inside each, so changes in a multi-repo workspace sync
|
|
1020
|
+
* without a full rescan. By default a gitignored dir is left alone, matching the
|
|
1021
|
+
* full-index scan (#970, #976). Deleting an ENTIRE embedded repo dir is the one
|
|
1022
|
+
* case this cannot see (the child status that would report the deletions is gone
|
|
1023
|
+
* with it); a full `codegraph index` reconciles that.
|
|
173
1024
|
*/
|
|
174
1025
|
function getGitChangedFiles(rootDir) {
|
|
175
1026
|
try {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
continue; // Minimum: "XY file"
|
|
183
|
-
const statusCode = line.substring(0, 2);
|
|
184
|
-
const filePath = (0, utils_1.normalizePath)(line.substring(3));
|
|
185
|
-
// Skip non-source files (git status already omits .gitignored paths).
|
|
186
|
-
if (!(0, grammars_1.isSourceFile)(filePath))
|
|
187
|
-
continue;
|
|
188
|
-
if (statusCode === '??') {
|
|
189
|
-
added.push(filePath);
|
|
190
|
-
}
|
|
191
|
-
else if (statusCode.includes('D')) {
|
|
192
|
-
deleted.push(filePath);
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
// M, MM, AM, A (staged), etc. — treat as modified
|
|
196
|
-
modified.push(filePath);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return { modified, added, deleted };
|
|
1027
|
+
const changes = { modified: [], added: [], deleted: [] };
|
|
1028
|
+
// Custom extension → language overrides from the project's codegraph.json,
|
|
1029
|
+
// so change detection sees the same custom-extension files the full index does.
|
|
1030
|
+
const overrides = (0, project_config_1.loadExtensionOverrides)(rootDir);
|
|
1031
|
+
collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir));
|
|
1032
|
+
return changes;
|
|
200
1033
|
}
|
|
201
1034
|
catch {
|
|
202
1035
|
return null;
|
|
203
1036
|
}
|
|
204
1037
|
}
|
|
1038
|
+
function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null, exclude = null) {
|
|
1039
|
+
const output = (0, child_process_1.execFileSync)('git', ['status', '--porcelain', '--no-renames'], { cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
1040
|
+
// This repo's own ignore rules — built-in defaults (#407) plus its .gitignore.
|
|
1041
|
+
// Change detection must exclude the SAME files the full index does, but git
|
|
1042
|
+
// status hides neither: it ignores nothing for *tracked* paths, and the
|
|
1043
|
+
// built-in defaults aren't gitignore at all. Without this filter a committed
|
|
1044
|
+
// vendor/ dir, or a tracked file under a .gitignored dir, surfaces here as a
|
|
1045
|
+
// change — so `codegraph status` (which reads getChangedFiles) reports a
|
|
1046
|
+
// pending edit the full index never tracks and `sync` never clears. Matching
|
|
1047
|
+
// repo-relative `rel` at each recursion level mirrors getGitVisibleFiles'
|
|
1048
|
+
// ScopeIgnore: every embedded repo is judged by ITS OWN rules, never the
|
|
1049
|
+
// parent's. (#766)
|
|
1050
|
+
const ig = buildDefaultIgnore(repoDir);
|
|
1051
|
+
const untrackedDirs = [];
|
|
1052
|
+
for (const line of output.split('\n')) {
|
|
1053
|
+
if (line.length < 4)
|
|
1054
|
+
continue; // Minimum: "XY file"
|
|
1055
|
+
const statusCode = line.substring(0, 2);
|
|
1056
|
+
const rel = (0, utils_1.normalizePath)(line.substring(3));
|
|
1057
|
+
// Untracked directory entries (trailing slash) may hide an embedded repo —
|
|
1058
|
+
// collect for the recursion below instead of treating as a file.
|
|
1059
|
+
if (statusCode === '??' && rel.endsWith('/')) {
|
|
1060
|
+
untrackedDirs.push(rel);
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const filePath = (0, utils_1.normalizePath)(prefix + rel);
|
|
1064
|
+
if (!(0, grammars_1.isSourceFile)(filePath, overrides))
|
|
1065
|
+
continue;
|
|
1066
|
+
if (statusCode.includes('D')) {
|
|
1067
|
+
// Deletions stay unfiltered: getChangedFiles acts on one only when the
|
|
1068
|
+
// path is already tracked in the DB, where removal is always correct — and
|
|
1069
|
+
// that lets a newly-excluded dir's stale rows clean themselves up. (#766)
|
|
1070
|
+
out.deleted.push(filePath);
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
// Added (`??`) / modified files inside an excluded dir must not enter the
|
|
1074
|
+
// index — match against the repo-relative path, same as the full scan. (#766)
|
|
1075
|
+
if (ig.ignores(rel))
|
|
1076
|
+
continue;
|
|
1077
|
+
// User `codegraph.json` `exclude` (#999) is project-root-relative, so it's
|
|
1078
|
+
// matched against the full path — sync must not re-add a tracked file the
|
|
1079
|
+
// full index now keeps out. Deletions above stay unfiltered so a file that
|
|
1080
|
+
// WAS indexed before an exclude was added still cleans itself out.
|
|
1081
|
+
if (exclude && exclude.ignores(filePath))
|
|
1082
|
+
continue;
|
|
1083
|
+
if (statusCode === '??') {
|
|
1084
|
+
out.added.push(filePath);
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
// M, MM, AM, A (staged), etc. — treat as modified
|
|
1088
|
+
out.modified.push(filePath);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
// Recurse embedded repos found under untracked dirs (at the dir itself or
|
|
1092
|
+
// nested deeper). Gitignored dirs are walked only for the directories the
|
|
1093
|
+
// project opted in via `includeIgnored`; by default `.gitignore` is respected
|
|
1094
|
+
// and they are left alone (#970, #976), mirroring the full-index scan.
|
|
1095
|
+
for (const rel of untrackedDirs) {
|
|
1096
|
+
for (const repoRel of findNestedGitRepos(path.join(repoDir, rel), rel)) {
|
|
1097
|
+
collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, overrides, includeIgnored, exclude);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
for (const rel of findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix)) {
|
|
1101
|
+
collectGitStatus(path.join(repoDir, rel), prefix + rel, out, overrides, includeIgnored, exclude);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
205
1104
|
/**
|
|
206
1105
|
* Recursively scan a directory for source files.
|
|
207
1106
|
*
|
|
@@ -210,13 +1109,15 @@ function getGitChangedFiles(rootDir) {
|
|
|
210
1109
|
* projects, falls back to a filesystem walk that parses .gitignore itself.
|
|
211
1110
|
*/
|
|
212
1111
|
function scanDirectory(rootDir, onProgress) {
|
|
1112
|
+
// Custom extension → language overrides from the project's codegraph.json.
|
|
1113
|
+
const overrides = (0, project_config_1.loadExtensionOverrides)(rootDir);
|
|
213
1114
|
// Fast path: use git to get all visible files (respects .gitignore everywhere)
|
|
214
1115
|
const gitFiles = getGitVisibleFiles(rootDir);
|
|
215
1116
|
if (gitFiles) {
|
|
216
1117
|
const files = [];
|
|
217
1118
|
let count = 0;
|
|
218
1119
|
for (const filePath of gitFiles) {
|
|
219
|
-
if ((0, grammars_1.isSourceFile)(filePath)) {
|
|
1120
|
+
if ((0, grammars_1.isSourceFile)(filePath, overrides)) {
|
|
220
1121
|
files.push(filePath);
|
|
221
1122
|
count++;
|
|
222
1123
|
onProgress?.(count, filePath);
|
|
@@ -232,12 +1133,14 @@ function scanDirectory(rootDir, onProgress) {
|
|
|
232
1133
|
* allowing worker threads to receive and render progress messages.
|
|
233
1134
|
*/
|
|
234
1135
|
async function scanDirectoryAsync(rootDir, onProgress) {
|
|
1136
|
+
// Custom extension → language overrides from the project's codegraph.json.
|
|
1137
|
+
const overrides = (0, project_config_1.loadExtensionOverrides)(rootDir);
|
|
235
1138
|
const gitFiles = getGitVisibleFiles(rootDir);
|
|
236
1139
|
if (gitFiles) {
|
|
237
1140
|
const files = [];
|
|
238
1141
|
let count = 0;
|
|
239
1142
|
for (const filePath of gitFiles) {
|
|
240
|
-
if ((0, grammars_1.isSourceFile)(filePath)) {
|
|
1143
|
+
if ((0, grammars_1.isSourceFile)(filePath, overrides)) {
|
|
241
1144
|
files.push(filePath);
|
|
242
1145
|
count++;
|
|
243
1146
|
onProgress?.(count, filePath);
|
|
@@ -258,17 +1161,17 @@ function scanDirectoryWalk(rootDir, onProgress) {
|
|
|
258
1161
|
const files = [];
|
|
259
1162
|
let count = 0;
|
|
260
1163
|
const visitedDirs = new Set();
|
|
1164
|
+
// Custom extension → language overrides from the project's codegraph.json.
|
|
1165
|
+
const overrides = (0, project_config_1.loadExtensionOverrides)(rootDir);
|
|
261
1166
|
const loadIgnore = (dir) => {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
return null;
|
|
1167
|
+
const giPath = path.join(dir, '.gitignore');
|
|
1168
|
+
if (!fs.existsSync(giPath))
|
|
1169
|
+
return null;
|
|
1170
|
+
// readGitignorePatterns is defensive: a non-UTF-8 (DLP-encrypted) or
|
|
1171
|
+
// uncompilable .gitignore is skipped/filtered with a warning, never thrown
|
|
1172
|
+
// (issue #682) — so the per-file `.ignores()` calls below can't crash.
|
|
1173
|
+
const patterns = readGitignorePatterns(giPath);
|
|
1174
|
+
return patterns ? { dir, ig: (0, ignore_1.default)().add(patterns) } : null;
|
|
272
1175
|
};
|
|
273
1176
|
const isIgnored = (fullPath, isDir, matchers) => {
|
|
274
1177
|
for (const { dir, ig } of matchers) {
|
|
@@ -297,7 +1200,9 @@ function scanDirectoryWalk(rootDir, onProgress) {
|
|
|
297
1200
|
}
|
|
298
1201
|
visitedDirs.add(realDir);
|
|
299
1202
|
// This directory's own .gitignore (if present) applies to everything below it.
|
|
300
|
-
|
|
1203
|
+
// The root's .gitignore is already merged into the seeded base matcher (so a
|
|
1204
|
+
// negation there can override a built-in default), so skip it here.
|
|
1205
|
+
const own = dir === rootDir ? null : loadIgnore(dir);
|
|
301
1206
|
const active = own ? [...matchers, own] : matchers;
|
|
302
1207
|
let entries;
|
|
303
1208
|
try {
|
|
@@ -308,8 +1213,9 @@ function scanDirectoryWalk(rootDir, onProgress) {
|
|
|
308
1213
|
return;
|
|
309
1214
|
}
|
|
310
1215
|
for (const entry of entries) {
|
|
311
|
-
// Never descend into git internals or
|
|
312
|
-
|
|
1216
|
+
// Never descend into git internals or any CodeGraph data directory
|
|
1217
|
+
// (the active one or a sibling another environment created — #636).
|
|
1218
|
+
if (entry.name === '.git' || (0, directory_1.isCodeGraphDataDir)(entry.name))
|
|
313
1219
|
continue;
|
|
314
1220
|
const fullPath = path.join(dir, entry.name);
|
|
315
1221
|
const relativePath = (0, utils_1.normalizePath)(path.relative(rootDir, fullPath));
|
|
@@ -323,7 +1229,7 @@ function scanDirectoryWalk(rootDir, onProgress) {
|
|
|
323
1229
|
}
|
|
324
1230
|
}
|
|
325
1231
|
else if (stat.isFile()) {
|
|
326
|
-
if (!isIgnored(fullPath, false, active) && (0, grammars_1.isSourceFile)(relativePath)) {
|
|
1232
|
+
if (!isIgnored(fullPath, false, active) && (0, grammars_1.isSourceFile)(relativePath, overrides)) {
|
|
327
1233
|
files.push(relativePath);
|
|
328
1234
|
count++;
|
|
329
1235
|
onProgress?.(count, relativePath);
|
|
@@ -341,7 +1247,7 @@ function scanDirectoryWalk(rootDir, onProgress) {
|
|
|
341
1247
|
}
|
|
342
1248
|
}
|
|
343
1249
|
else if (entry.isFile()) {
|
|
344
|
-
if (!isIgnored(fullPath, false, active) && (0, grammars_1.isSourceFile)(relativePath)) {
|
|
1250
|
+
if (!isIgnored(fullPath, false, active) && (0, grammars_1.isSourceFile)(relativePath, overrides)) {
|
|
345
1251
|
files.push(relativePath);
|
|
346
1252
|
count++;
|
|
347
1253
|
onProgress?.(count, relativePath);
|
|
@@ -349,9 +1255,61 @@ function scanDirectoryWalk(rootDir, onProgress) {
|
|
|
349
1255
|
}
|
|
350
1256
|
}
|
|
351
1257
|
}
|
|
352
|
-
|
|
1258
|
+
// Seed a base matcher with the built-in default ignores (merged with the root
|
|
1259
|
+
// .gitignore so a negation can override). Nested .gitignores still layer per-dir.
|
|
1260
|
+
const baseMatchers = [{ dir: rootDir, ig: buildDefaultIgnore(rootDir) }];
|
|
1261
|
+
// Project `codegraph.json` `exclude` patterns (#999), rooted at the project so
|
|
1262
|
+
// `isIgnored` matches them against root-relative paths — same coverage the
|
|
1263
|
+
// git path gets via ScopeIgnore, for non-git projects.
|
|
1264
|
+
const exclude = loadExcludeMatcher(rootDir);
|
|
1265
|
+
if (exclude)
|
|
1266
|
+
baseMatchers.push({ dir: rootDir, ig: exclude });
|
|
1267
|
+
walk(rootDir, baseMatchers);
|
|
1268
|
+
// Force-include first-party source whitelisted in `codegraph.json` `include`
|
|
1269
|
+
// — the walk above honours `.gitignore`, so anything gitignored was dropped;
|
|
1270
|
+
// add it back here (deduped). Mirrors the git path's union.
|
|
1271
|
+
const included = collectIncludedFilesForRoot(rootDir);
|
|
1272
|
+
if (included.size > 0) {
|
|
1273
|
+
const seen = new Set(files);
|
|
1274
|
+
for (const f of included) {
|
|
1275
|
+
if (!seen.has(f)) {
|
|
1276
|
+
files.push(f);
|
|
1277
|
+
seen.add(f);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
353
1281
|
return files;
|
|
354
1282
|
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Resurrect a resolution edge that is about to be dropped (its target symbol
|
|
1285
|
+
* was removed, renamed, or its whole file deleted) as the ORIGINAL unresolved
|
|
1286
|
+
* reference that created it, read from the refName/refKind stamp
|
|
1287
|
+
* `createEdges` writes into edge metadata. Inserted as status='pending', the
|
|
1288
|
+
* ref is consumed by the same sync's resolution sweep: it rebinds to an
|
|
1289
|
+
* alternative definition if one exists, or parks as status='failed' where the
|
|
1290
|
+
* #1240 retry finds it if the symbol later reappears.
|
|
1291
|
+
*
|
|
1292
|
+
* Returns null — drop silently, the pre-#1240 behavior — for edges without a
|
|
1293
|
+
* refName stamp (created before the stamp existed, or synthesized): rebuilding
|
|
1294
|
+
* a ref from the target's plain node name would strip the receiver/qualifier
|
|
1295
|
+
* context the original text carried (`h.greet` → `greet`) and could rebind
|
|
1296
|
+
* somewhere a full re-index never would. Silent beats wrong.
|
|
1297
|
+
*/
|
|
1298
|
+
function resurrectRefFromDroppedEdge(e) {
|
|
1299
|
+
const refName = e.metadata?.refName;
|
|
1300
|
+
if (typeof refName !== 'string' || refName.length === 0)
|
|
1301
|
+
return null;
|
|
1302
|
+
const refKind = typeof e.metadata?.refKind === 'string' ? e.metadata.refKind : e.kind;
|
|
1303
|
+
return {
|
|
1304
|
+
fromNodeId: e.source,
|
|
1305
|
+
referenceName: refName,
|
|
1306
|
+
referenceKind: refKind,
|
|
1307
|
+
line: e.line ?? 0,
|
|
1308
|
+
column: e.column ?? 0,
|
|
1309
|
+
filePath: e.sourceFilePath,
|
|
1310
|
+
language: e.sourceLanguage,
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
355
1313
|
/**
|
|
356
1314
|
* Extraction orchestrator
|
|
357
1315
|
*/
|
|
@@ -408,6 +1366,24 @@ class ExtractionOrchestrator {
|
|
|
408
1366
|
return null;
|
|
409
1367
|
}
|
|
410
1368
|
},
|
|
1369
|
+
// Monorepo support — needed by framework detect()s that probe
|
|
1370
|
+
// subpackage manifests (e.g. fabric-view looking at
|
|
1371
|
+
// packages/<sub>/package.json when the root manifest is just a
|
|
1372
|
+
// workspace declaration). Matches the resolver-context shape.
|
|
1373
|
+
listDirectories: (relativePath) => {
|
|
1374
|
+
const target = relativePath === '.' || relativePath === ''
|
|
1375
|
+
? rootDir
|
|
1376
|
+
: path.join(rootDir, relativePath);
|
|
1377
|
+
try {
|
|
1378
|
+
return fs
|
|
1379
|
+
.readdirSync(target, { withFileTypes: true })
|
|
1380
|
+
.filter((entry) => entry.isDirectory())
|
|
1381
|
+
.map((entry) => entry.name);
|
|
1382
|
+
}
|
|
1383
|
+
catch {
|
|
1384
|
+
return [];
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
411
1387
|
};
|
|
412
1388
|
}
|
|
413
1389
|
/**
|
|
@@ -426,7 +1402,12 @@ class ExtractionOrchestrator {
|
|
|
426
1402
|
/**
|
|
427
1403
|
* Index all files in the project
|
|
428
1404
|
*/
|
|
429
|
-
async indexAll(onProgress, signal, verbose
|
|
1405
|
+
async indexAll(onProgress, signal, verbose,
|
|
1406
|
+
// Writer-side backstop for deferred WAL checkpointing (#1231): returns
|
|
1407
|
+
// null in the normal case, or a promise to await (at this safe,
|
|
1408
|
+
// between-transactions boundary) when the WAL has outrun the off-thread
|
|
1409
|
+
// checkpointer past its hard cap. See db/wal-valve.ts.
|
|
1410
|
+
walBackpressure) {
|
|
430
1411
|
await (0, grammars_1.initGrammars)();
|
|
431
1412
|
const startTime = Date.now();
|
|
432
1413
|
const errors = [];
|
|
@@ -435,6 +1416,10 @@ class ExtractionOrchestrator {
|
|
|
435
1416
|
let filesErrored = 0;
|
|
436
1417
|
let totalNodes = 0;
|
|
437
1418
|
let totalEdges = 0;
|
|
1419
|
+
// Custom extension → language overrides from the project's codegraph.json.
|
|
1420
|
+
// Threaded into language detection so custom-extension files load the right
|
|
1421
|
+
// grammar and store under the mapped language.
|
|
1422
|
+
const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
|
|
438
1423
|
const log = verbose
|
|
439
1424
|
? (msg) => { console.log(`[worker] ${msg}`); }
|
|
440
1425
|
: (_msg) => { };
|
|
@@ -444,6 +1429,10 @@ class ExtractionOrchestrator {
|
|
|
444
1429
|
current: 0,
|
|
445
1430
|
total: 0,
|
|
446
1431
|
});
|
|
1432
|
+
// Phase attribution to stderr (same opt-in as the synthesis timings):
|
|
1433
|
+
// early-run 5-10s single stalls were observed on 95k-file repos but never
|
|
1434
|
+
// attributed — these labels settle scan vs framework-detect vs grammars.
|
|
1435
|
+
const tScan = Date.now();
|
|
447
1436
|
const files = await scanDirectoryAsync(this.rootDir, (current, file) => {
|
|
448
1437
|
onProgress?.({
|
|
449
1438
|
phase: 'scanning',
|
|
@@ -452,13 +1441,18 @@ class ExtractionOrchestrator {
|
|
|
452
1441
|
currentFile: file,
|
|
453
1442
|
});
|
|
454
1443
|
});
|
|
1444
|
+
if (process.env.CODEGRAPH_SYNTH_TIMINGS)
|
|
1445
|
+
console.error(`[phase-timing] scan: ${Date.now() - tScan}ms (${files.length} files)`);
|
|
455
1446
|
// Detect frameworks once per indexAll run using the scanned file list.
|
|
456
1447
|
// Names are passed to each parse call so framework-specific extractors
|
|
457
1448
|
// (route nodes, middleware, etc.) run after the tree-sitter pass.
|
|
458
1449
|
// Framework detection is reset each run so adding e.g. requirements.txt
|
|
459
1450
|
// between runs is picked up without restarting the process.
|
|
460
1451
|
this.detectedFrameworkNames = null;
|
|
1452
|
+
const tFw = Date.now();
|
|
461
1453
|
const frameworkNames = this.ensureDetectedFrameworks(files);
|
|
1454
|
+
if (process.env.CODEGRAPH_SYNTH_TIMINGS)
|
|
1455
|
+
console.error(`[phase-timing] framework-detect: ${Date.now() - tFw}ms`);
|
|
462
1456
|
if (signal?.aborted) {
|
|
463
1457
|
return {
|
|
464
1458
|
success: false,
|
|
@@ -484,155 +1478,204 @@ class ExtractionOrchestrator {
|
|
|
484
1478
|
});
|
|
485
1479
|
await new Promise(resolve => setImmediate(resolve));
|
|
486
1480
|
// Detect needed languages and load grammars in the parse worker
|
|
487
|
-
const neededLanguages = [...new Set(files.map((f) => (0, grammars_1.detectLanguage)(f)))];
|
|
1481
|
+
const neededLanguages = [...new Set(files.map((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides)))];
|
|
488
1482
|
// .h files default to 'c' but may be C++ — ensure cpp grammar is loaded when c is needed
|
|
489
1483
|
if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
|
|
490
1484
|
neededLanguages.push('cpp');
|
|
491
1485
|
}
|
|
492
|
-
//
|
|
493
|
-
// Falls back to in-process parsing
|
|
1486
|
+
// Parse files on a pool of worker threads (keeps the main thread free for UI
|
|
1487
|
+
// and uses every core). Falls back to in-process parsing when the compiled
|
|
1488
|
+
// worker is unavailable (e.g. running from source in tests).
|
|
494
1489
|
const parseWorkerPath = path.join(__dirname, 'parse-worker.js');
|
|
495
1490
|
const useWorker = fs.existsSync(parseWorkerPath);
|
|
496
|
-
let
|
|
1491
|
+
let pool = null;
|
|
497
1492
|
if (useWorker) {
|
|
498
|
-
|
|
499
|
-
|
|
1493
|
+
// CODEGRAPH_PARSE_WORKERS: explicit worker count; 1 = the old single-worker
|
|
1494
|
+
// behaviour (the conservative rollback). Unset → clamp(cores-1, 1, 8).
|
|
1495
|
+
const poolSize = (0, parse_pool_1.resolveParsePoolSize)(process.env.CODEGRAPH_PARSE_WORKERS, os.cpus().length);
|
|
1496
|
+
// Read each needed grammar's WASM ONCE here and hand the bytes to every
|
|
1497
|
+
// worker, so spawns/respawns load grammars from memory instead of
|
|
1498
|
+
// re-reading them from disk (#1231: on an HDD, respawn re-reads amplify
|
|
1499
|
+
// the very I/O contention that caused the respawn).
|
|
1500
|
+
const grammarBuffers = await (0, grammars_1.readGrammarWasmBytes)(neededLanguages);
|
|
1501
|
+
pool = new parse_pool_1.ParseWorkerPool({
|
|
1502
|
+
languages: neededLanguages,
|
|
1503
|
+
size: poolSize,
|
|
1504
|
+
workerScriptPath: parseWorkerPath,
|
|
1505
|
+
recycleInterval: WORKER_RECYCLE_INTERVAL,
|
|
1506
|
+
parseTimeoutMs: PARSE_TIMEOUT_MS,
|
|
1507
|
+
log,
|
|
1508
|
+
grammarBuffers,
|
|
1509
|
+
});
|
|
1510
|
+
log(`Parse worker pool: ${poolSize} worker(s)`);
|
|
500
1511
|
}
|
|
501
1512
|
else {
|
|
502
|
-
// In-process fallback: load grammars locally
|
|
1513
|
+
// In-process fallback: load grammars locally and parse on the main thread.
|
|
503
1514
|
await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
|
|
504
1515
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1516
|
+
/**
|
|
1517
|
+
* Parse one file: on the pool when available (the promise REJECTS on a worker
|
|
1518
|
+
* crash/timeout — the caller records it and the retry pass re-attempts), or
|
|
1519
|
+
* in-process synchronously as the no-worker fallback. The language is resolved
|
|
1520
|
+
* here on the main thread, where the codegraph.json overrides are loaded.
|
|
1521
|
+
*/
|
|
1522
|
+
const parseFile = (filePath, content) => {
|
|
1523
|
+
const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
|
|
1524
|
+
if (!pool)
|
|
1525
|
+
return Promise.resolve((0, tree_sitter_1.extractFromSource)(filePath, content, language, frameworkNames));
|
|
1526
|
+
return pool.requestParse({ filePath, content, language, frameworkNames });
|
|
1527
|
+
};
|
|
1528
|
+
// --- Bounded rolling-window dispatch, ordered commit ---
|
|
1529
|
+
// Reads stay batched/parallel; parses run concurrently across the pool; the
|
|
1530
|
+
// SQLite store stays on the main thread (it isn't thread-safe). Crucially we
|
|
1531
|
+
// COMMIT results in original file order, not parse-completion order: the
|
|
1532
|
+
// resolution phase (run after indexing) resolves an ambiguous reference to one
|
|
1533
|
+
// of several same-named candidates by the nodes' DB insertion order, so a
|
|
1534
|
+
// stable commit order keeps the resulting graph deterministic — byte-identical
|
|
1535
|
+
// to the single-worker path — instead of drifting with parse timing. The
|
|
1536
|
+
// `completed` buffer holds at most ~windowSize out-of-order results, so memory
|
|
1537
|
+
// stays bounded.
|
|
1538
|
+
const windowSize = pool ? Math.max(4, pool.size * 2) : 1;
|
|
1539
|
+
const inFlight = new Set();
|
|
1540
|
+
const completed = new Map();
|
|
1541
|
+
let nextSeq = 0; // file-order sequence assigned at dispatch
|
|
1542
|
+
let nextToStore = 0; // cursor: next sequence to commit
|
|
1543
|
+
let aborted = false;
|
|
1544
|
+
// Yielder for the in-order commit path: a single giant generated file's
|
|
1545
|
+
// store is otherwise one unyielding multi-second transaction span on the
|
|
1546
|
+
// main thread (5–14s single stalls measured on llvm-project), starving
|
|
1547
|
+
// the #850 watchdog heartbeat on slow hardware.
|
|
1548
|
+
const commitYield = (0, cooperative_yield_1.createYielder)();
|
|
1549
|
+
const storeResult = async (filePath, content, stats, result) => {
|
|
1550
|
+
processed++;
|
|
1551
|
+
// WAL hard-cap backstop: between files (never mid-transaction), pause
|
|
1552
|
+
// the store until the off-thread checkpoint catches up. Resolves to
|
|
1553
|
+
// null in the normal case — a single size check, no cost.
|
|
1554
|
+
const bp = walBackpressure?.();
|
|
1555
|
+
if (bp)
|
|
1556
|
+
await bp;
|
|
1557
|
+
// Store in database on main thread (SQLite is not thread-safe)
|
|
1558
|
+
if (result.nodes.length > 0 || result.errors.length === 0) {
|
|
1559
|
+
const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
|
|
1560
|
+
await this.storeExtractionResult(filePath, content, language, stats, result, commitYield);
|
|
519
1561
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const pending = pendingParses.get(msg.id);
|
|
525
|
-
if (pending) {
|
|
526
|
-
clearTimeout(pending.timer);
|
|
527
|
-
pendingParses.delete(msg.id);
|
|
528
|
-
pending.resolve(msg.result);
|
|
529
|
-
}
|
|
1562
|
+
if (result.errors.length > 0) {
|
|
1563
|
+
for (const err of result.errors) {
|
|
1564
|
+
if (!err.filePath)
|
|
1565
|
+
err.filePath = filePath;
|
|
530
1566
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1567
|
+
errors.push(...result.errors);
|
|
1568
|
+
}
|
|
1569
|
+
if (result.nodes.length > 0) {
|
|
1570
|
+
filesIndexed++;
|
|
1571
|
+
totalNodes += result.nodes.length;
|
|
1572
|
+
totalEdges += result.edges.length;
|
|
1573
|
+
}
|
|
1574
|
+
else if (result.errors.some((e) => e.severity === 'error')) {
|
|
1575
|
+
filesErrored++;
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
// Files with no symbols but no errors (yaml, twig, properties) are
|
|
1579
|
+
// tracked at the file level — count them as indexed so the CLI doesn't
|
|
1580
|
+
// misleadingly report "No files found to index".
|
|
1581
|
+
const lang = (0, grammars_1.detectLanguage)(filePath, content, overrides);
|
|
1582
|
+
if ((0, grammars_1.isFileLevelOnlyLanguage)(lang)) {
|
|
1583
|
+
filesIndexed++;
|
|
540
1584
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (parseWorker === w) {
|
|
544
|
-
parseWorker = null;
|
|
545
|
-
workerParseCount = 0;
|
|
1585
|
+
else {
|
|
1586
|
+
filesSkipped++;
|
|
546
1587
|
}
|
|
1588
|
+
}
|
|
1589
|
+
onProgress?.({ phase: 'parsing', current: processed, total, currentFile: filePath });
|
|
1590
|
+
};
|
|
1591
|
+
const recordParseFailure = (filePath, err) => {
|
|
1592
|
+
processed++;
|
|
1593
|
+
filesErrored++;
|
|
1594
|
+
errors.push({
|
|
1595
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1596
|
+
filePath,
|
|
1597
|
+
severity: 'error',
|
|
1598
|
+
code: 'parse_error',
|
|
547
1599
|
});
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
1600
|
+
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
1601
|
+
};
|
|
1602
|
+
// Commit buffered parses to the DB in file order, advancing the cursor over
|
|
1603
|
+
// contiguous completed results. Runs after each parse settles (and once more
|
|
1604
|
+
// after the drain). storeResult is now async (it yields between chunked
|
|
1605
|
+
// inserts), so commits are SERIALIZED on a promise chain — concurrent parse
|
|
1606
|
+
// completions append to the chain instead of interleaving mid-store, which
|
|
1607
|
+
// preserves both the file-order commit invariant (#1015: resolution
|
|
1608
|
+
// disambiguates same-named candidates by insertion order) and the
|
|
1609
|
+
// single-writer discipline for SQLite. Errors are recorded and re-thrown
|
|
1610
|
+
// at the drain, matching the old synchronous propagation.
|
|
1611
|
+
let flushChain = Promise.resolve();
|
|
1612
|
+
let flushError = null;
|
|
1613
|
+
const flushOrdered = () => {
|
|
1614
|
+
flushChain = flushChain.then(async () => {
|
|
1615
|
+
if (aborted || flushError)
|
|
1616
|
+
return;
|
|
1617
|
+
try {
|
|
1618
|
+
while (completed.has(nextToStore)) {
|
|
1619
|
+
const item = completed.get(nextToStore);
|
|
1620
|
+
completed.delete(nextToStore);
|
|
1621
|
+
nextToStore++;
|
|
1622
|
+
if (item.ok)
|
|
1623
|
+
await storeResult(item.filePath, item.content, item.stats, item.result);
|
|
1624
|
+
else
|
|
1625
|
+
recordParseFailure(item.filePath, item.err);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
catch (err) {
|
|
1629
|
+
flushError = err;
|
|
1630
|
+
}
|
|
564
1631
|
});
|
|
565
|
-
return
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1632
|
+
return flushChain;
|
|
1633
|
+
};
|
|
1634
|
+
// Dispatch one file's parse (parses run concurrently across the pool), tagged
|
|
1635
|
+
// with its file-order sequence so flushOrdered commits results in order. The
|
|
1636
|
+
// backpressure below bounds how far parsing runs ahead of the in-order commit.
|
|
1637
|
+
const feed = async (filePath, content, stats) => {
|
|
1638
|
+
const seq = nextSeq++;
|
|
1639
|
+
const p = (async () => {
|
|
1640
|
+
try {
|
|
1641
|
+
const result = await parseFile(filePath, content);
|
|
1642
|
+
completed.set(seq, { ok: true, filePath, content, stats, result });
|
|
1643
|
+
}
|
|
1644
|
+
catch (parseErr) {
|
|
1645
|
+
completed.set(seq, { ok: false, filePath, err: parseErr });
|
|
1646
|
+
}
|
|
1647
|
+
flushOrdered();
|
|
1648
|
+
})();
|
|
1649
|
+
const tracked = p.finally(() => { inFlight.delete(tracked); });
|
|
1650
|
+
inFlight.add(tracked);
|
|
1651
|
+
// Backpressure on the dispatched-but-not-yet-committed count (in-flight +
|
|
1652
|
+
// buffered), not just in-flight: a slow file sitting at the commit cursor
|
|
1653
|
+
// lets later parses finish and buffer, which would otherwise grow without
|
|
1654
|
+
// bound. Wait for parses to settle (each may advance the cursor) until the
|
|
1655
|
+
// window has room. When nothing is in flight but the window is still full,
|
|
1656
|
+
// the async commit chain is what's behind — await it so the cursor
|
|
1657
|
+
// advances (buffered items hold whole file contents, so this bound is
|
|
1658
|
+
// load-bearing for memory).
|
|
1659
|
+
while (nextSeq - nextToStore >= windowSize) {
|
|
1660
|
+
if (inFlight.size > 0)
|
|
1661
|
+
await Promise.race(inFlight);
|
|
1662
|
+
else
|
|
1663
|
+
await flushOrdered();
|
|
595
1664
|
}
|
|
596
|
-
|
|
597
|
-
const id = nextId++;
|
|
598
|
-
workerParseCount++;
|
|
599
|
-
// Scale timeout for large files: base 10s + 10s per 100KB
|
|
600
|
-
const timeoutMs = PARSE_TIMEOUT_MS + Math.floor(content.length / 100_000) * 10_000;
|
|
601
|
-
return new Promise((resolve, reject) => {
|
|
602
|
-
const timer = setTimeout(() => {
|
|
603
|
-
pendingParses.delete(id);
|
|
604
|
-
log(`TIMEOUT: ${filePath} exceeded ${timeoutMs}ms — killing worker`);
|
|
605
|
-
// Reject FIRST — worker.terminate() can hang if WASM is stuck
|
|
606
|
-
parseWorker = null;
|
|
607
|
-
workerParseCount = 0;
|
|
608
|
-
reject(new Error(`Parse timed out after ${timeoutMs}ms`));
|
|
609
|
-
// Fire-and-forget: kill the stuck worker in the background
|
|
610
|
-
worker.terminate().catch(() => { });
|
|
611
|
-
}, timeoutMs);
|
|
612
|
-
pendingParses.set(id, { resolve, reject, timer });
|
|
613
|
-
worker.postMessage({ type: 'parse', id, filePath, content, frameworkNames });
|
|
614
|
-
});
|
|
615
|
-
}
|
|
1665
|
+
};
|
|
616
1666
|
for (let i = 0; i < files.length; i += FILE_IO_BATCH_SIZE) {
|
|
617
1667
|
if (signal?.aborted) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return {
|
|
621
|
-
success: false,
|
|
622
|
-
filesIndexed,
|
|
623
|
-
filesSkipped,
|
|
624
|
-
filesErrored,
|
|
625
|
-
nodesCreated: totalNodes,
|
|
626
|
-
edgesCreated: totalEdges,
|
|
627
|
-
errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
|
|
628
|
-
durationMs: Date.now() - startTime,
|
|
629
|
-
};
|
|
1668
|
+
aborted = true;
|
|
1669
|
+
break;
|
|
630
1670
|
}
|
|
631
1671
|
const batch = files.slice(i, i + FILE_IO_BATCH_SIZE);
|
|
632
1672
|
// Read files in parallel (with path validation before any I/O)
|
|
633
1673
|
const fileContents = await Promise.all(batch.map(async (fp) => {
|
|
634
1674
|
try {
|
|
635
|
-
|
|
1675
|
+
// Indexing read: follow in-root symlinks the directory walk already
|
|
1676
|
+
// descended into (the `../` guard still applies) so files reached
|
|
1677
|
+
// via an in-root symlink-to-outside still index (#935).
|
|
1678
|
+
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, fp, { allowSymlinkEscape: true });
|
|
636
1679
|
if (!fullPath) {
|
|
637
1680
|
(0, errors_1.logWarn)('Path traversal blocked in batch reader', { filePath: fp });
|
|
638
1681
|
return { filePath: fp, content: null, stats: null, error: new Error('Path traversal blocked') };
|
|
@@ -645,29 +1688,13 @@ class ExtractionOrchestrator {
|
|
|
645
1688
|
return { filePath: fp, content: null, stats: null, error: err };
|
|
646
1689
|
}
|
|
647
1690
|
}));
|
|
648
|
-
//
|
|
1691
|
+
// Dispatch each readable file into the bounded parse window; the window
|
|
1692
|
+
// stores results on the main thread as they arrive.
|
|
649
1693
|
for (const { filePath, content, stats, error } of fileContents) {
|
|
650
1694
|
if (signal?.aborted) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
return {
|
|
654
|
-
success: false,
|
|
655
|
-
filesIndexed,
|
|
656
|
-
filesSkipped,
|
|
657
|
-
filesErrored,
|
|
658
|
-
nodesCreated: totalNodes,
|
|
659
|
-
edgesCreated: totalEdges,
|
|
660
|
-
errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
|
|
661
|
-
durationMs: Date.now() - startTime,
|
|
662
|
-
};
|
|
1695
|
+
aborted = true;
|
|
1696
|
+
break;
|
|
663
1697
|
}
|
|
664
|
-
// Report progress before parsing (show current file being worked on)
|
|
665
|
-
onProgress?.({
|
|
666
|
-
phase: 'parsing',
|
|
667
|
-
current: processed,
|
|
668
|
-
total,
|
|
669
|
-
currentFile: filePath,
|
|
670
|
-
});
|
|
671
1698
|
if (error || content === null || stats === null) {
|
|
672
1699
|
processed++;
|
|
673
1700
|
filesErrored++;
|
|
@@ -677,6 +1704,7 @@ class ExtractionOrchestrator {
|
|
|
677
1704
|
severity: 'error',
|
|
678
1705
|
code: 'read_error',
|
|
679
1706
|
});
|
|
1707
|
+
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
680
1708
|
continue;
|
|
681
1709
|
}
|
|
682
1710
|
// Honour MAX_FILE_SIZE. Without this check, vendored generated
|
|
@@ -696,48 +1724,35 @@ class ExtractionOrchestrator {
|
|
|
696
1724
|
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
697
1725
|
continue;
|
|
698
1726
|
}
|
|
699
|
-
// Parse
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
result = await requestParse(filePath, content);
|
|
704
|
-
}
|
|
705
|
-
catch (parseErr) {
|
|
706
|
-
processed++;
|
|
707
|
-
filesErrored++;
|
|
708
|
-
errors.push({
|
|
709
|
-
message: parseErr instanceof Error ? parseErr.message : String(parseErr),
|
|
710
|
-
filePath,
|
|
711
|
-
severity: 'error',
|
|
712
|
-
code: 'parse_error',
|
|
713
|
-
});
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
processed++;
|
|
717
|
-
// Store in database on main thread (SQLite is not thread-safe)
|
|
718
|
-
if (result.nodes.length > 0 || result.errors.length === 0) {
|
|
719
|
-
const language = (0, grammars_1.detectLanguage)(filePath, content);
|
|
720
|
-
this.storeExtractionResult(filePath, content, language, stats, result);
|
|
721
|
-
}
|
|
722
|
-
if (result.errors.length > 0) {
|
|
723
|
-
for (const err of result.errors) {
|
|
724
|
-
if (!err.filePath)
|
|
725
|
-
err.filePath = filePath;
|
|
726
|
-
}
|
|
727
|
-
errors.push(...result.errors);
|
|
728
|
-
}
|
|
729
|
-
if (result.nodes.length > 0) {
|
|
730
|
-
filesIndexed++;
|
|
731
|
-
totalNodes += result.nodes.length;
|
|
732
|
-
totalEdges += result.edges.length;
|
|
733
|
-
}
|
|
734
|
-
else if (result.errors.some((e) => e.severity === 'error')) {
|
|
735
|
-
filesErrored++;
|
|
736
|
-
}
|
|
737
|
-
else {
|
|
738
|
-
filesSkipped++;
|
|
739
|
-
}
|
|
1727
|
+
// Parse on the pool (main thread stays unblocked). Errors/timeouts are
|
|
1728
|
+
// handled inside feed() → recordParseFailure, feeding the retry pass.
|
|
1729
|
+
await feed(filePath, content, stats);
|
|
740
1730
|
}
|
|
1731
|
+
if (aborted)
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
// Drain parses still in flight (skip on abort — we tear down below instead),
|
|
1735
|
+
// then commit any results the cursor hasn't reached yet.
|
|
1736
|
+
if (!aborted) {
|
|
1737
|
+
await Promise.all(inFlight);
|
|
1738
|
+
await flushOrdered();
|
|
1739
|
+
if (flushError)
|
|
1740
|
+
throw flushError;
|
|
1741
|
+
}
|
|
1742
|
+
if (signal?.aborted || aborted) {
|
|
1743
|
+
if (pool)
|
|
1744
|
+
await pool.destroy();
|
|
1745
|
+
return {
|
|
1746
|
+
success: false,
|
|
1747
|
+
filesIndexed,
|
|
1748
|
+
filesSkipped,
|
|
1749
|
+
filesErrored,
|
|
1750
|
+
filesDiscovered: total,
|
|
1751
|
+
nodesCreated: totalNodes,
|
|
1752
|
+
edgesCreated: totalEdges,
|
|
1753
|
+
errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
|
|
1754
|
+
durationMs: Date.now() - startTime,
|
|
1755
|
+
};
|
|
741
1756
|
}
|
|
742
1757
|
// Report 100% so the progress bar doesn't hang at 99%
|
|
743
1758
|
onProgress?.({
|
|
@@ -751,18 +1766,25 @@ class ExtractionOrchestrator {
|
|
|
751
1766
|
await new Promise(resolve => setImmediate(resolve));
|
|
752
1767
|
// Retry pass: files that failed due to WASM memory corruption may succeed
|
|
753
1768
|
// on a fresh worker with a clean heap. Recycle before each attempt so
|
|
754
|
-
// every file gets the absolute cleanest WASM state possible.
|
|
1769
|
+
// every file gets the absolute cleanest WASM state possible. Timeouts are
|
|
1770
|
+
// retried too (#1231): most are main-thread-stall artifacts, not slow
|
|
1771
|
+
// parses, and this pass parses one file at a time with the store strictly
|
|
1772
|
+
// after each parse resolves, so the stall window can't recur here.
|
|
755
1773
|
const retryableErrors = errors.filter((e) => e.code === 'parse_error' && e.filePath &&
|
|
756
|
-
(e.message.includes('Worker exited') ||
|
|
757
|
-
|
|
758
|
-
|
|
1774
|
+
(e.message.includes('Worker exited') ||
|
|
1775
|
+
e.message.includes('memory access out of bounds') ||
|
|
1776
|
+
e.message.includes('timed out')));
|
|
1777
|
+
if (retryableErrors.length > 0 && pool) {
|
|
1778
|
+
log(`Retrying ${retryableErrors.length} files that failed due to WASM memory errors or timeouts...`);
|
|
1779
|
+
// Fresh WASM heaps for the retry phase. A retry that still crashes its
|
|
1780
|
+
// worker makes the pool respawn it, so later retries keep landing on clean
|
|
1781
|
+
// workers too.
|
|
1782
|
+
pool.recycleAll();
|
|
759
1783
|
const stillFailing = [];
|
|
760
1784
|
for (const errEntry of retryableErrors) {
|
|
761
1785
|
const filePath = errEntry.filePath;
|
|
762
1786
|
if (signal?.aborted)
|
|
763
1787
|
break;
|
|
764
|
-
// Fresh worker for every retry — maximum WASM headroom
|
|
765
|
-
recycleWorker();
|
|
766
1788
|
let content;
|
|
767
1789
|
try {
|
|
768
1790
|
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
|
|
@@ -775,16 +1797,16 @@ class ExtractionOrchestrator {
|
|
|
775
1797
|
}
|
|
776
1798
|
let result;
|
|
777
1799
|
try {
|
|
778
|
-
result = await
|
|
1800
|
+
result = await parseFile(filePath, content);
|
|
779
1801
|
}
|
|
780
1802
|
catch {
|
|
781
1803
|
stillFailing.push(errEntry);
|
|
782
1804
|
continue;
|
|
783
1805
|
}
|
|
784
1806
|
if (result.nodes.length > 0 || result.errors.length === 0) {
|
|
785
|
-
const language = (0, grammars_1.detectLanguage)(filePath, content);
|
|
1807
|
+
const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
|
|
786
1808
|
const stats = await fsp.stat(path.join(this.rootDir, filePath));
|
|
787
|
-
this.storeExtractionResult(filePath, content, language, stats, result);
|
|
1809
|
+
await this.storeExtractionResult(filePath, content, language, stats, result, commitYield);
|
|
788
1810
|
const idx = errors.indexOf(errEntry);
|
|
789
1811
|
if (idx >= 0)
|
|
790
1812
|
errors.splice(idx, 1);
|
|
@@ -801,11 +1823,11 @@ class ExtractionOrchestrator {
|
|
|
801
1823
|
// code nodes but consume parser memory.
|
|
802
1824
|
if (stillFailing.length > 0) {
|
|
803
1825
|
log(`${stillFailing.length} files still failing — retrying with comments stripped...`);
|
|
1826
|
+
pool.recycleAll();
|
|
804
1827
|
for (const errEntry of stillFailing) {
|
|
805
1828
|
const filePath = errEntry.filePath;
|
|
806
1829
|
if (signal?.aborted)
|
|
807
1830
|
break;
|
|
808
|
-
recycleWorker();
|
|
809
1831
|
let fullContent;
|
|
810
1832
|
try {
|
|
811
1833
|
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
|
|
@@ -824,15 +1846,15 @@ class ExtractionOrchestrator {
|
|
|
824
1846
|
.join('\n');
|
|
825
1847
|
let result;
|
|
826
1848
|
try {
|
|
827
|
-
result = await
|
|
1849
|
+
result = await parseFile(filePath, stripped);
|
|
828
1850
|
}
|
|
829
1851
|
catch {
|
|
830
1852
|
continue;
|
|
831
1853
|
}
|
|
832
1854
|
if (result.nodes.length > 0 || result.errors.length === 0) {
|
|
833
|
-
const language = (0, grammars_1.detectLanguage)(filePath, fullContent);
|
|
1855
|
+
const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
|
|
834
1856
|
const stats = await fsp.stat(path.join(this.rootDir, filePath));
|
|
835
|
-
this.storeExtractionResult(filePath, fullContent, language, stats, result);
|
|
1857
|
+
await this.storeExtractionResult(filePath, fullContent, language, stats, result, commitYield);
|
|
836
1858
|
const idx = errors.indexOf(errEntry);
|
|
837
1859
|
if (idx >= 0)
|
|
838
1860
|
errors.splice(idx, 1);
|
|
@@ -845,16 +1867,15 @@ class ExtractionOrchestrator {
|
|
|
845
1867
|
}
|
|
846
1868
|
}
|
|
847
1869
|
}
|
|
848
|
-
// Shut down parse worker
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
parseWorker.terminate().catch(() => { });
|
|
852
|
-
}
|
|
1870
|
+
// Shut down the parse worker pool.
|
|
1871
|
+
if (pool)
|
|
1872
|
+
await pool.destroy();
|
|
853
1873
|
return {
|
|
854
1874
|
success: filesIndexed > 0 || errors.filter((e) => e.severity === 'error').length === 0,
|
|
855
1875
|
filesIndexed,
|
|
856
1876
|
filesSkipped,
|
|
857
1877
|
filesErrored,
|
|
1878
|
+
filesDiscovered: total,
|
|
858
1879
|
nodesCreated: totalNodes,
|
|
859
1880
|
edgesCreated: totalEdges,
|
|
860
1881
|
errors,
|
|
@@ -886,7 +1907,13 @@ class ExtractionOrchestrator {
|
|
|
886
1907
|
filesErrored++;
|
|
887
1908
|
}
|
|
888
1909
|
else {
|
|
889
|
-
|
|
1910
|
+
const tracked = this.queries.getFileByPath(filePath);
|
|
1911
|
+
if (tracked && (0, grammars_1.isFileLevelOnlyLanguage)(tracked.language)) {
|
|
1912
|
+
filesIndexed++;
|
|
1913
|
+
}
|
|
1914
|
+
else {
|
|
1915
|
+
filesSkipped++;
|
|
1916
|
+
}
|
|
890
1917
|
}
|
|
891
1918
|
}
|
|
892
1919
|
return {
|
|
@@ -904,7 +1931,8 @@ class ExtractionOrchestrator {
|
|
|
904
1931
|
* Index a single file
|
|
905
1932
|
*/
|
|
906
1933
|
async indexFile(relativePath) {
|
|
907
|
-
|
|
1934
|
+
// Indexing read: follow in-root symlinks (the `../` guard still applies), #935.
|
|
1935
|
+
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, relativePath, { allowSymlinkEscape: true });
|
|
908
1936
|
if (!fullPath) {
|
|
909
1937
|
return {
|
|
910
1938
|
nodes: [],
|
|
@@ -944,8 +1972,8 @@ class ExtractionOrchestrator {
|
|
|
944
1972
|
* Used by the parallel batch reader to avoid redundant file I/O.
|
|
945
1973
|
*/
|
|
946
1974
|
async indexFileWithContent(relativePath, content, stats) {
|
|
947
|
-
// Prevent
|
|
948
|
-
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, relativePath);
|
|
1975
|
+
// Prevent `../` traversal; follow in-root symlinks like the directory walk (#935).
|
|
1976
|
+
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, relativePath, { allowSymlinkEscape: true });
|
|
949
1977
|
if (!fullPath) {
|
|
950
1978
|
(0, errors_1.logWarn)('Path traversal blocked in indexFileWithContent', { relativePath });
|
|
951
1979
|
return {
|
|
@@ -973,8 +2001,8 @@ class ExtractionOrchestrator {
|
|
|
973
2001
|
durationMs: 0,
|
|
974
2002
|
};
|
|
975
2003
|
}
|
|
976
|
-
// Detect language
|
|
977
|
-
const language = (0, grammars_1.detectLanguage)(relativePath, content);
|
|
2004
|
+
// Detect language (honoring the project's codegraph.json extension overrides)
|
|
2005
|
+
const language = (0, grammars_1.detectLanguage)(relativePath, content, (0, project_config_1.loadExtensionOverrides)(this.rootDir));
|
|
978
2006
|
if (!(0, grammars_1.isLanguageSupported)(language)) {
|
|
979
2007
|
return {
|
|
980
2008
|
nodes: [],
|
|
@@ -991,20 +2019,46 @@ class ExtractionOrchestrator {
|
|
|
991
2019
|
const result = (0, tree_sitter_1.extractFromSource)(relativePath, content, language, frameworkNames);
|
|
992
2020
|
// Store in database
|
|
993
2021
|
if (result.nodes.length > 0 || result.errors.length === 0) {
|
|
994
|
-
this.storeExtractionResult(relativePath, content, language, stats, result);
|
|
2022
|
+
await this.storeExtractionResult(relativePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
|
|
995
2023
|
}
|
|
996
2024
|
return result;
|
|
997
2025
|
}
|
|
998
2026
|
/**
|
|
999
2027
|
* Store extraction result in database
|
|
1000
2028
|
*/
|
|
1001
|
-
storeExtractionResult(filePath, content, language, stats, result) {
|
|
2029
|
+
async storeExtractionResult(filePath, content, language, stats, result, onYield) {
|
|
2030
|
+
// Bulk inserts run in bounded sub-transactions with a yield between, so a
|
|
2031
|
+
// giant generated file (tens of thousands of symbols) can't block the
|
|
2032
|
+
// event loop — and the #850 watchdog heartbeat — for the whole store.
|
|
2033
|
+
// The file was NEVER one atomic transaction (each insert call has its
|
|
2034
|
+
// own), and the files-table record still lands last, so crash recovery
|
|
2035
|
+
// is unchanged: a partially-stored file has no record and re-indexes.
|
|
2036
|
+
const STORE_CHUNK = 2000;
|
|
1002
2037
|
const contentHash = hashContent(content);
|
|
1003
2038
|
// Check if file already exists and hasn't changed
|
|
1004
2039
|
const existingFile = this.queries.getFileByPath(filePath);
|
|
1005
2040
|
if (existingFile && existingFile.contentHash === contentHash) {
|
|
1006
2041
|
return; // No changes
|
|
1007
2042
|
}
|
|
2043
|
+
// Snapshot incoming cross-file edges BEFORE deleting this file's nodes.
|
|
2044
|
+
// `deleteFile` cascades to delete every edge whose source OR target is a
|
|
2045
|
+
// node in this file (edges.FK ... ON DELETE CASCADE). Edges whose SOURCE is
|
|
2046
|
+
// in this file are re-emitted by the extractor below, but edges whose SOURCE
|
|
2047
|
+
// is in a *different* (unchanged) file are not — they would be silently
|
|
2048
|
+
// dropped, which is issue #899: re-indexing a callee file severs `calls`/
|
|
2049
|
+
// `references` edges from callers that import it via module-attribute
|
|
2050
|
+
// access (`pkg.mod.fn(...)`).
|
|
2051
|
+
//
|
|
2052
|
+
// We snapshot the edge plus the target node's (name, kind) so we can
|
|
2053
|
+
// re-resolve to the re-indexed target's NEW id. Node ids are
|
|
2054
|
+
// `sha256(filePath:kind:name:line)`, so any line shift in the callee file
|
|
2055
|
+
// (e.g. a docstring-only edit above the symbol) changes every target id and
|
|
2056
|
+
// a naive re-insert by old id would silently drop every edge. Matching by
|
|
2057
|
+
// (filePath, kind, name) is stable across line shifts; if the symbol was
|
|
2058
|
+
// renamed/removed, no match is found and the edge stays dropped (correct).
|
|
2059
|
+
const crossFileIncomingEdges = existingFile
|
|
2060
|
+
? this.queries.getCrossFileIncomingEdgesWithTarget(filePath)
|
|
2061
|
+
: [];
|
|
1008
2062
|
// Delete existing data for this file
|
|
1009
2063
|
if (existingFile) {
|
|
1010
2064
|
this.queries.deleteFile(filePath);
|
|
@@ -1013,30 +2067,76 @@ class ExtractionOrchestrator {
|
|
|
1013
2067
|
// This prevents FK violations when edges reference nodes that would
|
|
1014
2068
|
// be silently skipped by insertNode() (see issue #42).
|
|
1015
2069
|
const validNodes = result.nodes.filter((n) => n.id && n.kind && n.name && n.filePath && n.language);
|
|
1016
|
-
// Insert nodes
|
|
1017
|
-
|
|
1018
|
-
this.queries.insertNodes(validNodes);
|
|
2070
|
+
// Insert nodes (chunked — see STORE_CHUNK above)
|
|
2071
|
+
for (let i = 0; i < validNodes.length; i += STORE_CHUNK) {
|
|
2072
|
+
this.queries.insertNodes(validNodes.slice(i, i + STORE_CHUNK));
|
|
2073
|
+
await onYield?.();
|
|
1019
2074
|
}
|
|
1020
2075
|
// Filter edges to only reference nodes that were actually inserted
|
|
1021
2076
|
if (result.edges.length > 0) {
|
|
1022
2077
|
const insertedIds = new Set(validNodes.map((n) => n.id));
|
|
1023
2078
|
const validEdges = result.edges.filter((e) => insertedIds.has(e.source) && insertedIds.has(e.target));
|
|
1024
|
-
|
|
1025
|
-
this.queries.insertEdges(validEdges);
|
|
2079
|
+
for (let i = 0; i < validEdges.length; i += STORE_CHUNK) {
|
|
2080
|
+
this.queries.insertEdges(validEdges.slice(i, i + STORE_CHUNK));
|
|
2081
|
+
await onYield?.();
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
// Re-insert cross-file incoming edges snapshotted before the delete,
|
|
2085
|
+
// re-resolving each edge's target to the re-indexed node's new id by
|
|
2086
|
+
// (filePath, kind, name). Node ids include the source line, so any line
|
|
2087
|
+
// shift in the callee file (e.g. a docstring-only edit above the symbol)
|
|
2088
|
+
// changes every target id and a naive re-insert by old id would drop them
|
|
2089
|
+
// all. `insertEdges` still filters to endpoints that exist. This closes
|
|
2090
|
+
// the #899 edge-drop on `sync`.
|
|
2091
|
+
//
|
|
2092
|
+
// Edges whose callee (target) was renamed/removed during the re-index (no
|
|
2093
|
+
// match in `newNodesByKindName`) are not silently dropped anymore: each is
|
|
2094
|
+
// resurrected as its ORIGINAL unresolved ref (stamped on the edge as
|
|
2095
|
+
// metadata.refName/refKind at creation) so the same sync's resolution
|
|
2096
|
+
// sweep can rebind it to an alternative definition elsewhere, or park it
|
|
2097
|
+
// as status='failed' to be retried when the symbol reappears — the
|
|
2098
|
+
// removal-side counterpart of #1240. Edges without refName (built before
|
|
2099
|
+
// the stamp existed, or synthesized) still drop silently: reconstructing
|
|
2100
|
+
// a ref from the target's plain name would strip receiver/qualifier
|
|
2101
|
+
// context and risk a rebind a full re-index would never make.
|
|
2102
|
+
if (crossFileIncomingEdges.length > 0) {
|
|
2103
|
+
const newNodesByKindName = new Map();
|
|
2104
|
+
for (const n of validNodes) {
|
|
2105
|
+
newNodesByKindName.set(`${n.kind}\0${n.name}`, n.id);
|
|
2106
|
+
}
|
|
2107
|
+
const reinserted = [];
|
|
2108
|
+
const resurrected = [];
|
|
2109
|
+
for (const e of crossFileIncomingEdges) {
|
|
2110
|
+
const newTargetId = newNodesByKindName.get(`${e.targetKind}\0${e.targetName}`);
|
|
2111
|
+
if (newTargetId) {
|
|
2112
|
+
reinserted.push({ source: e.source, target: newTargetId, kind: e.kind, metadata: e.metadata, line: e.line, column: e.column, provenance: e.provenance });
|
|
2113
|
+
}
|
|
2114
|
+
else {
|
|
2115
|
+
const ref = resurrectRefFromDroppedEdge(e);
|
|
2116
|
+
if (ref)
|
|
2117
|
+
resurrected.push(ref);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (reinserted.length > 0) {
|
|
2121
|
+
this.queries.insertEdges(reinserted);
|
|
2122
|
+
}
|
|
2123
|
+
if (resurrected.length > 0) {
|
|
2124
|
+
this.queries.insertUnresolvedRefsBatch(resurrected);
|
|
1026
2125
|
}
|
|
1027
2126
|
}
|
|
1028
2127
|
// Insert unresolved references in batch with denormalized filePath/language
|
|
1029
2128
|
if (result.unresolvedReferences.length > 0) {
|
|
1030
2129
|
const insertedIds = new Set(validNodes.map((n) => n.id));
|
|
1031
2130
|
const refsWithContext = result.unresolvedReferences
|
|
1032
|
-
.filter((ref) => insertedIds.has(ref.fromNodeId)
|
|
2131
|
+
.filter((ref) => insertedIds.has(ref.fromNodeId))
|
|
1033
2132
|
.map((ref) => ({
|
|
1034
2133
|
...ref,
|
|
1035
2134
|
filePath: ref.filePath ?? filePath,
|
|
1036
2135
|
language: ref.language ?? language,
|
|
1037
2136
|
}));
|
|
1038
|
-
|
|
1039
|
-
this.queries.insertUnresolvedRefsBatch(refsWithContext);
|
|
2137
|
+
for (let i = 0; i < refsWithContext.length; i += STORE_CHUNK) {
|
|
2138
|
+
this.queries.insertUnresolvedRefsBatch(refsWithContext.slice(i, i + STORE_CHUNK));
|
|
2139
|
+
await onYield?.();
|
|
1040
2140
|
}
|
|
1041
2141
|
}
|
|
1042
2142
|
// Insert file record
|
|
@@ -1053,8 +2153,12 @@ class ExtractionOrchestrator {
|
|
|
1053
2153
|
this.queries.upsertFile(fileRecord);
|
|
1054
2154
|
}
|
|
1055
2155
|
/**
|
|
1056
|
-
* Sync with current file state.
|
|
1057
|
-
*
|
|
2156
|
+
* Sync the index with the current file state.
|
|
2157
|
+
*
|
|
2158
|
+
* Change detection is filesystem-based, never git: a (size, mtime) stat
|
|
2159
|
+
* pre-filter skips unchanged files, then a content-hash compare confirms real
|
|
2160
|
+
* changes. This works in non-git projects and catches committed changes from
|
|
2161
|
+
* `git pull`/`checkout`/`merge`/`rebase` that `git status` cannot see.
|
|
1058
2162
|
*/
|
|
1059
2163
|
async sync(onProgress) {
|
|
1060
2164
|
await (0, grammars_1.initGrammars)(); // Initialize WASM runtime (grammars loaded lazily below)
|
|
@@ -1071,93 +2175,112 @@ class ExtractionOrchestrator {
|
|
|
1071
2175
|
total: 0,
|
|
1072
2176
|
});
|
|
1073
2177
|
const filesToIndex = [];
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
2178
|
+
// === Filesystem reconcile (git-independent) ===
|
|
2179
|
+
// The source of truth for "what changed" is the filesystem vs the indexed
|
|
2180
|
+
// state — never git. We enumerate the current source files and reconcile
|
|
2181
|
+
// each against the DB. A cheap (size, mtime) stat pre-filter skips unchanged
|
|
2182
|
+
// files without reading or hashing them, so the expensive read+hash+parse
|
|
2183
|
+
// only runs for files that actually changed. This catches edits/adds/deletes
|
|
2184
|
+
// whether or not the project uses git, and crucially also catches committed
|
|
2185
|
+
// changes from `git pull`/`checkout`/`merge`/`rebase` — which `git status`
|
|
2186
|
+
// cannot see, because the working tree is clean afterward.
|
|
2187
|
+
const tSyncScan = Date.now();
|
|
2188
|
+
const currentFiles = await scanDirectoryAsync(this.rootDir);
|
|
2189
|
+
if (process.env.CODEGRAPH_SYNTH_TIMINGS)
|
|
2190
|
+
console.error(`[phase-timing] sync-scan: ${Date.now() - tSyncScan}ms (${currentFiles.length} files)`);
|
|
2191
|
+
filesChecked = currentFiles.length;
|
|
2192
|
+
const currentSet = new Set(currentFiles);
|
|
2193
|
+
const tTracked = Date.now();
|
|
2194
|
+
const trackedFiles = this.queries.getAllFiles();
|
|
2195
|
+
if (process.env.CODEGRAPH_SYNTH_TIMINGS)
|
|
2196
|
+
console.error(`[phase-timing] sync-tracked-load: ${Date.now() - tTracked}ms (${trackedFiles.length} tracked)`);
|
|
2197
|
+
const trackedMap = new Map();
|
|
2198
|
+
for (const f of trackedFiles) {
|
|
2199
|
+
trackedMap.set(f.path, f);
|
|
2200
|
+
}
|
|
2201
|
+
// Removals: tracked in the DB but no longer a present source file. Check the
|
|
2202
|
+
// filesystem directly — `scanDirectory` (via `git ls-files`) still lists a
|
|
2203
|
+
// file deleted from disk but not yet staged, so set membership alone misses it.
|
|
2204
|
+
// `reconcileChecks` drives the cooperative yield shared with the adds/mods loop
|
|
2205
|
+
// below (see SYNC_RECONCILE_YIELD_INTERVAL / issue #905).
|
|
2206
|
+
let reconcileChecks = 0;
|
|
2207
|
+
for (const tracked of trackedFiles) {
|
|
2208
|
+
if (!currentSet.has(tracked.path) || !fs.existsSync(path.join(this.rootDir, tracked.path))) {
|
|
2209
|
+
// Before the cascade deletes them, resurrect incoming cross-file
|
|
2210
|
+
// resolution edges as their original refs (#1240 removal case): the
|
|
2211
|
+
// callers live in files this sync will NOT revisit, so this is their
|
|
2212
|
+
// only chance to rebind to an alternative definition — or to park as
|
|
2213
|
+
// failed until the symbol reappears somewhere. (A deleted file whose
|
|
2214
|
+
// CALLERS are also being deleted is fine: their nodes cascade later
|
|
2215
|
+
// in this loop and take the resurrected rows with them.)
|
|
2216
|
+
const incoming = this.queries.getCrossFileIncomingEdgesWithTarget(tracked.path);
|
|
2217
|
+
if (incoming.length > 0) {
|
|
2218
|
+
const resurrected = incoming
|
|
2219
|
+
.map((e) => resurrectRefFromDroppedEdge(e))
|
|
2220
|
+
.filter((r) => r !== null);
|
|
2221
|
+
if (resurrected.length > 0) {
|
|
2222
|
+
this.queries.insertUnresolvedRefsBatch(resurrected);
|
|
2223
|
+
}
|
|
1085
2224
|
}
|
|
2225
|
+
this.queries.deleteFile(tracked.path);
|
|
2226
|
+
filesRemoved++;
|
|
1086
2227
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
// can't be trusted as "new": re-hash and compare against the DB exactly
|
|
1090
|
-
// like modified files. Otherwise every sync re-indexes them and status
|
|
1091
|
-
// reports them as pending forever. (See issue #206.)
|
|
1092
|
-
for (const filePath of [...gitChanges.modified, ...gitChanges.added]) {
|
|
1093
|
-
const fullPath = path.join(this.rootDir, filePath);
|
|
1094
|
-
let content;
|
|
1095
|
-
try {
|
|
1096
|
-
content = fs.readFileSync(fullPath, 'utf-8');
|
|
1097
|
-
}
|
|
1098
|
-
catch (error) {
|
|
1099
|
-
(0, errors_1.logDebug)('Skipping unreadable file during sync', { filePath, error: String(error) });
|
|
1100
|
-
continue;
|
|
1101
|
-
}
|
|
1102
|
-
const contentHash = hashContent(content);
|
|
1103
|
-
const tracked = this.queries.getFileByPath(filePath);
|
|
1104
|
-
if (!tracked) {
|
|
1105
|
-
filesToIndex.push(filePath);
|
|
1106
|
-
changedFilePaths.push(filePath);
|
|
1107
|
-
filesAdded++;
|
|
1108
|
-
}
|
|
1109
|
-
else if (tracked.contentHash !== contentHash) {
|
|
1110
|
-
filesToIndex.push(filePath);
|
|
1111
|
-
changedFilePaths.push(filePath);
|
|
1112
|
-
filesModified++;
|
|
1113
|
-
}
|
|
2228
|
+
if (++reconcileChecks % SYNC_RECONCILE_YIELD_INTERVAL === 0) {
|
|
2229
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1114
2230
|
}
|
|
1115
2231
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
for (const f of trackedFiles) {
|
|
1124
|
-
trackedMap.set(f.path, f);
|
|
1125
|
-
}
|
|
1126
|
-
// Find files to remove (in DB but not on disk)
|
|
1127
|
-
for (const tracked of trackedFiles) {
|
|
1128
|
-
if (!currentFiles.has(tracked.path)) {
|
|
1129
|
-
this.queries.deleteFile(tracked.path);
|
|
1130
|
-
filesRemoved++;
|
|
1131
|
-
}
|
|
2232
|
+
// Adds / modifications.
|
|
2233
|
+
for (const filePath of currentFiles) {
|
|
2234
|
+
// Same cooperative yield as the removals loop — this is the other O(files)
|
|
2235
|
+
// synchronous-stat loop that wedges the main thread on a large repo (#905).
|
|
2236
|
+
// Yield at the top of the body so the `continue` fast-paths below still hit it.
|
|
2237
|
+
if (++reconcileChecks % SYNC_RECONCILE_YIELD_INTERVAL === 0) {
|
|
2238
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1132
2239
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
2240
|
+
const fullPath = path.join(this.rootDir, filePath);
|
|
2241
|
+
const tracked = trackedMap.get(filePath);
|
|
2242
|
+
// Cheap pre-filter: an already-indexed file whose size AND mtime both match
|
|
2243
|
+
// the DB is unchanged — skip it without reading or hashing. (A content
|
|
2244
|
+
// change that preserves both exactly is the blind spot every mtime-based
|
|
2245
|
+
// incremental tool accepts; `index --force` is the escape hatch. Git bumps
|
|
2246
|
+
// mtime on every file it writes during checkout/merge, so pulls are caught.)
|
|
2247
|
+
if (tracked) {
|
|
1137
2248
|
try {
|
|
1138
|
-
|
|
2249
|
+
const stat = fs.statSync(fullPath);
|
|
2250
|
+
if (stat.size === tracked.size && Math.floor(stat.mtimeMs) === Math.floor(tracked.modifiedAt)) {
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
1139
2253
|
}
|
|
1140
2254
|
catch (error) {
|
|
1141
|
-
(0, errors_1.logDebug)('Skipping
|
|
2255
|
+
(0, errors_1.logDebug)('Skipping unstattable file during sync', { filePath, error: String(error) });
|
|
1142
2256
|
continue;
|
|
1143
2257
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
2258
|
+
}
|
|
2259
|
+
// New, or size/mtime changed — read + hash to confirm a real content change.
|
|
2260
|
+
let content;
|
|
2261
|
+
try {
|
|
2262
|
+
content = fs.readFileSync(fullPath, 'utf-8');
|
|
2263
|
+
}
|
|
2264
|
+
catch (error) {
|
|
2265
|
+
(0, errors_1.logDebug)('Skipping unreadable file during sync', { filePath, error: String(error) });
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
const contentHash = hashContent(content);
|
|
2269
|
+
if (!tracked) {
|
|
2270
|
+
filesToIndex.push(filePath);
|
|
2271
|
+
changedFilePaths.push(filePath);
|
|
2272
|
+
filesAdded++;
|
|
2273
|
+
}
|
|
2274
|
+
else if (tracked.contentHash !== contentHash) {
|
|
2275
|
+
filesToIndex.push(filePath);
|
|
2276
|
+
changedFilePaths.push(filePath);
|
|
2277
|
+
filesModified++;
|
|
1156
2278
|
}
|
|
1157
2279
|
}
|
|
1158
2280
|
// Load only grammars needed for changed files
|
|
1159
2281
|
if (filesToIndex.length > 0) {
|
|
1160
|
-
const
|
|
2282
|
+
const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
|
|
2283
|
+
const neededLanguages = [...new Set(filesToIndex.map((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides)))];
|
|
1161
2284
|
// .h files default to 'c' but may be C++ — ensure cpp grammar is loaded
|
|
1162
2285
|
if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
|
|
1163
2286
|
neededLanguages.push('cpp');
|