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.
Files changed (361) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +268 -243
  3. package/dist/arkts/ohos-api-index.d.ts +15 -0
  4. package/dist/arkts/ohos-api-index.d.ts.map +1 -0
  5. package/dist/arkts/ohos-api-index.js +190 -0
  6. package/dist/arkts/ohos-api-index.js.map +1 -0
  7. package/dist/arkts/ohos-sdk-input.d.ts +36 -0
  8. package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
  9. package/dist/arkts/ohos-sdk-input.js +214 -0
  10. package/dist/arkts/ohos-sdk-input.js.map +1 -0
  11. package/dist/bin/command-supervision.d.ts +12 -0
  12. package/dist/bin/command-supervision.d.ts.map +1 -0
  13. package/dist/bin/command-supervision.js +86 -0
  14. package/dist/bin/command-supervision.js.map +1 -0
  15. package/dist/bin/homegraph.d.ts +1 -1
  16. package/dist/bin/homegraph.js +971 -71
  17. package/dist/bin/homegraph.js.map +1 -1
  18. package/dist/db/index.d.ts +20 -0
  19. package/dist/db/index.d.ts.map +1 -1
  20. package/dist/db/index.js +39 -0
  21. package/dist/db/index.js.map +1 -1
  22. package/dist/db/migrations.d.ts +1 -1
  23. package/dist/db/migrations.d.ts.map +1 -1
  24. package/dist/db/migrations.js +58 -19
  25. package/dist/db/migrations.js.map +1 -1
  26. package/dist/db/queries.d.ts +14 -0
  27. package/dist/db/queries.d.ts.map +1 -1
  28. package/dist/db/queries.js +314 -143
  29. package/dist/db/queries.js.map +1 -1
  30. package/dist/db/schema.sql +172 -152
  31. package/dist/directory.d.ts +32 -0
  32. package/dist/directory.d.ts.map +1 -1
  33. package/dist/directory.js +88 -5
  34. package/dist/directory.js.map +1 -1
  35. package/dist/extraction/arkts-batch-worker.d.ts +2 -0
  36. package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
  37. package/dist/extraction/arkts-batch-worker.js +28 -0
  38. package/dist/extraction/arkts-batch-worker.js.map +1 -0
  39. package/dist/extraction/context.d.ts +11 -0
  40. package/dist/extraction/context.d.ts.map +1 -1
  41. package/dist/extraction/context.js +20 -0
  42. package/dist/extraction/context.js.map +1 -1
  43. package/dist/extraction/index.d.ts +16 -2
  44. package/dist/extraction/index.d.ts.map +1 -1
  45. package/dist/extraction/index.js +619 -380
  46. package/dist/extraction/index.js.map +1 -1
  47. package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
  48. package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
  49. package/dist/extraction/languages/arkts-state-decorators.js +26 -0
  50. package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
  51. package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
  52. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
  53. package/dist/extraction/languages/arkts-viewtree.js +21 -6
  54. package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
  55. package/dist/extraction/languages/arkts.d.ts +98 -2
  56. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  57. package/dist/extraction/languages/arkts.js +1290 -61
  58. package/dist/extraction/languages/arkts.js.map +1 -1
  59. package/dist/extraction/languages/c-cpp.d.ts +56 -0
  60. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  61. package/dist/extraction/languages/c-cpp.js +198 -1
  62. package/dist/extraction/languages/c-cpp.js.map +1 -1
  63. package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
  64. package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
  65. package/dist/extraction/languages/ohos-api-consumer.js +283 -0
  66. package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
  67. package/dist/extraction/parse-pool.d.ts +126 -0
  68. package/dist/extraction/parse-pool.d.ts.map +1 -0
  69. package/dist/extraction/parse-pool.js +319 -0
  70. package/dist/extraction/parse-pool.js.map +1 -0
  71. package/dist/extraction/tree-sitter-types.d.ts +17 -0
  72. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  73. package/dist/extraction/tree-sitter.d.ts +21 -0
  74. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  75. package/dist/extraction/tree-sitter.js +198 -27
  76. package/dist/extraction/tree-sitter.js.map +1 -1
  77. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  78. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  79. package/dist/graph/traversal.d.ts.map +1 -1
  80. package/dist/graph/traversal.js +76 -17
  81. package/dist/graph/traversal.js.map +1 -1
  82. package/dist/index.d.ts +25 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +78 -4
  85. package/dist/index.js.map +1 -1
  86. package/dist/installer/instructions-template.js +9 -9
  87. package/dist/installer/targets/shared.d.ts +5 -6
  88. package/dist/installer/targets/shared.d.ts.map +1 -1
  89. package/dist/installer/targets/shared.js +5 -6
  90. package/dist/installer/targets/shared.js.map +1 -1
  91. package/dist/mcp/daemon-paths.d.ts +30 -3
  92. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  93. package/dist/mcp/daemon-paths.js +50 -10
  94. package/dist/mcp/daemon-paths.js.map +1 -1
  95. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  96. package/dist/mcp/daemon-registry.js +7 -3
  97. package/dist/mcp/daemon-registry.js.map +1 -1
  98. package/dist/mcp/daemon.d.ts +48 -0
  99. package/dist/mcp/daemon.d.ts.map +1 -1
  100. package/dist/mcp/daemon.js +196 -32
  101. package/dist/mcp/daemon.js.map +1 -1
  102. package/dist/mcp/engine.d.ts +17 -0
  103. package/dist/mcp/engine.d.ts.map +1 -1
  104. package/dist/mcp/engine.js +73 -1
  105. package/dist/mcp/engine.js.map +1 -1
  106. package/dist/mcp/index.d.ts.map +1 -1
  107. package/dist/mcp/index.js +25 -43
  108. package/dist/mcp/index.js.map +1 -1
  109. package/dist/mcp/liveness-watchdog.js +16 -16
  110. package/dist/mcp/ppid-watchdog.d.ts +18 -0
  111. package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
  112. package/dist/mcp/ppid-watchdog.js +37 -0
  113. package/dist/mcp/ppid-watchdog.js.map +1 -1
  114. package/dist/mcp/query-cache.d.ts +25 -0
  115. package/dist/mcp/query-cache.d.ts.map +1 -0
  116. package/dist/mcp/query-cache.js +191 -0
  117. package/dist/mcp/query-cache.js.map +1 -0
  118. package/dist/mcp/query-pool.d.ts +94 -0
  119. package/dist/mcp/query-pool.d.ts.map +1 -0
  120. package/dist/mcp/query-pool.js +297 -0
  121. package/dist/mcp/query-pool.js.map +1 -0
  122. package/dist/mcp/query-worker.d.ts +24 -0
  123. package/dist/mcp/query-worker.d.ts.map +1 -0
  124. package/dist/mcp/query-worker.js +87 -0
  125. package/dist/mcp/query-worker.js.map +1 -0
  126. package/dist/mcp/server-instructions.d.ts +5 -7
  127. package/dist/mcp/server-instructions.d.ts.map +1 -1
  128. package/dist/mcp/server-instructions.js +72 -74
  129. package/dist/mcp/server-instructions.js.map +1 -1
  130. package/dist/mcp/tools.d.ts +94 -4
  131. package/dist/mcp/tools.d.ts.map +1 -1
  132. package/dist/mcp/tools.js +877 -71
  133. package/dist/mcp/tools.js.map +1 -1
  134. package/dist/project-config.d.ts +20 -0
  135. package/dist/project-config.d.ts.map +1 -1
  136. package/dist/project-config.js +42 -2
  137. package/dist/project-config.js.map +1 -1
  138. package/dist/reasoning/login.js +1 -1
  139. package/dist/reasoning/login.js.map +1 -1
  140. package/dist/reasoning/reasoner.js +32 -32
  141. package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
  142. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  143. package/dist/resolution/c-fnptr-synthesizer.js +765 -79
  144. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  145. package/dist/resolution/callback-synthesizer.d.ts +1 -1
  146. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  147. package/dist/resolution/callback-synthesizer.js +72 -11
  148. package/dist/resolution/callback-synthesizer.js.map +1 -1
  149. package/dist/resolution/cooperative-yield.d.ts +32 -0
  150. package/dist/resolution/cooperative-yield.d.ts.map +1 -0
  151. package/dist/resolution/cooperative-yield.js +42 -0
  152. package/dist/resolution/cooperative-yield.js.map +1 -0
  153. package/dist/resolution/index.d.ts +11 -2
  154. package/dist/resolution/index.d.ts.map +1 -1
  155. package/dist/resolution/index.js +72 -4
  156. package/dist/resolution/index.js.map +1 -1
  157. package/dist/resolution/name-matcher.d.ts +22 -0
  158. package/dist/resolution/name-matcher.d.ts.map +1 -1
  159. package/dist/resolution/name-matcher.js +317 -20
  160. package/dist/resolution/name-matcher.js.map +1 -1
  161. package/dist/spec/config.d.ts +39 -0
  162. package/dist/spec/config.d.ts.map +1 -0
  163. package/dist/spec/config.js +304 -0
  164. package/dist/spec/config.js.map +1 -0
  165. package/dist/spec/db/commit-node.d.ts +23 -0
  166. package/dist/spec/db/commit-node.d.ts.map +1 -0
  167. package/dist/spec/db/commit-node.js +62 -0
  168. package/dist/spec/db/commit-node.js.map +1 -0
  169. package/dist/spec/db/fragment-node.d.ts +24 -0
  170. package/dist/spec/db/fragment-node.d.ts.map +1 -0
  171. package/dist/spec/db/fragment-node.js +128 -0
  172. package/dist/spec/db/fragment-node.js.map +1 -0
  173. package/dist/spec/db/fts.d.ts +74 -0
  174. package/dist/spec/db/fts.d.ts.map +1 -0
  175. package/dist/spec/db/fts.js +324 -0
  176. package/dist/spec/db/fts.js.map +1 -0
  177. package/dist/spec/db/index.d.ts +13 -0
  178. package/dist/spec/db/index.d.ts.map +1 -0
  179. package/dist/spec/db/index.js +50 -0
  180. package/dist/spec/db/index.js.map +1 -0
  181. package/dist/spec/db/relations.d.ts +55 -0
  182. package/dist/spec/db/relations.d.ts.map +1 -0
  183. package/dist/spec/db/relations.js +158 -0
  184. package/dist/spec/db/relations.js.map +1 -0
  185. package/dist/spec/db/schema.d.ts +33 -0
  186. package/dist/spec/db/schema.d.ts.map +1 -0
  187. package/dist/spec/db/schema.js +119 -0
  188. package/dist/spec/db/schema.js.map +1 -0
  189. package/dist/spec/db/schema.sql +117 -0
  190. package/dist/spec/db/spec-node.d.ts +41 -0
  191. package/dist/spec/db/spec-node.d.ts.map +1 -0
  192. package/dist/spec/db/spec-node.js +114 -0
  193. package/dist/spec/db/spec-node.js.map +1 -0
  194. package/dist/spec/evolve/impact-locator.d.ts +13 -0
  195. package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
  196. package/dist/spec/evolve/impact-locator.js +25 -0
  197. package/dist/spec/evolve/impact-locator.js.map +1 -0
  198. package/dist/spec/evolve/llm-client.d.ts +50 -0
  199. package/dist/spec/evolve/llm-client.d.ts.map +1 -0
  200. package/dist/spec/evolve/llm-client.js +176 -0
  201. package/dist/spec/evolve/llm-client.js.map +1 -0
  202. package/dist/spec/evolve/logic-checker.d.ts +12 -0
  203. package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
  204. package/dist/spec/evolve/logic-checker.js +24 -0
  205. package/dist/spec/evolve/logic-checker.js.map +1 -0
  206. package/dist/spec/evolve/pipeline.d.ts +42 -0
  207. package/dist/spec/evolve/pipeline.d.ts.map +1 -0
  208. package/dist/spec/evolve/pipeline.js +567 -0
  209. package/dist/spec/evolve/pipeline.js.map +1 -0
  210. package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
  211. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
  212. package/dist/spec/evolve/spec-rewriter.js +230 -0
  213. package/dist/spec/evolve/spec-rewriter.js.map +1 -0
  214. package/dist/spec/graph/queries.d.ts +155 -0
  215. package/dist/spec/graph/queries.d.ts.map +1 -0
  216. package/dist/spec/graph/queries.js +440 -0
  217. package/dist/spec/graph/queries.js.map +1 -0
  218. package/dist/spec/llm/client.d.ts +29 -0
  219. package/dist/spec/llm/client.d.ts.map +1 -0
  220. package/dist/spec/llm/client.js +123 -0
  221. package/dist/spec/llm/client.js.map +1 -0
  222. package/dist/spec/llm/index.d.ts +3 -0
  223. package/dist/spec/llm/index.d.ts.map +1 -0
  224. package/dist/spec/llm/index.js +11 -0
  225. package/dist/spec/llm/index.js.map +1 -0
  226. package/dist/spec/llm/prompts.d.ts +13 -0
  227. package/dist/spec/llm/prompts.d.ts.map +1 -0
  228. package/dist/spec/llm/prompts.js +75 -0
  229. package/dist/spec/llm/prompts.js.map +1 -0
  230. package/dist/spec/mining/diff-parser.d.ts +33 -0
  231. package/dist/spec/mining/diff-parser.d.ts.map +1 -0
  232. package/dist/spec/mining/diff-parser.js +166 -0
  233. package/dist/spec/mining/diff-parser.js.map +1 -0
  234. package/dist/spec/mining/git-scanner.d.ts +103 -0
  235. package/dist/spec/mining/git-scanner.d.ts.map +1 -0
  236. package/dist/spec/mining/git-scanner.js +307 -0
  237. package/dist/spec/mining/git-scanner.js.map +1 -0
  238. package/dist/spec/mining/pipeline.d.ts +53 -0
  239. package/dist/spec/mining/pipeline.d.ts.map +1 -0
  240. package/dist/spec/mining/pipeline.js +178 -0
  241. package/dist/spec/mining/pipeline.js.map +1 -0
  242. package/dist/spec/mining/scope-resolver.d.ts +45 -0
  243. package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
  244. package/dist/spec/mining/scope-resolver.js +103 -0
  245. package/dist/spec/mining/scope-resolver.js.map +1 -0
  246. package/dist/spec/mining/spec-extractor.d.ts +69 -0
  247. package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
  248. package/dist/spec/mining/spec-extractor.js +369 -0
  249. package/dist/spec/mining/spec-extractor.js.map +1 -0
  250. package/dist/spec/types.d.ts +149 -0
  251. package/dist/spec/types.d.ts.map +1 -0
  252. package/dist/spec/types.js +15 -0
  253. package/dist/spec/types.js.map +1 -0
  254. package/dist/spec/utils.d.ts +167 -0
  255. package/dist/spec/utils.d.ts.map +1 -0
  256. package/dist/spec/utils.js +463 -0
  257. package/dist/spec/utils.js.map +1 -0
  258. package/dist/sync/worktree.d.ts +9 -0
  259. package/dist/sync/worktree.d.ts.map +1 -1
  260. package/dist/sync/worktree.js +40 -0
  261. package/dist/sync/worktree.js.map +1 -1
  262. package/dist/types.d.ts +6 -1
  263. package/dist/types.d.ts.map +1 -1
  264. package/dist/ui/shimmer-progress.d.ts +2 -0
  265. package/dist/ui/shimmer-progress.d.ts.map +1 -1
  266. package/dist/ui/shimmer-progress.js +19 -2
  267. package/dist/ui/shimmer-progress.js.map +1 -1
  268. package/dist/upgrade/index.js +1 -1
  269. package/dist/upgrade/index.js.map +1 -1
  270. package/package.json +58 -57
  271. package/scripts/add-lang/bench.sh +60 -60
  272. package/scripts/add-lang/check-grammar.mjs +75 -75
  273. package/scripts/add-lang/dump-ast.mjs +103 -103
  274. package/scripts/add-lang/verify-extraction.mjs +70 -70
  275. package/scripts/agent-eval/ab-adoption.sh +91 -91
  276. package/scripts/agent-eval/ab-hook.sh +86 -86
  277. package/scripts/agent-eval/ab-impl.sh +78 -78
  278. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
  279. package/scripts/agent-eval/ab-sufficiency.sh +78 -78
  280. package/scripts/agent-eval/arms-F.sh +21 -21
  281. package/scripts/agent-eval/arms-matrix.sh +37 -37
  282. package/scripts/agent-eval/audit.sh +68 -68
  283. package/scripts/agent-eval/bench-readme.sh +28 -28
  284. package/scripts/agent-eval/bench-why-repo.sh +22 -22
  285. package/scripts/agent-eval/block-read-hook.sh +19 -19
  286. package/scripts/agent-eval/hook-settings.json +15 -15
  287. package/scripts/agent-eval/itrun.sh +120 -120
  288. package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
  289. package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
  290. package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
  291. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
  292. package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
  293. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
  294. package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
  295. package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
  296. package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
  297. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
  298. package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
  299. package/scripts/agent-eval/offload-eval-setup.sh +24 -24
  300. package/scripts/agent-eval/offload-eval-styles.sh +71 -71
  301. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
  302. package/scripts/agent-eval/offload-eval.md +76 -76
  303. package/scripts/agent-eval/parse-arms.mjs +116 -116
  304. package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
  305. package/scripts/agent-eval/parse-run.mjs +45 -45
  306. package/scripts/agent-eval/parse-session.mjs +93 -93
  307. package/scripts/agent-eval/probe-context.mjs +21 -21
  308. package/scripts/agent-eval/probe-explore.mjs +40 -40
  309. package/scripts/agent-eval/probe-node.mjs +20 -20
  310. package/scripts/agent-eval/probe-sweep.mjs +119 -119
  311. package/scripts/agent-eval/probe-trace.mjs +20 -20
  312. package/scripts/agent-eval/redirect-read-hook.sh +38 -38
  313. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
  314. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
  315. package/scripts/agent-eval/run-agent.sh +34 -34
  316. package/scripts/agent-eval/run-all.sh +69 -69
  317. package/scripts/agent-eval/run-arms.sh +56 -56
  318. package/scripts/agent-eval/seq-matrix.mjs +137 -137
  319. package/scripts/build-bundle.sh +118 -118
  320. package/scripts/exp_boundary_eval/README.md +247 -0
  321. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  322. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  323. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  324. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  325. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  326. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  327. package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
  328. package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
  329. package/scripts/exp_boundary_eval/_utils.py +1116 -0
  330. package/scripts/exp_boundary_eval/analyze.py +1313 -0
  331. package/scripts/exp_boundary_eval/data/agents.json +109 -0
  332. package/scripts/exp_boundary_eval/data/experiments.json +140 -0
  333. package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
  334. package/scripts/exp_boundary_eval/run_all.py +378 -0
  335. package/scripts/exp_boundary_eval/run_one.py +165 -0
  336. package/scripts/exp_boundary_eval/run_session.py +158 -0
  337. package/scripts/exp_boundary_eval/setup.py +120 -0
  338. package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
  339. package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
  340. package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
  341. package/scripts/extract-release-notes.mjs +130 -130
  342. package/scripts/local-install.sh +41 -41
  343. package/scripts/npm-sdk.js +75 -75
  344. package/scripts/npm-shim.js +268 -246
  345. package/scripts/ohos-sdk-publish.mjs +133 -0
  346. package/scripts/pack-npm.sh +119 -119
  347. package/scripts/prepare-release.mjs +270 -270
  348. package/scripts/qa_eval/README.md +407 -404
  349. package/scripts/qa_eval/_test_deveco_probe.py +41 -41
  350. package/scripts/qa_eval/agent_runner.py +526 -526
  351. package/scripts/qa_eval/data/.gitignore +4 -4
  352. package/scripts/qa_eval/data/test-set.jsonl +2 -22
  353. package/scripts/qa_eval/eval_metrics.py +274 -233
  354. package/scripts/qa_eval/external_agent.py +976 -671
  355. package/scripts/qa_eval/llm_config.py +92 -92
  356. package/scripts/qa_eval/memory_monitor.py +132 -132
  357. package/scripts/qa_eval/my_answer_accuracy.py +187 -187
  358. package/scripts/qa_eval/requirements.txt +2 -2
  359. package/scripts/qa_eval/run_pipeline.py +804 -711
  360. package/scripts/qa_eval/stats_efficiency.py +279 -279
  361. package/scripts/qa_eval/stats_scores.py +207 -207
@@ -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 git-visible files (tracked + untracked, .gitignore-respected) from the
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
- * Recurses into embedded git repositories nested repos that are NOT submodules
518
- * (independent clones living inside the workspace, common in CMake "super-repo"
519
- * layouts). The parent repo's `git ls-files` cannot see into them: tracked output
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
- const tracked = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-c', '--recurse-submodules'], gitOpts);
543
- for (const rel of tracked.split('\0')) {
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
- walk(rootDir, [{ dir: rootDir, ig: buildDefaultIgnore(rootDir) }]);
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
- if (signal?.aborted) {
996
- return {
997
- success: false,
998
- filesIndexed: 0,
999
- filesSkipped: 0,
1000
- filesErrored: 0,
1001
- nodesCreated: 0,
1002
- edgesCreated: 0,
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
- for (let i = 0; i < files.length; i += FILE_IO_BATCH_SIZE) {
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: totalNodes,
1164
- edgesCreated: totalEdges,
1165
- errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
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
- const batch = files.slice(i, i + FILE_IO_BATCH_SIZE);
1170
- // Read files in parallel (with path validation before any I/O)
1171
- const fileContents = await Promise.all(batch.map(async (fp) => {
1172
- try {
1173
- // Indexing read: follow in-root symlinks the directory walk already
1174
- // descended into (the `../` guard still applies) so files reached
1175
- // via an in-root symlink-to-outside still index (#935).
1176
- const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, fp, { allowSymlinkEscape: true });
1177
- if (!fullPath) {
1178
- (0, errors_1.logWarn)('Path traversal blocked in batch reader', { filePath: fp });
1179
- return { filePath: fp, content: null, stats: null, error: new Error('Path traversal blocked') };
1180
- }
1181
- const content = await fsp.readFile(fullPath, 'utf-8');
1182
- const stats = await fsp.stat(fullPath);
1183
- return { filePath: fp, content, stats, error: null };
1184
- }
1185
- catch (err) {
1186
- return { filePath: fp, content: null, stats: null, error: err };
1187
- }
1188
- }));
1189
- // Send to worker for parsing, store results on main thread
1190
- for (const { filePath, content, stats, error } of fileContents) {
1191
- if (signal?.aborted) {
1192
- if (parseWorker)
1193
- parseWorker.terminate().catch(() => { });
1194
- return {
1195
- success: false,
1196
- filesIndexed,
1197
- filesSkipped,
1198
- filesErrored,
1199
- nodesCreated: totalNodes,
1200
- edgesCreated: totalEdges,
1201
- errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
1202
- durationMs: Date.now() - startTime,
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
- if (error || content === null || stats === null) {
1213
- processed++;
1214
- filesErrored++;
1215
- errors.push({
1216
- message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
1217
- filePath,
1218
- severity: 'error',
1219
- code: 'read_error',
1220
- });
1221
- continue;
1222
- }
1223
- // Honour MAX_FILE_SIZE. Without this check, vendored generated
1224
- // headers, minified bundles, and other multi-MB files get indexed,
1225
- // wasting WASM heap and the worker recycle budget on inputs with no
1226
- // useful symbols. The single-file extractFile path already enforces
1227
- // this; the bulk path used to silently skip the check.
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
- // doesn't misleadingly report "No files found to index".
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
- // Report 100% so the progress bar doesn't hang at 99%
1293
- onProgress?.({
1294
- phase: 'parsing',
1295
- current: total,
1296
- total,
1297
- });
1298
- // Yield so the shimmer worker's buffered stdout writes can flush.
1299
- // Worker thread stdout is proxied through the main thread's event loop,
1300
- // so synchronous work here blocks the animation from rendering.
1301
- await new Promise(resolve => setImmediate(resolve));
1302
- // Retry pass: files that failed due to WASM memory corruption may succeed
1303
- // on a fresh worker with a clean heap. Recycle before each attempt so
1304
- // every file gets the absolute cleanest WASM state possible.
1305
- const retryableErrors = errors.filter((e) => e.code === 'parse_error' && e.filePath &&
1306
- (e.message.includes('Worker exited') || e.message.includes('memory access out of bounds')));
1307
- if (retryableErrors.length > 0 && WorkerClass) {
1308
- log(`Retrying ${retryableErrors.length} files that failed due to WASM memory errors...`);
1309
- const stillFailing = [];
1310
- for (const errEntry of retryableErrors) {
1311
- const filePath = errEntry.filePath;
1312
- if (signal?.aborted)
1313
- break;
1314
- // Fresh worker for every retry — maximum WASM headroom
1315
- recycleWorker();
1316
- let content;
1317
- try {
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
- let result;
1327
- try {
1328
- result = await requestParse(filePath, content);
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
- catch {
1331
- stillFailing.push(errEntry);
1332
- continue;
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
- if (result.nodes.length > 0 || result.errors.length === 0) {
1335
- const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1336
- const stats = await fsp.stat(path.join(this.rootDir, filePath));
1337
- this.storeExtractionResult(filePath, content, language, stats, result);
1338
- const idx = errors.indexOf(errEntry);
1339
- if (idx >= 0)
1340
- errors.splice(idx, 1);
1341
- filesErrored--;
1342
- filesIndexed++;
1343
- totalNodes += result.nodes.length;
1344
- totalEdges += result.edges.length;
1345
- log(`Retry OK: ${filePath} (${result.nodes.length} nodes)`);
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
- // Last resort: for files that still crash on a clean worker, strip
1349
- // comment-only lines to reduce WASM memory pressure. Many compiler
1350
- // test files are 90%+ comments (CHECK directives) that don't contribute
1351
- // code nodes but consume parser memory.
1352
- if (stillFailing.length > 0) {
1353
- log(`${stillFailing.length} files still failing — retrying with comments stripped...`);
1354
- for (const errEntry of stillFailing) {
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
- recycleWorker();
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
- fullContent = await fsp.readFile(fullPath, 'utf-8');
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 requestParse(filePath, stripped);
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, fullContent, overrides);
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, fullContent, language, stats, result);
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 (stripped) OK: ${filePath} (${result.nodes.length} nodes)`);
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
- // Shut down parse worker and clear any pending timers
1399
- rejectAllPending('Indexing complete');
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
- for (let i = 0; i < filesToIndex.length; i++) {
1772
- const filePath = filesToIndex[i];
1773
- onProgress?.({
1774
- phase: 'parsing',
1775
- current: i + 1,
1776
- total,
1777
- currentFile: filePath,
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 result = await this.indexFile(filePath);
1780
- nodesUpdated += result.nodes.length;
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,