homegraph 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +268 -243
- package/dist/arkts/ohos-api-index.d.ts +15 -0
- package/dist/arkts/ohos-api-index.d.ts.map +1 -0
- package/dist/arkts/ohos-api-index.js +190 -0
- package/dist/arkts/ohos-api-index.js.map +1 -0
- package/dist/arkts/ohos-sdk-input.d.ts +36 -0
- package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
- package/dist/arkts/ohos-sdk-input.js +214 -0
- package/dist/arkts/ohos-sdk-input.js.map +1 -0
- package/dist/bin/command-supervision.d.ts +12 -0
- package/dist/bin/command-supervision.d.ts.map +1 -0
- package/dist/bin/command-supervision.js +86 -0
- package/dist/bin/command-supervision.js.map +1 -0
- package/dist/bin/homegraph.d.ts +1 -1
- package/dist/bin/homegraph.js +971 -71
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/db/index.d.ts +20 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +39 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +58 -19
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +14 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +314 -143
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +172 -152
- package/dist/directory.d.ts +32 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +88 -5
- package/dist/directory.js.map +1 -1
- package/dist/extraction/arkts-batch-worker.d.ts +2 -0
- package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
- package/dist/extraction/arkts-batch-worker.js +28 -0
- package/dist/extraction/arkts-batch-worker.js.map +1 -0
- package/dist/extraction/context.d.ts +11 -0
- package/dist/extraction/context.d.ts.map +1 -1
- package/dist/extraction/context.js +20 -0
- package/dist/extraction/context.js.map +1 -1
- package/dist/extraction/index.d.ts +16 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +619 -380
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
- package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
- package/dist/extraction/languages/arkts-state-decorators.js +26 -0
- package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
- package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
- package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
- package/dist/extraction/languages/arkts-viewtree.js +21 -6
- package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +98 -2
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +1290 -61
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +56 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +198 -1
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
- package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
- package/dist/extraction/languages/ohos-api-consumer.js +283 -0
- package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
- package/dist/extraction/parse-pool.d.ts +126 -0
- package/dist/extraction/parse-pool.d.ts.map +1 -0
- package/dist/extraction/parse-pool.js +319 -0
- package/dist/extraction/parse-pool.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +17 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +21 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +198 -27
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +76 -17
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -4
- package/dist/index.js.map +1 -1
- package/dist/installer/instructions-template.js +9 -9
- package/dist/installer/targets/shared.d.ts +5 -6
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +5 -6
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/mcp/daemon-paths.d.ts +30 -3
- package/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/dist/mcp/daemon-paths.js +50 -10
- package/dist/mcp/daemon-paths.js.map +1 -1
- package/dist/mcp/daemon-registry.d.ts.map +1 -1
- package/dist/mcp/daemon-registry.js +7 -3
- package/dist/mcp/daemon-registry.js.map +1 -1
- package/dist/mcp/daemon.d.ts +48 -0
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +196 -32
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/engine.d.ts +17 -0
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +73 -1
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +25 -43
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.js +16 -16
- package/dist/mcp/ppid-watchdog.d.ts +18 -0
- package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
- package/dist/mcp/ppid-watchdog.js +37 -0
- package/dist/mcp/ppid-watchdog.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +25 -0
- package/dist/mcp/query-cache.d.ts.map +1 -0
- package/dist/mcp/query-cache.js +191 -0
- package/dist/mcp/query-cache.js.map +1 -0
- package/dist/mcp/query-pool.d.ts +94 -0
- package/dist/mcp/query-pool.d.ts.map +1 -0
- package/dist/mcp/query-pool.js +297 -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 +5 -7
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +72 -74
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +94 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +877 -71
- package/dist/mcp/tools.js.map +1 -1
- package/dist/project-config.d.ts +20 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +42 -2
- package/dist/project-config.js.map +1 -1
- package/dist/reasoning/login.js +1 -1
- package/dist/reasoning/login.js.map +1 -1
- package/dist/reasoning/reasoner.js +32 -32
- package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +765 -79
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +1 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +72 -11
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- 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/index.d.ts +11 -2
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +72 -4
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +22 -0
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +317 -20
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/spec/config.d.ts +39 -0
- package/dist/spec/config.d.ts.map +1 -0
- package/dist/spec/config.js +304 -0
- package/dist/spec/config.js.map +1 -0
- package/dist/spec/db/commit-node.d.ts +23 -0
- package/dist/spec/db/commit-node.d.ts.map +1 -0
- package/dist/spec/db/commit-node.js +62 -0
- package/dist/spec/db/commit-node.js.map +1 -0
- package/dist/spec/db/fragment-node.d.ts +24 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -0
- package/dist/spec/db/fragment-node.js +128 -0
- package/dist/spec/db/fragment-node.js.map +1 -0
- package/dist/spec/db/fts.d.ts +74 -0
- package/dist/spec/db/fts.d.ts.map +1 -0
- package/dist/spec/db/fts.js +324 -0
- package/dist/spec/db/fts.js.map +1 -0
- package/dist/spec/db/index.d.ts +13 -0
- package/dist/spec/db/index.d.ts.map +1 -0
- package/dist/spec/db/index.js +50 -0
- package/dist/spec/db/index.js.map +1 -0
- package/dist/spec/db/relations.d.ts +55 -0
- package/dist/spec/db/relations.d.ts.map +1 -0
- package/dist/spec/db/relations.js +158 -0
- package/dist/spec/db/relations.js.map +1 -0
- package/dist/spec/db/schema.d.ts +33 -0
- package/dist/spec/db/schema.d.ts.map +1 -0
- package/dist/spec/db/schema.js +119 -0
- package/dist/spec/db/schema.js.map +1 -0
- package/dist/spec/db/schema.sql +117 -0
- package/dist/spec/db/spec-node.d.ts +41 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -0
- package/dist/spec/db/spec-node.js +114 -0
- package/dist/spec/db/spec-node.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +13 -0
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
- package/dist/spec/evolve/impact-locator.js +25 -0
- package/dist/spec/evolve/impact-locator.js.map +1 -0
- package/dist/spec/evolve/llm-client.d.ts +50 -0
- package/dist/spec/evolve/llm-client.d.ts.map +1 -0
- package/dist/spec/evolve/llm-client.js +176 -0
- package/dist/spec/evolve/llm-client.js.map +1 -0
- package/dist/spec/evolve/logic-checker.d.ts +12 -0
- package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
- package/dist/spec/evolve/logic-checker.js +24 -0
- package/dist/spec/evolve/logic-checker.js.map +1 -0
- package/dist/spec/evolve/pipeline.d.ts +42 -0
- package/dist/spec/evolve/pipeline.d.ts.map +1 -0
- package/dist/spec/evolve/pipeline.js +567 -0
- package/dist/spec/evolve/pipeline.js.map +1 -0
- package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
- package/dist/spec/evolve/spec-rewriter.js +230 -0
- package/dist/spec/evolve/spec-rewriter.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +155 -0
- package/dist/spec/graph/queries.d.ts.map +1 -0
- package/dist/spec/graph/queries.js +440 -0
- package/dist/spec/graph/queries.js.map +1 -0
- package/dist/spec/llm/client.d.ts +29 -0
- package/dist/spec/llm/client.d.ts.map +1 -0
- package/dist/spec/llm/client.js +123 -0
- package/dist/spec/llm/client.js.map +1 -0
- package/dist/spec/llm/index.d.ts +3 -0
- package/dist/spec/llm/index.d.ts.map +1 -0
- package/dist/spec/llm/index.js +11 -0
- package/dist/spec/llm/index.js.map +1 -0
- package/dist/spec/llm/prompts.d.ts +13 -0
- package/dist/spec/llm/prompts.d.ts.map +1 -0
- package/dist/spec/llm/prompts.js +75 -0
- package/dist/spec/llm/prompts.js.map +1 -0
- package/dist/spec/mining/diff-parser.d.ts +33 -0
- package/dist/spec/mining/diff-parser.d.ts.map +1 -0
- package/dist/spec/mining/diff-parser.js +166 -0
- package/dist/spec/mining/diff-parser.js.map +1 -0
- package/dist/spec/mining/git-scanner.d.ts +103 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -0
- package/dist/spec/mining/git-scanner.js +307 -0
- package/dist/spec/mining/git-scanner.js.map +1 -0
- package/dist/spec/mining/pipeline.d.ts +53 -0
- package/dist/spec/mining/pipeline.d.ts.map +1 -0
- package/dist/spec/mining/pipeline.js +178 -0
- package/dist/spec/mining/pipeline.js.map +1 -0
- package/dist/spec/mining/scope-resolver.d.ts +45 -0
- package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
- package/dist/spec/mining/scope-resolver.js +103 -0
- package/dist/spec/mining/scope-resolver.js.map +1 -0
- package/dist/spec/mining/spec-extractor.d.ts +69 -0
- package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
- package/dist/spec/mining/spec-extractor.js +369 -0
- package/dist/spec/mining/spec-extractor.js.map +1 -0
- package/dist/spec/types.d.ts +149 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +15 -0
- package/dist/spec/types.js.map +1 -0
- package/dist/spec/utils.d.ts +167 -0
- package/dist/spec/utils.d.ts.map +1 -0
- package/dist/spec/utils.js +463 -0
- package/dist/spec/utils.js.map +1 -0
- package/dist/sync/worktree.d.ts +9 -0
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +40 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/shimmer-progress.d.ts +2 -0
- package/dist/ui/shimmer-progress.d.ts.map +1 -1
- package/dist/ui/shimmer-progress.js +19 -2
- package/dist/ui/shimmer-progress.js.map +1 -1
- package/dist/upgrade/index.js +1 -1
- package/dist/upgrade/index.js.map +1 -1
- package/package.json +58 -57
- package/scripts/add-lang/bench.sh +60 -60
- package/scripts/add-lang/check-grammar.mjs +75 -75
- package/scripts/add-lang/dump-ast.mjs +103 -103
- package/scripts/add-lang/verify-extraction.mjs +70 -70
- package/scripts/agent-eval/ab-adoption.sh +91 -91
- package/scripts/agent-eval/ab-hook.sh +86 -86
- package/scripts/agent-eval/ab-impl.sh +78 -78
- package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
- package/scripts/agent-eval/ab-sufficiency.sh +78 -78
- package/scripts/agent-eval/arms-F.sh +21 -21
- package/scripts/agent-eval/arms-matrix.sh +37 -37
- package/scripts/agent-eval/audit.sh +68 -68
- package/scripts/agent-eval/bench-readme.sh +28 -28
- package/scripts/agent-eval/bench-why-repo.sh +22 -22
- package/scripts/agent-eval/block-read-hook.sh +19 -19
- package/scripts/agent-eval/hook-settings.json +15 -15
- package/scripts/agent-eval/itrun.sh +120 -120
- package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
- package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
- package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
- package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
- package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
- package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
- package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
- package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
- package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
- package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
- package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
- package/scripts/agent-eval/offload-eval-setup.sh +24 -24
- package/scripts/agent-eval/offload-eval-styles.sh +71 -71
- package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
- package/scripts/agent-eval/offload-eval.md +76 -76
- package/scripts/agent-eval/parse-arms.mjs +116 -116
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
- package/scripts/agent-eval/parse-run.mjs +45 -45
- package/scripts/agent-eval/parse-session.mjs +93 -93
- package/scripts/agent-eval/probe-context.mjs +21 -21
- package/scripts/agent-eval/probe-explore.mjs +40 -40
- package/scripts/agent-eval/probe-node.mjs +20 -20
- package/scripts/agent-eval/probe-sweep.mjs +119 -119
- package/scripts/agent-eval/probe-trace.mjs +20 -20
- package/scripts/agent-eval/redirect-read-hook.sh +38 -38
- 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 -34
- package/scripts/agent-eval/run-all.sh +69 -69
- package/scripts/agent-eval/run-arms.sh +56 -56
- package/scripts/agent-eval/seq-matrix.mjs +137 -137
- package/scripts/build-bundle.sh +118 -118
- package/scripts/exp_boundary_eval/README.md +247 -0
- package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
- package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
- package/scripts/exp_boundary_eval/_utils.py +1116 -0
- package/scripts/exp_boundary_eval/analyze.py +1313 -0
- package/scripts/exp_boundary_eval/data/agents.json +109 -0
- package/scripts/exp_boundary_eval/data/experiments.json +140 -0
- package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
- package/scripts/exp_boundary_eval/run_all.py +378 -0
- package/scripts/exp_boundary_eval/run_one.py +165 -0
- package/scripts/exp_boundary_eval/run_session.py +158 -0
- package/scripts/exp_boundary_eval/setup.py +120 -0
- package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
- package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
- package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
- package/scripts/extract-release-notes.mjs +130 -130
- package/scripts/local-install.sh +41 -41
- package/scripts/npm-sdk.js +75 -75
- package/scripts/npm-shim.js +268 -246
- package/scripts/ohos-sdk-publish.mjs +133 -0
- package/scripts/pack-npm.sh +119 -119
- package/scripts/prepare-release.mjs +270 -270
- package/scripts/qa_eval/README.md +407 -404
- package/scripts/qa_eval/_test_deveco_probe.py +41 -41
- package/scripts/qa_eval/agent_runner.py +526 -526
- package/scripts/qa_eval/data/.gitignore +4 -4
- package/scripts/qa_eval/data/test-set.jsonl +2 -22
- package/scripts/qa_eval/eval_metrics.py +274 -233
- package/scripts/qa_eval/external_agent.py +976 -671
- package/scripts/qa_eval/llm_config.py +92 -92
- package/scripts/qa_eval/memory_monitor.py +132 -132
- package/scripts/qa_eval/my_answer_accuracy.py +187 -187
- package/scripts/qa_eval/requirements.txt +2 -2
- package/scripts/qa_eval/run_pipeline.py +804 -711
- package/scripts/qa_eval/stats_efficiency.py +279 -279
- package/scripts/qa_eval/stats_scores.py +207 -207
package/dist/extraction/index.js
CHANGED
|
@@ -51,9 +51,11 @@ exports.scanDirectoryAsync = scanDirectoryAsync;
|
|
|
51
51
|
const fs = __importStar(require("fs"));
|
|
52
52
|
const fsp = __importStar(require("fs/promises"));
|
|
53
53
|
const path = __importStar(require("path"));
|
|
54
|
+
const os = __importStar(require("os"));
|
|
54
55
|
const crypto = __importStar(require("crypto"));
|
|
55
56
|
const child_process_1 = require("child_process");
|
|
56
57
|
const tree_sitter_1 = require("./tree-sitter");
|
|
58
|
+
const parse_pool_1 = require("./parse-pool");
|
|
57
59
|
const grammars_1 = require("./grammars");
|
|
58
60
|
const project_config_1 = require("../project-config");
|
|
59
61
|
const directory_1 = require("../directory");
|
|
@@ -61,6 +63,8 @@ const errors_1 = require("../errors");
|
|
|
61
63
|
const utils_1 = require("../utils");
|
|
62
64
|
const ignore_1 = __importDefault(require("ignore"));
|
|
63
65
|
const frameworks_1 = require("../resolution/frameworks");
|
|
66
|
+
const context_1 = require("./context");
|
|
67
|
+
const arkts_1 = require("./languages/arkts");
|
|
64
68
|
/**
|
|
65
69
|
* Number of files to read in parallel during indexing.
|
|
66
70
|
* File reads are I/O-bound; batching overlaps I/O wait with CPU parse work.
|
|
@@ -155,12 +159,34 @@ const DEFAULT_IGNORE_DIRS = new Set([
|
|
|
155
159
|
// Generic cache
|
|
156
160
|
'.cache',
|
|
157
161
|
]);
|
|
162
|
+
/**
|
|
163
|
+
* Android resource directory types. A `res/` tree holds ONLY non-code resources —
|
|
164
|
+
* layouts, drawables, value bags (strings/colors/styles), menus, navigation
|
|
165
|
+
* graphs — split into one typed subdirectory per kind, optionally density/locale/
|
|
166
|
+
* version-qualified (`values-es`, `drawable-hdpi`, `layout-v21`, …). None of it
|
|
167
|
+
* yields an extractable code symbol, yet on an Android app it DOMINATES the tree
|
|
168
|
+
* (one report: 26k XML files = 97% of the project, 0 symbols), bloating the DB,
|
|
169
|
+
* slowing indexing, and skewing both the file count and `codegraph_explore`
|
|
170
|
+
* results (#1047). So these are excluded by default. The structure is
|
|
171
|
+
* self-identifying — a non-Android project has no `res/layout/` etc., so it's
|
|
172
|
+
* untouched — and the only XML that DOES produce symbols (MyBatis mappers) lives
|
|
173
|
+
* under `src/main/resources/`, never `res/`, so nothing useful is dropped.
|
|
174
|
+
* `res/raw/` is deliberately NOT here: it holds arbitrary bundled assets that can
|
|
175
|
+
* be code-ish (a `.sql` schema, a `.js`), so we leave it indexed. Override any of
|
|
176
|
+
* these with a `.gitignore` negation (e.g. `!res/values/`).
|
|
177
|
+
*/
|
|
178
|
+
const ANDROID_RES_TYPES = [
|
|
179
|
+
'anim', 'animator', 'color', 'drawable', 'font', 'layout',
|
|
180
|
+
'menu', 'mipmap', 'navigation', 'transition', 'values', 'xml',
|
|
181
|
+
];
|
|
158
182
|
/** Gitignore-style patterns for the `ignore` matcher: the dirs above plus a few globs. */
|
|
159
183
|
const DEFAULT_IGNORE_PATTERNS = [
|
|
160
184
|
...Array.from(DEFAULT_IGNORE_DIRS, (d) => `${d}/`),
|
|
161
185
|
'*.egg-info/', // Python packaging metadata
|
|
162
186
|
'cmake-build-*/', // CLion / CMake build trees
|
|
163
187
|
'bazel-*/', // Bazel output symlink trees
|
|
188
|
+
// Android resource dirs at any depth, with their qualifier variants (#1047).
|
|
189
|
+
...ANDROID_RES_TYPES.map((t) => `**/res/${t}*/`),
|
|
164
190
|
];
|
|
165
191
|
/** True if `buf` decodes as strict UTF-8 (no invalid byte sequences). */
|
|
166
192
|
function isValidUtf8(buf) {
|
|
@@ -265,6 +291,19 @@ function loadIncludeIgnoredMatcher(rootDir) {
|
|
|
265
291
|
const patterns = (0, project_config_1.loadIncludeIgnoredPatterns)(rootDir);
|
|
266
292
|
return patterns.length > 0 ? (0, ignore_1.default)().add(patterns) : null;
|
|
267
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Matcher for the project's `codegraph.json` `exclude` patterns — paths to keep
|
|
296
|
+
* OUT of the index even when git-tracked, which `.gitignore` cannot do (#999).
|
|
297
|
+
* The escape hatch for a committed vendor/theme/SDK directory. Returns `null`
|
|
298
|
+
* when nothing is excluded (the zero-config default → no overhead). Matched
|
|
299
|
+
* against project-root-relative paths, so it applies uniformly across the whole
|
|
300
|
+
* workspace, including inside embedded repos (excluding `static/` means gone
|
|
301
|
+
* everywhere). Built once per scan/sync/scope operation from the scan root.
|
|
302
|
+
*/
|
|
303
|
+
function loadExcludeMatcher(rootDir) {
|
|
304
|
+
const patterns = (0, project_config_1.loadExcludePatterns)(rootDir);
|
|
305
|
+
return patterns.length > 0 ? (0, ignore_1.default)().add(patterns) : null;
|
|
306
|
+
}
|
|
268
307
|
/**
|
|
269
308
|
* `git ls-files --directory` collapses a wholly-untracked/ignored directory into
|
|
270
309
|
* one entry — and when the command's own cwd is such a directory (the indexed
|
|
@@ -404,14 +443,27 @@ function findNestedGitRepos(absDir, relPrefix) {
|
|
|
404
443
|
*/
|
|
405
444
|
class ScopeIgnore {
|
|
406
445
|
rootMatcher;
|
|
446
|
+
exclude;
|
|
407
447
|
embedded;
|
|
408
448
|
defaults = defaultsOnlyIgnore();
|
|
409
|
-
constructor(rootMatcher, embedded
|
|
449
|
+
constructor(rootMatcher, embedded,
|
|
450
|
+
/**
|
|
451
|
+
* Project `codegraph.json` `exclude` patterns (#999), matched against the
|
|
452
|
+
* full root-relative path. Wins over everything else — an explicit user
|
|
453
|
+
* exclude applies even to tracked files and even inside embedded repos.
|
|
454
|
+
*/
|
|
455
|
+
exclude = null) {
|
|
410
456
|
this.rootMatcher = rootMatcher;
|
|
457
|
+
this.exclude = exclude;
|
|
411
458
|
// Longest root first so paths in nested embedded repos hit the innermost matcher.
|
|
412
459
|
this.embedded = [...embedded].sort((a, b) => b.root.length - a.root.length);
|
|
413
460
|
}
|
|
414
461
|
ignores(rel) {
|
|
462
|
+
// User `exclude` (#999) is checked first and against the full root-relative
|
|
463
|
+
// path: it must drop git-TRACKED paths (which `.gitignore` can't) and apply
|
|
464
|
+
// everywhere, including ancestors of embedded repos.
|
|
465
|
+
if (this.exclude && this.exclude.ignores(rel))
|
|
466
|
+
return true;
|
|
415
467
|
for (const { root, matcher } of this.embedded) {
|
|
416
468
|
if (rel.startsWith(root)) {
|
|
417
469
|
const inner = rel.slice(root.length);
|
|
@@ -438,7 +490,36 @@ exports.ScopeIgnore = ScopeIgnore;
|
|
|
438
490
|
*/
|
|
439
491
|
function buildScopeIgnore(rootDir, embeddedRoots) {
|
|
440
492
|
const roots = embeddedRoots ? [...embeddedRoots] : discoverEmbeddedRepoRoots(rootDir);
|
|
441
|
-
return new ScopeIgnore(buildDefaultIgnore(rootDir), roots.map((root) => ({ root, matcher: buildDefaultIgnore(path.join(rootDir, root)) })));
|
|
493
|
+
return new ScopeIgnore(buildDefaultIgnore(rootDir), roots.map((root) => ({ root, matcher: buildDefaultIgnore(path.join(rootDir, root)) })), loadExcludeMatcher(rootDir));
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Whether an embedded repo found as a tracked gitlink (mode 160000, #1031/#1033)
|
|
497
|
+
* must be SKIPPED rather than indexed. A gitlink is tracked, so `.gitignore`
|
|
498
|
+
* can't untrack it — but the discovery passes for it must still honor the same
|
|
499
|
+
* scope rules as every other path, or a gitignored reference/data dir full of
|
|
500
|
+
* `git add`ed clones gets pulled into the index against the user's stated intent
|
|
501
|
+
* (#1065). Two reasons to skip:
|
|
502
|
+
* 1. It sits in a built-in default-ignored location — an npm git-dependency
|
|
503
|
+
* under `node_modules` is never project code; not even an explicit opt-in
|
|
504
|
+
* revives it (matches `findIgnoredEmbeddedRepos`).
|
|
505
|
+
* 2. The parent repo's own `.gitignore` covers its path and the project did
|
|
506
|
+
* NOT opt that path in via `codegraph.json` `includeIgnored`. The gitignore
|
|
507
|
+
* rule is the user's stated intent to keep that path out of scope, exactly
|
|
508
|
+
* as for an UNtracked embedded repo — respect it by default, opt back in
|
|
509
|
+
* with `includeIgnored` (#514, #970, #976).
|
|
510
|
+
* `relDir` is repoDir-relative (trailing-slashed); `prefix` is repoDir's
|
|
511
|
+
* scan-root-relative path so the `includeIgnored` pattern is matched on the full
|
|
512
|
+
* scan-root-relative path. `defaults` is `defaultsOnlyIgnore()` and `repoIgnore`
|
|
513
|
+
* is `buildDefaultIgnore(repoDir)` (defaults + the repo's own `.gitignore`),
|
|
514
|
+
* both passed in so they're built once per repo level rather than per gitlink.
|
|
515
|
+
*/
|
|
516
|
+
function gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored) {
|
|
517
|
+
if (defaults.ignores(relDir))
|
|
518
|
+
return true; // default-ignored — never index, opt-in can't revive
|
|
519
|
+
if (!repoIgnore.ignores(relDir))
|
|
520
|
+
return false; // not ignored at all — index as before (#1031/#1033)
|
|
521
|
+
// Gitignored by the repo's own rules — skip unless the project opted it in.
|
|
522
|
+
return !includeIgnored?.ignores((0, utils_1.normalizePath)(prefix + relDir));
|
|
442
523
|
}
|
|
443
524
|
/**
|
|
444
525
|
* Standalone discovery of every embedded repo root under `rootDir` (relative,
|
|
@@ -470,6 +551,25 @@ function discoverEmbeddedRepoRoots(rootDir) {
|
|
|
470
551
|
}
|
|
471
552
|
}
|
|
472
553
|
catch { /* untracked listing failed — ignored-side discovery still runs */ }
|
|
554
|
+
// Unexpanded gitlinks (mode 160000) with a real checkout on disk — embedded
|
|
555
|
+
// repos `git add`ed without `.gitmodules`, or submodules not active here. The
|
|
556
|
+
// untracked listing above can't see them (they're tracked), so find them the
|
|
557
|
+
// same way collectGitFiles does, keeping watcher scope == indexer scope.
|
|
558
|
+
// (#1031, #1033)
|
|
559
|
+
try {
|
|
560
|
+
const gitOpts = { cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
|
|
561
|
+
const repoIgnore = buildDefaultIgnore(repoAbs);
|
|
562
|
+
for (const rel of listGitlinkRels(gitOpts)) {
|
|
563
|
+
const relDir = rel.endsWith('/') ? rel : rel + '/';
|
|
564
|
+
// A gitlink under a gitignored path is respected (not indexed) unless the
|
|
565
|
+
// project opted it in — same rule as the untracked-ignored kind (#1065).
|
|
566
|
+
if (gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored))
|
|
567
|
+
continue;
|
|
568
|
+
if (classifyGitDir(path.join(repoAbs, rel)) === 'embedded')
|
|
569
|
+
candidates.push(relDir);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch { /* staged listing failed — other discovery still runs */ }
|
|
473
573
|
candidates.push(...findIgnoredEmbeddedRepos(repoAbs, includeIgnored, prefix));
|
|
474
574
|
for (const rel of candidates) {
|
|
475
575
|
const full = (0, utils_1.normalizePath)(prefix + rel);
|
|
@@ -510,24 +610,65 @@ function findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix) {
|
|
|
510
610
|
return repos;
|
|
511
611
|
}
|
|
512
612
|
/**
|
|
513
|
-
* Collect
|
|
514
|
-
* git repository rooted at `repoDir`, adding each to `files` with `prefix`
|
|
515
|
-
* prepended so paths stay relative to the original scan root.
|
|
613
|
+
* Collect tracked paths and unexpanded gitlinks (mode 160000) from `git ls-files`.
|
|
516
614
|
*
|
|
517
|
-
*
|
|
518
|
-
* (
|
|
519
|
-
*
|
|
520
|
-
* skips them entirely, and untracked output reports them only as an opaque
|
|
521
|
-
* "subdir/" entry (trailing slash) rather than expanding their files. Each
|
|
522
|
-
* embedded repo is its own git boundary, so we re-run `git ls-files` inside it.
|
|
523
|
-
* (See issue #193.) GITIGNORED embedded repos are invisible even to that; they
|
|
524
|
-
* are discovered separately via `findIgnoredEmbeddedRepos` (#514) but ONLY for
|
|
525
|
-
* directories the project opted in through `homegraph.json` `includeIgnored`
|
|
526
|
-
* (`includeIgnored` here, threaded from the scan root) — by default `.gitignore`
|
|
527
|
-
* is respected and they stay out (#970, #976). Every embedded repo root (however
|
|
528
|
-
* found) is recorded in `embeddedRoots` so callers can exempt its files from the
|
|
529
|
-
* parent's own gitignore rules.
|
|
615
|
+
* Prefer one `-s --recurse-submodules` pass (git ≥2.38). Older git rejects that
|
|
616
|
+
* combo ("unsupported mode"), so fall back to `-c --recurse-submodules` for
|
|
617
|
+
* expanded submodule files plus a plain `-s` pass for gitlink detection (#1031).
|
|
530
618
|
*/
|
|
619
|
+
function listTrackedGitFiles(gitOpts, prefix, files) {
|
|
620
|
+
const gitlinkRels = [];
|
|
621
|
+
const ingestStaged = (tracked) => {
|
|
622
|
+
for (const entry of tracked.split('\0')) {
|
|
623
|
+
if (!entry)
|
|
624
|
+
continue;
|
|
625
|
+
const tab = entry.indexOf('\t');
|
|
626
|
+
if (tab === -1)
|
|
627
|
+
continue; // --stage always emits "<mode> <object> <stage>\t<path>"
|
|
628
|
+
const rel = entry.slice(tab + 1);
|
|
629
|
+
if (entry.slice(0, 6) === '160000') {
|
|
630
|
+
gitlinkRels.push(rel); // an unexpanded gitlink — recursed into below, not a source file itself
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
files.add((0, utils_1.normalizePath)(prefix + rel));
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
try {
|
|
637
|
+
ingestStaged((0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], gitOpts));
|
|
638
|
+
return gitlinkRels;
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
// Older git: submodule expansion and gitlink mode detection need two passes.
|
|
642
|
+
const cached = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-c', '--recurse-submodules'], gitOpts);
|
|
643
|
+
for (const rel of cached.split('\0')) {
|
|
644
|
+
if (rel)
|
|
645
|
+
files.add((0, utils_1.normalizePath)(prefix + rel));
|
|
646
|
+
}
|
|
647
|
+
ingestStaged((0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s'], gitOpts));
|
|
648
|
+
return gitlinkRels;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/** Unexpanded gitlink paths (mode 160000) from `git ls-files --stage`. */
|
|
652
|
+
function listGitlinkRels(gitOpts) {
|
|
653
|
+
const rels = [];
|
|
654
|
+
const ingest = (staged) => {
|
|
655
|
+
for (const entry of staged.split('\0')) {
|
|
656
|
+
if (!entry || entry.slice(0, 6) !== '160000')
|
|
657
|
+
continue;
|
|
658
|
+
const tab = entry.indexOf('\t');
|
|
659
|
+
if (tab === -1)
|
|
660
|
+
continue;
|
|
661
|
+
rels.push(entry.slice(tab + 1));
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
try {
|
|
665
|
+
ingest((0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], gitOpts));
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
ingest((0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s'], gitOpts));
|
|
669
|
+
}
|
|
670
|
+
return rels;
|
|
671
|
+
}
|
|
531
672
|
function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored = null) {
|
|
532
673
|
const gitOpts = { cwd: repoDir, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
|
|
533
674
|
// Tracked files. --recurse-submodules pulls in files from active submodules,
|
|
@@ -535,15 +676,24 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
|
|
|
535
676
|
// Without this, monorepos using submodules index 0 files. (See issue #147.)
|
|
536
677
|
// Note: --recurse-submodules only supports -c/--cached and --stage modes — it
|
|
537
678
|
// can't be combined with -o, so untracked files are gathered separately below.
|
|
679
|
+
//
|
|
680
|
+
// We use --stage (-s) rather than -c so each entry carries its file mode. That
|
|
681
|
+
// lets us spot gitlink entries (mode 160000) that --recurse-submodules did NOT
|
|
682
|
+
// expand: a nested repo `git add`ed without a `.gitmodules` entry, or a
|
|
683
|
+
// submodule that isn't active/initialized in this checkout. Such a gitlink
|
|
684
|
+
// falls through every pass — it's tracked, so the untracked `-o` listing below
|
|
685
|
+
// never reports it, and --recurse-submodules only expands ACTIVE submodules —
|
|
686
|
+
// so its source would be silently skipped, leaving only the super-repo's own
|
|
687
|
+
// files indexed. We collect those gitlinks here and recurse into them below.
|
|
688
|
+
// (An active submodule is expanded inline by --recurse-submodules and so never
|
|
689
|
+
// surfaces as a 160000 entry — only the unhandled gitlinks do.) (#1031, #1033)
|
|
690
|
+
//
|
|
538
691
|
// -z gives NUL-separated, unquoted output so non-ASCII (e.g. CJK) paths
|
|
539
692
|
// survive verbatim. Without it git octal-escapes and double-quotes such paths
|
|
540
693
|
// (the core.quotepath default), and the quoted form never matches a real file
|
|
541
|
-
// on disk → those files are silently dropped from the index. (#541)
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (rel)
|
|
545
|
-
files.add((0, utils_1.normalizePath)(prefix + rel));
|
|
546
|
-
}
|
|
694
|
+
// on disk → those files are silently dropped from the index. (#541) With -s the
|
|
695
|
+
// path follows a TAB after the `<mode> <object> <stage>` prefix.
|
|
696
|
+
const gitlinkRels = listTrackedGitFiles(gitOpts, prefix, files);
|
|
547
697
|
// Untracked files (submodules manage their own untracked state). Embedded git
|
|
548
698
|
// repos surface here as a single "subdir/" entry that git refuses to descend
|
|
549
699
|
// into — recurse into those as their own repos so their source gets indexed.
|
|
@@ -568,6 +718,32 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
|
|
|
568
718
|
}
|
|
569
719
|
files.add((0, utils_1.normalizePath)(prefix + rel));
|
|
570
720
|
}
|
|
721
|
+
// Gitlink entries (mode 160000) that --recurse-submodules left unexpanded —
|
|
722
|
+
// an embedded repo `git add`ed without `.gitmodules`, or a submodule not
|
|
723
|
+
// active/initialized in this checkout. When such a gitlink has a real working
|
|
724
|
+
// tree on disk it is distinct first-party code we must index as its own
|
|
725
|
+
// embedded repo: the tracked pass skipped its contents and the untracked pass
|
|
726
|
+
// never sees it (it's tracked, not "other"). A gitlink with no checkout on disk
|
|
727
|
+
// (an uninitialized submodule — empty dir, no `.git`) has nothing to index and
|
|
728
|
+
// is left alone, as is a submodule worktree (a duplicate view, #945). (#1031, #1033)
|
|
729
|
+
if (gitlinkRels.length > 0) {
|
|
730
|
+
const defaults = defaultsOnlyIgnore();
|
|
731
|
+
const repoIgnore = buildDefaultIgnore(repoDir);
|
|
732
|
+
for (const rel of gitlinkRels) {
|
|
733
|
+
const relDir = rel.endsWith('/') ? rel : rel + '/';
|
|
734
|
+
// A gitlink under a gitignored path is respected (not indexed) unless the
|
|
735
|
+
// project opted it in via `includeIgnored` — keep tracked gitlinks under
|
|
736
|
+
// the same scope rule as the untracked-ignored kind below (#1065).
|
|
737
|
+
if (gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored))
|
|
738
|
+
continue;
|
|
739
|
+
const childDir = path.join(repoDir, rel);
|
|
740
|
+
// 'embedded' = a real .git checkout on disk; 'worktree' and 'none' are skipped.
|
|
741
|
+
if (classifyGitDir(childDir) !== 'embedded')
|
|
742
|
+
continue;
|
|
743
|
+
embeddedRoots?.add((0, utils_1.normalizePath)(prefix + relDir));
|
|
744
|
+
collectGitFiles(childDir, prefix + relDir, files, embeddedRoots, includeIgnored);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
571
747
|
// Embedded repos hidden by THIS repo's ignore rules (`/packages/` in a
|
|
572
748
|
// super-repo .gitignore) never appear in any listing above. By default they
|
|
573
749
|
// stay hidden — `.gitignore` is respected (#970, #976). They are recursed into
|
|
@@ -637,14 +813,14 @@ function getGitChangedFiles(rootDir) {
|
|
|
637
813
|
// Custom extension → language overrides from the project's homegraph.json,
|
|
638
814
|
// so change detection sees the same custom-extension files the full index does.
|
|
639
815
|
const overrides = (0, project_config_1.loadExtensionOverrides)(rootDir);
|
|
640
|
-
collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir));
|
|
816
|
+
collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir));
|
|
641
817
|
return changes;
|
|
642
818
|
}
|
|
643
819
|
catch {
|
|
644
820
|
return null;
|
|
645
821
|
}
|
|
646
822
|
}
|
|
647
|
-
function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null) {
|
|
823
|
+
function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null, exclude = null) {
|
|
648
824
|
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 });
|
|
649
825
|
// This repo's own ignore rules — built-in defaults (#407) plus its .gitignore.
|
|
650
826
|
// Change detection must exclude the SAME files the full index does, but git
|
|
@@ -683,6 +859,12 @@ function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null
|
|
|
683
859
|
// index — match against the repo-relative path, same as the full scan. (#766)
|
|
684
860
|
if (ig.ignores(rel))
|
|
685
861
|
continue;
|
|
862
|
+
// User `codegraph.json` `exclude` (#999) is project-root-relative, so it's
|
|
863
|
+
// matched against the full path — sync must not re-add a tracked file the
|
|
864
|
+
// full index now keeps out. Deletions above stay unfiltered so a file that
|
|
865
|
+
// WAS indexed before an exclude was added still cleans itself out.
|
|
866
|
+
if (exclude && exclude.ignores(filePath))
|
|
867
|
+
continue;
|
|
686
868
|
if (statusCode === '??') {
|
|
687
869
|
out.added.push(filePath);
|
|
688
870
|
}
|
|
@@ -697,11 +879,11 @@ function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null
|
|
|
697
879
|
// and they are left alone (#970, #976), mirroring the full-index scan.
|
|
698
880
|
for (const rel of untrackedDirs) {
|
|
699
881
|
for (const repoRel of findNestedGitRepos(path.join(repoDir, rel), rel)) {
|
|
700
|
-
collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, overrides, includeIgnored);
|
|
882
|
+
collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, overrides, includeIgnored, exclude);
|
|
701
883
|
}
|
|
702
884
|
}
|
|
703
885
|
for (const rel of findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix)) {
|
|
704
|
-
collectGitStatus(path.join(repoDir, rel), prefix + rel, out, overrides, includeIgnored);
|
|
886
|
+
collectGitStatus(path.join(repoDir, rel), prefix + rel, out, overrides, includeIgnored, exclude);
|
|
705
887
|
}
|
|
706
888
|
}
|
|
707
889
|
/**
|
|
@@ -860,7 +1042,14 @@ function scanDirectoryWalk(rootDir, onProgress) {
|
|
|
860
1042
|
}
|
|
861
1043
|
// Seed a base matcher with the built-in default ignores (merged with the root
|
|
862
1044
|
// .gitignore so a negation can override). Nested .gitignores still layer per-dir.
|
|
863
|
-
|
|
1045
|
+
const baseMatchers = [{ dir: rootDir, ig: buildDefaultIgnore(rootDir) }];
|
|
1046
|
+
// Project `codegraph.json` `exclude` patterns (#999), rooted at the project so
|
|
1047
|
+
// `isIgnored` matches them against root-relative paths — same coverage the
|
|
1048
|
+
// git path gets via ScopeIgnore, for non-git projects.
|
|
1049
|
+
const exclude = loadExcludeMatcher(rootDir);
|
|
1050
|
+
if (exclude)
|
|
1051
|
+
baseMatchers.push({ dir: rootDir, ig: exclude });
|
|
1052
|
+
walk(rootDir, baseMatchers);
|
|
864
1053
|
return files;
|
|
865
1054
|
}
|
|
866
1055
|
/**
|
|
@@ -992,268 +1181,101 @@ class ExtractionOrchestrator {
|
|
|
992
1181
|
// between runs is picked up without restarting the process.
|
|
993
1182
|
this.detectedFrameworkNames = null;
|
|
994
1183
|
const frameworkNames = this.ensureDetectedFrameworks(files);
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
errors: [{ message: 'Aborted', severity: 'error' }],
|
|
1004
|
-
durationMs: Date.now() - startTime,
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
// Phase 2: Parse files in a worker thread (keeps main thread unblocked for UI)
|
|
1008
|
-
const total = files.length;
|
|
1009
|
-
let processed = 0;
|
|
1010
|
-
// Emit parsing phase immediately so the progress bar appears during worker setup.
|
|
1011
|
-
// The yield lets the shimmer worker flush the phase transition to stdout before
|
|
1012
|
-
// the main thread starts synchronous grammar detection work.
|
|
1013
|
-
onProgress?.({
|
|
1014
|
-
phase: 'parsing',
|
|
1015
|
-
current: 0,
|
|
1016
|
-
total,
|
|
1017
|
-
});
|
|
1018
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
1019
|
-
// Detect needed languages and load grammars in the parse worker
|
|
1020
|
-
const neededLanguages = [...new Set(files.map((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides)))];
|
|
1021
|
-
// .h files default to 'c' but may be C++ — ensure cpp grammar is loaded when c is needed
|
|
1022
|
-
if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
|
|
1023
|
-
neededLanguages.push('cpp');
|
|
1024
|
-
}
|
|
1025
|
-
// Try to use a worker thread for parsing (keeps main thread unblocked for UI).
|
|
1026
|
-
// Falls back to in-process parsing if the compiled worker is unavailable (e.g. tests).
|
|
1027
|
-
const parseWorkerPath = path.join(__dirname, 'parse-worker.js');
|
|
1028
|
-
const useWorker = fs.existsSync(parseWorkerPath);
|
|
1029
|
-
let WorkerClass = null;
|
|
1030
|
-
if (useWorker) {
|
|
1031
|
-
const { Worker } = await Promise.resolve().then(() => __importStar(require('worker_threads')));
|
|
1032
|
-
WorkerClass = Worker;
|
|
1033
|
-
}
|
|
1034
|
-
else {
|
|
1035
|
-
// In-process fallback: load grammars locally
|
|
1036
|
-
await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
|
|
1037
|
-
}
|
|
1038
|
-
// --- Worker lifecycle management ---
|
|
1039
|
-
// The worker can crash (OOM in WASM) or hang on pathological files.
|
|
1040
|
-
// We track pending parse promises and handle both cases:
|
|
1041
|
-
// - Timeout: terminate + restart the worker, reject the timed-out request
|
|
1042
|
-
// - Crash: reject all pending promises, restart for remaining files
|
|
1043
|
-
let parseWorker = null;
|
|
1044
|
-
let nextId = 0;
|
|
1045
|
-
let workerParseCount = 0;
|
|
1046
|
-
const pendingParses = new Map();
|
|
1047
|
-
function rejectAllPending(reason) {
|
|
1048
|
-
for (const [id, pending] of pendingParses) {
|
|
1049
|
-
clearTimeout(pending.timer);
|
|
1050
|
-
pendingParses.delete(id);
|
|
1051
|
-
pending.reject(new Error(reason));
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
function attachWorkerHandlers(w) {
|
|
1055
|
-
w.on('message', (msg) => {
|
|
1056
|
-
if (msg.type === 'parse-result' && msg.id !== undefined) {
|
|
1057
|
-
const pending = pendingParses.get(msg.id);
|
|
1058
|
-
if (pending) {
|
|
1059
|
-
clearTimeout(pending.timer);
|
|
1060
|
-
pendingParses.delete(msg.id);
|
|
1061
|
-
pending.resolve(msg.result);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
});
|
|
1065
|
-
w.on('error', (err) => {
|
|
1066
|
-
(0, errors_1.logWarn)('Parse worker error', { error: err.message });
|
|
1067
|
-
rejectAllPending(`Worker error: ${err.message}`);
|
|
1068
|
-
});
|
|
1069
|
-
w.on('exit', (code) => {
|
|
1070
|
-
if (code !== 0 && pendingParses.size > 0) {
|
|
1071
|
-
(0, errors_1.logWarn)('Parse worker exited unexpectedly', { code });
|
|
1072
|
-
rejectAllPending(`Worker exited with code ${code}`);
|
|
1073
|
-
}
|
|
1074
|
-
// Clear reference so we know to respawn, reset count so
|
|
1075
|
-
// the fresh worker gets a full cycle before recycling.
|
|
1076
|
-
if (parseWorker === w) {
|
|
1077
|
-
parseWorker = null;
|
|
1078
|
-
workerParseCount = 0;
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
async function ensureWorker() {
|
|
1083
|
-
if (parseWorker)
|
|
1084
|
-
return parseWorker;
|
|
1085
|
-
log('Spawning new parse worker...');
|
|
1086
|
-
parseWorker = new WorkerClass(parseWorkerPath);
|
|
1087
|
-
attachWorkerHandlers(parseWorker);
|
|
1088
|
-
// Load grammars in the new worker
|
|
1089
|
-
await new Promise((resolve, reject) => {
|
|
1090
|
-
parseWorker.once('message', (msg) => {
|
|
1091
|
-
if (msg.type === 'grammars-loaded')
|
|
1092
|
-
resolve();
|
|
1093
|
-
else
|
|
1094
|
-
reject(new Error(`Unexpected message: ${msg.type}`));
|
|
1095
|
-
});
|
|
1096
|
-
parseWorker.postMessage({ type: 'load-grammars', languages: neededLanguages });
|
|
1097
|
-
});
|
|
1098
|
-
return parseWorker;
|
|
1099
|
-
}
|
|
1100
|
-
if (WorkerClass) {
|
|
1101
|
-
await ensureWorker();
|
|
1102
|
-
}
|
|
1103
|
-
/**
|
|
1104
|
-
* Recycle the worker thread to reclaim WASM memory.
|
|
1105
|
-
* Terminates the current worker and clears the reference so
|
|
1106
|
-
* ensureWorker() will spawn a fresh one on the next call.
|
|
1107
|
-
*/
|
|
1108
|
-
function recycleWorker() {
|
|
1109
|
-
if (!parseWorker)
|
|
1110
|
-
return;
|
|
1111
|
-
log(`Recycling worker after ${workerParseCount} parses (heap: ${Math.round(process.memoryUsage().rss / 1024 / 1024)}MB RSS)`);
|
|
1112
|
-
const w = parseWorker;
|
|
1113
|
-
parseWorker = null;
|
|
1114
|
-
workerParseCount = 0;
|
|
1115
|
-
// Fire-and-forget: worker.terminate() can hang if WASM is stuck
|
|
1116
|
-
w.terminate().catch(() => { });
|
|
1117
|
-
}
|
|
1118
|
-
async function requestParse(filePath, content) {
|
|
1119
|
-
// Resolve the language on the main thread (where the project's
|
|
1120
|
-
// homegraph.json overrides are loaded) and hand it to the worker, so the
|
|
1121
|
-
// worker never needs the override map itself.
|
|
1122
|
-
const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
|
|
1123
|
-
if (!WorkerClass || (0, grammars_1.requiresInProcessExtraction)(language)) {
|
|
1124
|
-
// In-process fallback (also required for languages whose extractors
|
|
1125
|
-
// cannot run in a worker thread).
|
|
1126
|
-
return (0, tree_sitter_1.extractFromSource)(filePath, content, language, frameworkNames);
|
|
1127
|
-
}
|
|
1128
|
-
// Recycle the worker before the next parse if we've hit the threshold.
|
|
1129
|
-
// This destroys the WASM linear memory (which can grow but never shrink)
|
|
1130
|
-
// and starts a fresh worker with a clean heap.
|
|
1131
|
-
if (workerParseCount >= WORKER_RECYCLE_INTERVAL) {
|
|
1132
|
-
await recycleWorker();
|
|
1133
|
-
}
|
|
1134
|
-
const worker = await ensureWorker();
|
|
1135
|
-
const id = nextId++;
|
|
1136
|
-
workerParseCount++;
|
|
1137
|
-
// Scale timeout for large files: base 10s + 10s per 100KB
|
|
1138
|
-
const timeoutMs = PARSE_TIMEOUT_MS + Math.floor(content.length / 100_000) * 10_000;
|
|
1139
|
-
return new Promise((resolve, reject) => {
|
|
1140
|
-
const timer = setTimeout(() => {
|
|
1141
|
-
pendingParses.delete(id);
|
|
1142
|
-
log(`TIMEOUT: ${filePath} exceeded ${timeoutMs}ms — killing worker`);
|
|
1143
|
-
// Reject FIRST — worker.terminate() can hang if WASM is stuck
|
|
1144
|
-
parseWorker = null;
|
|
1145
|
-
workerParseCount = 0;
|
|
1146
|
-
reject(new Error(`Parse timed out after ${timeoutMs}ms`));
|
|
1147
|
-
// Fire-and-forget: kill the stuck worker in the background
|
|
1148
|
-
worker.terminate().catch(() => { });
|
|
1149
|
-
}, timeoutMs);
|
|
1150
|
-
pendingParses.set(id, { resolve, reject, timer });
|
|
1151
|
-
worker.postMessage({ type: 'parse', id, filePath, content, frameworkNames, language });
|
|
1184
|
+
(0, arkts_1.resetArkTSBatch)();
|
|
1185
|
+
(0, context_1.setArkTSBatchProgressCallback)((batchProgress) => {
|
|
1186
|
+
onProgress?.({
|
|
1187
|
+
phase: 'arkts-batch',
|
|
1188
|
+
current: batchProgress.current,
|
|
1189
|
+
total: batchProgress.total,
|
|
1190
|
+
currentFile: batchProgress.currentFile,
|
|
1191
|
+
subphase: batchProgress.subphase,
|
|
1152
1192
|
});
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1193
|
+
});
|
|
1194
|
+
try {
|
|
1155
1195
|
if (signal?.aborted) {
|
|
1156
|
-
if (parseWorker)
|
|
1157
|
-
parseWorker.terminate().catch(() => { });
|
|
1158
1196
|
return {
|
|
1159
1197
|
success: false,
|
|
1160
|
-
filesIndexed,
|
|
1161
|
-
filesSkipped,
|
|
1162
|
-
filesErrored,
|
|
1163
|
-
nodesCreated:
|
|
1164
|
-
edgesCreated:
|
|
1165
|
-
errors: [{ message: 'Aborted', severity: 'error' }
|
|
1198
|
+
filesIndexed: 0,
|
|
1199
|
+
filesSkipped: 0,
|
|
1200
|
+
filesErrored: 0,
|
|
1201
|
+
nodesCreated: 0,
|
|
1202
|
+
edgesCreated: 0,
|
|
1203
|
+
errors: [{ message: 'Aborted', severity: 'error' }],
|
|
1166
1204
|
durationMs: Date.now() - startTime,
|
|
1167
1205
|
};
|
|
1168
1206
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
//
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
// Report progress before parsing (show current file being worked on)
|
|
1206
|
-
onProgress?.({
|
|
1207
|
-
phase: 'parsing',
|
|
1208
|
-
current: processed,
|
|
1209
|
-
total,
|
|
1210
|
-
currentFile: filePath,
|
|
1207
|
+
// Phase 2: Parse files in a worker thread (keeps main thread unblocked for UI)
|
|
1208
|
+
const total = files.length;
|
|
1209
|
+
let processed = 0;
|
|
1210
|
+
// Emit parsing phase immediately so the progress bar appears during worker setup.
|
|
1211
|
+
// The yield lets the shimmer worker flush the phase transition to stdout before
|
|
1212
|
+
// the main thread starts synchronous grammar detection work.
|
|
1213
|
+
onProgress?.({
|
|
1214
|
+
phase: 'parsing',
|
|
1215
|
+
current: 0,
|
|
1216
|
+
total,
|
|
1217
|
+
});
|
|
1218
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
1219
|
+
// Detect needed languages and load grammars in the parse worker
|
|
1220
|
+
const neededLanguages = [...new Set(files.map((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides)))];
|
|
1221
|
+
// .h files default to 'c' but may be C++ — ensure cpp grammar is loaded when c is needed
|
|
1222
|
+
if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
|
|
1223
|
+
neededLanguages.push('cpp');
|
|
1224
|
+
}
|
|
1225
|
+
// Parse files on a pool of worker threads (keeps the main thread free for UI
|
|
1226
|
+
// and uses every core). Falls back to in-process parsing when the compiled
|
|
1227
|
+
// worker is unavailable (e.g. running from source in tests).
|
|
1228
|
+
const parseWorkerPath = path.join(__dirname, 'parse-worker.js');
|
|
1229
|
+
const useWorker = fs.existsSync(parseWorkerPath);
|
|
1230
|
+
let pool = null;
|
|
1231
|
+
if (useWorker) {
|
|
1232
|
+
// CODEGRAPH_PARSE_WORKERS: explicit worker count; 1 = the old single-worker
|
|
1233
|
+
// behaviour (the conservative rollback). Unset → clamp(cores-1, 1, 8).
|
|
1234
|
+
const poolSize = (0, parse_pool_1.resolveParsePoolSize)(process.env.CODEGRAPH_PARSE_WORKERS, os.cpus().length);
|
|
1235
|
+
pool = new parse_pool_1.ParseWorkerPool({
|
|
1236
|
+
languages: neededLanguages,
|
|
1237
|
+
size: poolSize,
|
|
1238
|
+
workerScriptPath: parseWorkerPath,
|
|
1239
|
+
recycleInterval: WORKER_RECYCLE_INTERVAL,
|
|
1240
|
+
parseTimeoutMs: PARSE_TIMEOUT_MS,
|
|
1241
|
+
log,
|
|
1211
1242
|
});
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
if (stats.size > MAX_FILE_SIZE) {
|
|
1229
|
-
processed++;
|
|
1230
|
-
filesSkipped++;
|
|
1231
|
-
errors.push({
|
|
1232
|
-
message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
|
|
1233
|
-
filePath,
|
|
1234
|
-
severity: 'warning',
|
|
1235
|
-
code: 'size_exceeded',
|
|
1236
|
-
});
|
|
1237
|
-
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
1238
|
-
continue;
|
|
1239
|
-
}
|
|
1240
|
-
// Parse in worker thread (main thread stays unblocked).
|
|
1241
|
-
// Wrapped in try/catch to handle worker timeouts and crashes gracefully.
|
|
1242
|
-
let result;
|
|
1243
|
-
try {
|
|
1244
|
-
result = await requestParse(filePath, content);
|
|
1245
|
-
}
|
|
1246
|
-
catch (parseErr) {
|
|
1247
|
-
processed++;
|
|
1248
|
-
filesErrored++;
|
|
1249
|
-
errors.push({
|
|
1250
|
-
message: parseErr instanceof Error ? parseErr.message : String(parseErr),
|
|
1251
|
-
filePath,
|
|
1252
|
-
severity: 'error',
|
|
1253
|
-
code: 'parse_error',
|
|
1254
|
-
});
|
|
1255
|
-
continue;
|
|
1243
|
+
log(`Parse worker pool: ${poolSize} worker(s)`);
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
// In-process fallback: load grammars locally and parse on the main thread.
|
|
1247
|
+
await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Parse one file: on the pool when available (the promise REJECTS on a worker
|
|
1251
|
+
* crash/timeout — the caller records it and the retry pass re-attempts), or
|
|
1252
|
+
* in-process synchronously as the no-worker fallback. The language is resolved
|
|
1253
|
+
* here on the main thread, where the codegraph.json overrides are loaded.
|
|
1254
|
+
*/
|
|
1255
|
+
const parseFile = (filePath, content) => {
|
|
1256
|
+
const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
|
|
1257
|
+
if (!pool || (0, grammars_1.requiresInProcessExtraction)(language)) {
|
|
1258
|
+
return Promise.resolve((0, tree_sitter_1.extractFromSource)(filePath, content, language, frameworkNames));
|
|
1256
1259
|
}
|
|
1260
|
+
return pool.requestParse({ filePath, content, language, frameworkNames });
|
|
1261
|
+
};
|
|
1262
|
+
// --- Bounded rolling-window dispatch, ordered commit ---
|
|
1263
|
+
// Reads stay batched/parallel; parses run concurrently across the pool; the
|
|
1264
|
+
// SQLite store stays on the main thread (it isn't thread-safe). Crucially we
|
|
1265
|
+
// COMMIT results in original file order, not parse-completion order: the
|
|
1266
|
+
// resolution phase (run after indexing) resolves an ambiguous reference to one
|
|
1267
|
+
// of several same-named candidates by the nodes' DB insertion order, so a
|
|
1268
|
+
// stable commit order keeps the resulting graph deterministic — byte-identical
|
|
1269
|
+
// to the single-worker path — instead of drifting with parse timing. The
|
|
1270
|
+
// `completed` buffer holds at most ~windowSize out-of-order results, so memory
|
|
1271
|
+
// stays bounded.
|
|
1272
|
+
const windowSize = pool ? Math.max(4, pool.size * 2) : 1;
|
|
1273
|
+
const inFlight = new Set();
|
|
1274
|
+
const completed = new Map();
|
|
1275
|
+
let nextSeq = 0; // file-order sequence assigned at dispatch
|
|
1276
|
+
let nextToStore = 0; // cursor: next sequence to commit
|
|
1277
|
+
let aborted = false;
|
|
1278
|
+
const storeResult = (filePath, content, stats, result) => {
|
|
1257
1279
|
processed++;
|
|
1258
1280
|
// Store in database on main thread (SQLite is not thread-safe)
|
|
1259
1281
|
if (result.nodes.length > 0 || result.errors.length === 0) {
|
|
@@ -1277,8 +1299,8 @@ class ExtractionOrchestrator {
|
|
|
1277
1299
|
}
|
|
1278
1300
|
else {
|
|
1279
1301
|
// Files with no symbols but no errors (yaml, twig, properties) are
|
|
1280
|
-
// tracked at the file level — count them as indexed so the CLI
|
|
1281
|
-
//
|
|
1302
|
+
// tracked at the file level — count them as indexed so the CLI doesn't
|
|
1303
|
+
// misleadingly report "No files found to index".
|
|
1282
1304
|
const lang = (0, grammars_1.detectLanguage)(filePath, content, overrides);
|
|
1283
1305
|
if ((0, grammars_1.isFileLevelOnlyLanguage)(lang)) {
|
|
1284
1306
|
filesIndexed++;
|
|
@@ -1287,102 +1309,229 @@ class ExtractionOrchestrator {
|
|
|
1287
1309
|
filesSkipped++;
|
|
1288
1310
|
}
|
|
1289
1311
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
|
|
1319
|
-
if (!fullPath)
|
|
1320
|
-
continue;
|
|
1321
|
-
content = await fsp.readFile(fullPath, 'utf-8');
|
|
1322
|
-
}
|
|
1323
|
-
catch {
|
|
1324
|
-
continue;
|
|
1312
|
+
onProgress?.({ phase: 'parsing', current: processed, total, currentFile: filePath });
|
|
1313
|
+
};
|
|
1314
|
+
const recordParseFailure = (filePath, err) => {
|
|
1315
|
+
processed++;
|
|
1316
|
+
filesErrored++;
|
|
1317
|
+
errors.push({
|
|
1318
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1319
|
+
filePath,
|
|
1320
|
+
severity: 'error',
|
|
1321
|
+
code: 'parse_error',
|
|
1322
|
+
});
|
|
1323
|
+
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
1324
|
+
};
|
|
1325
|
+
// Commit buffered parses to the DB in file order, advancing the cursor over
|
|
1326
|
+
// contiguous completed results. Runs after each parse settles (and once more
|
|
1327
|
+
// after the drain). storeResult / recordParseFailure run here single-threaded,
|
|
1328
|
+
// so shared counters and SQLite writes never race despite parallel parsing.
|
|
1329
|
+
const flushOrdered = () => {
|
|
1330
|
+
if (aborted)
|
|
1331
|
+
return;
|
|
1332
|
+
while (completed.has(nextToStore)) {
|
|
1333
|
+
const item = completed.get(nextToStore);
|
|
1334
|
+
completed.delete(nextToStore);
|
|
1335
|
+
nextToStore++;
|
|
1336
|
+
if (item.ok)
|
|
1337
|
+
storeResult(item.filePath, item.content, item.stats, item.result);
|
|
1338
|
+
else
|
|
1339
|
+
recordParseFailure(item.filePath, item.err);
|
|
1325
1340
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1341
|
+
};
|
|
1342
|
+
// Dispatch one file's parse (parses run concurrently across the pool), tagged
|
|
1343
|
+
// with its file-order sequence so flushOrdered commits results in order. The
|
|
1344
|
+
// backpressure below bounds how far parsing runs ahead of the in-order commit.
|
|
1345
|
+
const feed = async (filePath, content, stats) => {
|
|
1346
|
+
const seq = nextSeq++;
|
|
1347
|
+
const p = (async () => {
|
|
1348
|
+
try {
|
|
1349
|
+
const result = await parseFile(filePath, content);
|
|
1350
|
+
completed.set(seq, { ok: true, filePath, content, stats, result });
|
|
1351
|
+
}
|
|
1352
|
+
catch (parseErr) {
|
|
1353
|
+
completed.set(seq, { ok: false, filePath, err: parseErr });
|
|
1354
|
+
}
|
|
1355
|
+
flushOrdered();
|
|
1356
|
+
})();
|
|
1357
|
+
const tracked = p.finally(() => { inFlight.delete(tracked); });
|
|
1358
|
+
inFlight.add(tracked);
|
|
1359
|
+
// Backpressure on the dispatched-but-not-yet-committed count (in-flight +
|
|
1360
|
+
// buffered), not just in-flight: a slow file sitting at the commit cursor
|
|
1361
|
+
// lets later parses finish and buffer, which would otherwise grow without
|
|
1362
|
+
// bound. Wait for parses to settle (each may advance the cursor) until the
|
|
1363
|
+
// window has room. `inFlight.size > 0` guards against an empty race — the
|
|
1364
|
+
// cursor file is always still in flight when the window is full.
|
|
1365
|
+
while (nextSeq - nextToStore >= windowSize && inFlight.size > 0) {
|
|
1366
|
+
await Promise.race(inFlight);
|
|
1329
1367
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1368
|
+
};
|
|
1369
|
+
const firstEtsFile = files.find((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts');
|
|
1370
|
+
if (firstEtsFile) {
|
|
1371
|
+
await (0, arkts_1.primeArkTSBatch)(this.rootDir, this.queries, firstEtsFile);
|
|
1372
|
+
errors.push(...(0, arkts_1.drainArkTSIndexNotices)());
|
|
1373
|
+
}
|
|
1374
|
+
for (let i = 0; i < files.length; i += FILE_IO_BATCH_SIZE) {
|
|
1375
|
+
if (signal?.aborted) {
|
|
1376
|
+
aborted = true;
|
|
1377
|
+
break;
|
|
1333
1378
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1379
|
+
const batch = files.slice(i, i + FILE_IO_BATCH_SIZE);
|
|
1380
|
+
// Read files in parallel (with path validation before any I/O)
|
|
1381
|
+
const fileContents = await Promise.all(batch.map(async (fp) => {
|
|
1382
|
+
if ((0, arkts_1.isArkTSBatchPersisted)(fp)) {
|
|
1383
|
+
return {
|
|
1384
|
+
filePath: fp,
|
|
1385
|
+
content: null,
|
|
1386
|
+
stats: null,
|
|
1387
|
+
error: null,
|
|
1388
|
+
arktsBatchSkipped: true,
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
// Indexing read: follow in-root symlinks the directory walk already
|
|
1393
|
+
// descended into (the `../` guard still applies) so files reached
|
|
1394
|
+
// via an in-root symlink-to-outside still index (#935).
|
|
1395
|
+
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, fp, { allowSymlinkEscape: true });
|
|
1396
|
+
if (!fullPath) {
|
|
1397
|
+
(0, errors_1.logWarn)('Path traversal blocked in batch reader', { filePath: fp });
|
|
1398
|
+
return { filePath: fp, content: null, stats: null, error: new Error('Path traversal blocked') };
|
|
1399
|
+
}
|
|
1400
|
+
const content = await fsp.readFile(fullPath, 'utf-8');
|
|
1401
|
+
const stats = await fsp.stat(fullPath);
|
|
1402
|
+
return { filePath: fp, content, stats, error: null };
|
|
1403
|
+
}
|
|
1404
|
+
catch (err) {
|
|
1405
|
+
return { filePath: fp, content: null, stats: null, error: err };
|
|
1406
|
+
}
|
|
1407
|
+
}));
|
|
1408
|
+
// Dispatch each readable file into the bounded parse window; the window
|
|
1409
|
+
// stores results on the main thread as they arrive.
|
|
1410
|
+
for (const fileEntry of fileContents) {
|
|
1411
|
+
const { filePath, content, stats, error } = fileEntry;
|
|
1412
|
+
const arktsBatchSkipped = 'arktsBatchSkipped' in fileEntry && fileEntry.arktsBatchSkipped;
|
|
1413
|
+
if (signal?.aborted) {
|
|
1414
|
+
aborted = true;
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
if ((0, arkts_1.isArkTSBatchPersisted)(filePath) || arktsBatchSkipped) {
|
|
1418
|
+
processed++;
|
|
1419
|
+
filesIndexed++;
|
|
1420
|
+
onProgress?.({ phase: 'parsing', current: processed, total, currentFile: filePath });
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
if (!(0, context_1.isArktsBatchRunning)()) {
|
|
1424
|
+
onProgress?.({
|
|
1425
|
+
phase: 'parsing',
|
|
1426
|
+
current: processed,
|
|
1427
|
+
total,
|
|
1428
|
+
currentFile: filePath,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
if (error || content === null || stats === null) {
|
|
1432
|
+
processed++;
|
|
1433
|
+
filesErrored++;
|
|
1434
|
+
errors.push({
|
|
1435
|
+
message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
|
1436
|
+
filePath,
|
|
1437
|
+
severity: 'error',
|
|
1438
|
+
code: 'read_error',
|
|
1439
|
+
});
|
|
1440
|
+
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
// Honour MAX_FILE_SIZE. Without this check, vendored generated
|
|
1444
|
+
// headers, minified bundles, and other multi-MB files get indexed,
|
|
1445
|
+
// wasting WASM heap and the worker recycle budget on inputs with no
|
|
1446
|
+
// useful symbols. The single-file extractFile path already enforces
|
|
1447
|
+
// this; the bulk path used to silently skip the check.
|
|
1448
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
1449
|
+
processed++;
|
|
1450
|
+
filesSkipped++;
|
|
1451
|
+
errors.push({
|
|
1452
|
+
message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
|
|
1453
|
+
filePath,
|
|
1454
|
+
severity: 'warning',
|
|
1455
|
+
code: 'size_exceeded',
|
|
1456
|
+
});
|
|
1457
|
+
onProgress?.({ phase: 'parsing', current: processed, total });
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
// Parse on the pool (main thread stays unblocked). Errors/timeouts are
|
|
1461
|
+
// handled inside feed() → recordParseFailure, feeding the retry pass.
|
|
1462
|
+
await feed(filePath, content, stats);
|
|
1346
1463
|
}
|
|
1464
|
+
if (aborted)
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
// Drain parses still in flight (skip on abort — we tear down below instead),
|
|
1468
|
+
// then commit any results the cursor hasn't reached yet.
|
|
1469
|
+
if (!aborted) {
|
|
1470
|
+
await Promise.all(inFlight);
|
|
1471
|
+
flushOrdered();
|
|
1347
1472
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1473
|
+
if (signal?.aborted || aborted) {
|
|
1474
|
+
if (pool)
|
|
1475
|
+
await pool.destroy();
|
|
1476
|
+
return {
|
|
1477
|
+
success: false,
|
|
1478
|
+
filesIndexed,
|
|
1479
|
+
filesSkipped,
|
|
1480
|
+
filesErrored,
|
|
1481
|
+
nodesCreated: totalNodes,
|
|
1482
|
+
edgesCreated: totalEdges,
|
|
1483
|
+
errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
|
|
1484
|
+
durationMs: Date.now() - startTime,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
// Report 100% so the progress bar doesn't hang at 99%
|
|
1488
|
+
onProgress?.({
|
|
1489
|
+
phase: 'parsing',
|
|
1490
|
+
current: total,
|
|
1491
|
+
total,
|
|
1492
|
+
});
|
|
1493
|
+
// Yield so the shimmer worker's buffered stdout writes can flush.
|
|
1494
|
+
// Worker thread stdout is proxied through the main thread's event loop,
|
|
1495
|
+
// so synchronous work here blocks the animation from rendering.
|
|
1496
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
1497
|
+
// Retry pass: files that failed due to WASM memory corruption may succeed
|
|
1498
|
+
// on a fresh worker with a clean heap. Recycle before each attempt so
|
|
1499
|
+
// every file gets the absolute cleanest WASM state possible.
|
|
1500
|
+
const retryableErrors = errors.filter((e) => e.code === 'parse_error' && e.filePath &&
|
|
1501
|
+
(e.message.includes('Worker exited') || e.message.includes('memory access out of bounds')));
|
|
1502
|
+
if (retryableErrors.length > 0 && pool) {
|
|
1503
|
+
log(`Retrying ${retryableErrors.length} files that failed due to WASM memory errors...`);
|
|
1504
|
+
// Fresh WASM heaps for the retry phase. A retry that still crashes its
|
|
1505
|
+
// worker makes the pool respawn it, so later retries keep landing on clean
|
|
1506
|
+
// workers too.
|
|
1507
|
+
pool.recycleAll();
|
|
1508
|
+
const stillFailing = [];
|
|
1509
|
+
for (const errEntry of retryableErrors) {
|
|
1355
1510
|
const filePath = errEntry.filePath;
|
|
1356
1511
|
if (signal?.aborted)
|
|
1357
1512
|
break;
|
|
1358
|
-
|
|
1359
|
-
let fullContent;
|
|
1513
|
+
let content;
|
|
1360
1514
|
try {
|
|
1361
1515
|
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
|
|
1362
1516
|
if (!fullPath)
|
|
1363
1517
|
continue;
|
|
1364
|
-
|
|
1518
|
+
content = await fsp.readFile(fullPath, 'utf-8');
|
|
1365
1519
|
}
|
|
1366
1520
|
catch {
|
|
1367
1521
|
continue;
|
|
1368
1522
|
}
|
|
1369
|
-
// Strip lines that are entirely comments (preserving line numbers
|
|
1370
|
-
// by replacing with empty lines so node positions stay correct)
|
|
1371
|
-
const stripped = fullContent
|
|
1372
|
-
.split('\n')
|
|
1373
|
-
.map(line => /^\s*\/\//.test(line) ? '' : line)
|
|
1374
|
-
.join('\n');
|
|
1375
1523
|
let result;
|
|
1376
1524
|
try {
|
|
1377
|
-
result = await
|
|
1525
|
+
result = await parseFile(filePath, content);
|
|
1378
1526
|
}
|
|
1379
1527
|
catch {
|
|
1528
|
+
stillFailing.push(errEntry);
|
|
1380
1529
|
continue;
|
|
1381
1530
|
}
|
|
1382
1531
|
if (result.nodes.length > 0 || result.errors.length === 0) {
|
|
1383
|
-
const language = (0, grammars_1.detectLanguage)(filePath,
|
|
1532
|
+
const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
|
|
1384
1533
|
const stats = await fsp.stat(path.join(this.rootDir, filePath));
|
|
1385
|
-
this.storeExtractionResult(filePath,
|
|
1534
|
+
this.storeExtractionResult(filePath, content, language, stats, result);
|
|
1386
1535
|
const idx = errors.indexOf(errEntry);
|
|
1387
1536
|
if (idx >= 0)
|
|
1388
1537
|
errors.splice(idx, 1);
|
|
@@ -1390,26 +1539,76 @@ class ExtractionOrchestrator {
|
|
|
1390
1539
|
filesIndexed++;
|
|
1391
1540
|
totalNodes += result.nodes.length;
|
|
1392
1541
|
totalEdges += result.edges.length;
|
|
1393
|
-
log(`Retry
|
|
1542
|
+
log(`Retry OK: ${filePath} (${result.nodes.length} nodes)`);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
// Last resort: for files that still crash on a clean worker, strip
|
|
1546
|
+
// comment-only lines to reduce WASM memory pressure. Many compiler
|
|
1547
|
+
// test files are 90%+ comments (CHECK directives) that don't contribute
|
|
1548
|
+
// code nodes but consume parser memory.
|
|
1549
|
+
if (stillFailing.length > 0) {
|
|
1550
|
+
log(`${stillFailing.length} files still failing — retrying with comments stripped...`);
|
|
1551
|
+
pool.recycleAll();
|
|
1552
|
+
for (const errEntry of stillFailing) {
|
|
1553
|
+
const filePath = errEntry.filePath;
|
|
1554
|
+
if (signal?.aborted)
|
|
1555
|
+
break;
|
|
1556
|
+
let fullContent;
|
|
1557
|
+
try {
|
|
1558
|
+
const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
|
|
1559
|
+
if (!fullPath)
|
|
1560
|
+
continue;
|
|
1561
|
+
fullContent = await fsp.readFile(fullPath, 'utf-8');
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
// Strip lines that are entirely comments (preserving line numbers
|
|
1567
|
+
// by replacing with empty lines so node positions stay correct)
|
|
1568
|
+
const stripped = fullContent
|
|
1569
|
+
.split('\n')
|
|
1570
|
+
.map(line => /^\s*\/\//.test(line) ? '' : line)
|
|
1571
|
+
.join('\n');
|
|
1572
|
+
let result;
|
|
1573
|
+
try {
|
|
1574
|
+
result = await parseFile(filePath, stripped);
|
|
1575
|
+
}
|
|
1576
|
+
catch {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
if (result.nodes.length > 0 || result.errors.length === 0) {
|
|
1580
|
+
const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
|
|
1581
|
+
const stats = await fsp.stat(path.join(this.rootDir, filePath));
|
|
1582
|
+
this.storeExtractionResult(filePath, fullContent, language, stats, result);
|
|
1583
|
+
const idx = errors.indexOf(errEntry);
|
|
1584
|
+
if (idx >= 0)
|
|
1585
|
+
errors.splice(idx, 1);
|
|
1586
|
+
filesErrored--;
|
|
1587
|
+
filesIndexed++;
|
|
1588
|
+
totalNodes += result.nodes.length;
|
|
1589
|
+
totalEdges += result.edges.length;
|
|
1590
|
+
log(`Retry (stripped) OK: ${filePath} (${result.nodes.length} nodes)`);
|
|
1591
|
+
}
|
|
1394
1592
|
}
|
|
1395
1593
|
}
|
|
1396
1594
|
}
|
|
1595
|
+
// Shut down the parse worker pool.
|
|
1596
|
+
if (pool)
|
|
1597
|
+
await pool.destroy();
|
|
1598
|
+
return {
|
|
1599
|
+
success: filesIndexed > 0 || errors.filter((e) => e.severity === 'error').length === 0,
|
|
1600
|
+
filesIndexed,
|
|
1601
|
+
filesSkipped,
|
|
1602
|
+
filesErrored,
|
|
1603
|
+
nodesCreated: totalNodes,
|
|
1604
|
+
edgesCreated: totalEdges,
|
|
1605
|
+
errors,
|
|
1606
|
+
durationMs: Date.now() - startTime,
|
|
1607
|
+
};
|
|
1397
1608
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
if (parseWorker) {
|
|
1401
|
-
parseWorker.terminate().catch(() => { });
|
|
1609
|
+
finally {
|
|
1610
|
+
(0, context_1.setArkTSBatchProgressCallback)(null);
|
|
1402
1611
|
}
|
|
1403
|
-
return {
|
|
1404
|
-
success: filesIndexed > 0 || errors.filter((e) => e.severity === 'error').length === 0,
|
|
1405
|
-
filesIndexed,
|
|
1406
|
-
filesSkipped,
|
|
1407
|
-
filesErrored,
|
|
1408
|
-
nodesCreated: totalNodes,
|
|
1409
|
-
edgesCreated: totalEdges,
|
|
1410
|
-
errors,
|
|
1411
|
-
durationMs: Date.now() - startTime,
|
|
1412
|
-
};
|
|
1413
1612
|
}
|
|
1414
1613
|
/**
|
|
1415
1614
|
* Index specific files
|
|
@@ -1532,6 +1731,15 @@ class ExtractionOrchestrator {
|
|
|
1532
1731
|
}
|
|
1533
1732
|
// Detect language (honoring the project's homegraph.json extension overrides)
|
|
1534
1733
|
const language = (0, grammars_1.detectLanguage)(relativePath, content, (0, project_config_1.loadExtensionOverrides)(this.rootDir));
|
|
1734
|
+
if ((0, arkts_1.isArkTSBatchPersisted)(relativePath)) {
|
|
1735
|
+
return {
|
|
1736
|
+
nodes: [],
|
|
1737
|
+
edges: [],
|
|
1738
|
+
unresolvedReferences: [],
|
|
1739
|
+
errors: [],
|
|
1740
|
+
durationMs: 0,
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1535
1743
|
if (!(0, grammars_1.isLanguageSupported)(language)) {
|
|
1536
1744
|
return {
|
|
1537
1745
|
nodes: [],
|
|
@@ -1768,16 +1976,47 @@ class ExtractionOrchestrator {
|
|
|
1768
1976
|
}
|
|
1769
1977
|
// Index changed files
|
|
1770
1978
|
const total = filesToIndex.length;
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1979
|
+
const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
|
|
1980
|
+
const needsArkTS = filesToIndex.some((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts');
|
|
1981
|
+
if (needsArkTS) {
|
|
1982
|
+
(0, context_1.setArkTSBatchProgressCallback)((batchProgress) => {
|
|
1983
|
+
onProgress?.({
|
|
1984
|
+
phase: 'arkts-batch',
|
|
1985
|
+
current: batchProgress.current,
|
|
1986
|
+
total: batchProgress.total,
|
|
1987
|
+
currentFile: batchProgress.currentFile,
|
|
1988
|
+
subphase: batchProgress.subphase,
|
|
1989
|
+
});
|
|
1778
1990
|
});
|
|
1779
|
-
const
|
|
1780
|
-
|
|
1991
|
+
const firstEts = filesToIndex.find((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts');
|
|
1992
|
+
await (0, arkts_1.primeArkTSBatch)(this.rootDir, this.queries, firstEts);
|
|
1993
|
+
}
|
|
1994
|
+
try {
|
|
1995
|
+
for (let i = 0; i < filesToIndex.length; i++) {
|
|
1996
|
+
const filePath = filesToIndex[i];
|
|
1997
|
+
if ((0, arkts_1.isArkTSBatchPersisted)(filePath)) {
|
|
1998
|
+
onProgress?.({
|
|
1999
|
+
phase: 'parsing',
|
|
2000
|
+
current: i + 1,
|
|
2001
|
+
total,
|
|
2002
|
+
currentFile: filePath,
|
|
2003
|
+
});
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
onProgress?.({
|
|
2007
|
+
phase: 'parsing',
|
|
2008
|
+
current: i + 1,
|
|
2009
|
+
total,
|
|
2010
|
+
currentFile: filePath,
|
|
2011
|
+
});
|
|
2012
|
+
const result = await this.indexFile(filePath);
|
|
2013
|
+
nodesUpdated += result.nodes.length;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
finally {
|
|
2017
|
+
if (needsArkTS) {
|
|
2018
|
+
(0, context_1.setArkTSBatchProgressCallback)(null);
|
|
2019
|
+
}
|
|
1781
2020
|
}
|
|
1782
2021
|
return {
|
|
1783
2022
|
filesChecked,
|