homegraph 1.1.3 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (412) hide show
  1. package/README.md +43 -18
  2. package/dist/arkts/ohos-api-index.d.ts +15 -0
  3. package/dist/arkts/ohos-api-index.d.ts.map +1 -0
  4. package/dist/arkts/ohos-api-index.js +190 -0
  5. package/dist/arkts/ohos-api-index.js.map +1 -0
  6. package/dist/arkts/ohos-sdk-input.d.ts +36 -0
  7. package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
  8. package/dist/arkts/ohos-sdk-input.js +214 -0
  9. package/dist/arkts/ohos-sdk-input.js.map +1 -0
  10. package/dist/bin/command-supervision.d.ts +12 -0
  11. package/dist/bin/command-supervision.d.ts.map +1 -0
  12. package/dist/bin/command-supervision.js +86 -0
  13. package/dist/bin/command-supervision.js.map +1 -0
  14. package/dist/bin/homegraph.d.ts +1 -1
  15. package/dist/bin/homegraph.js +709 -168
  16. package/dist/bin/homegraph.js.map +1 -1
  17. package/dist/context/index.d.ts.map +1 -1
  18. package/dist/context/index.js +85 -0
  19. package/dist/context/index.js.map +1 -1
  20. package/dist/db/index.d.ts +84 -5
  21. package/dist/db/index.d.ts.map +1 -1
  22. package/dist/db/index.js +217 -14
  23. package/dist/db/index.js.map +1 -1
  24. package/dist/db/migrations.d.ts +1 -1
  25. package/dist/db/migrations.d.ts.map +1 -1
  26. package/dist/db/migrations.js +58 -1
  27. package/dist/db/migrations.js.map +1 -1
  28. package/dist/db/queries.d.ts +131 -2
  29. package/dist/db/queries.d.ts.map +1 -1
  30. package/dist/db/queries.js +473 -9
  31. package/dist/db/queries.js.map +1 -1
  32. package/dist/db/schema.sql +52 -1
  33. package/dist/db/wal-valve.d.ts +106 -0
  34. package/dist/db/wal-valve.d.ts.map +1 -0
  35. package/dist/db/wal-valve.js +208 -0
  36. package/dist/db/wal-valve.js.map +1 -0
  37. package/dist/directory.d.ts +36 -0
  38. package/dist/directory.d.ts.map +1 -1
  39. package/dist/directory.js +274 -0
  40. package/dist/directory.js.map +1 -1
  41. package/dist/extraction/arkts-batch-worker.d.ts +2 -0
  42. package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
  43. package/dist/extraction/arkts-batch-worker.js +28 -0
  44. package/dist/extraction/arkts-batch-worker.js.map +1 -0
  45. package/dist/extraction/cfml-extractor.d.ts +107 -0
  46. package/dist/extraction/cfml-extractor.d.ts.map +1 -0
  47. package/dist/extraction/cfml-extractor.js +494 -0
  48. package/dist/extraction/cfml-extractor.js.map +1 -0
  49. package/dist/extraction/context.d.ts +11 -0
  50. package/dist/extraction/context.d.ts.map +1 -1
  51. package/dist/extraction/context.js +20 -0
  52. package/dist/extraction/context.js.map +1 -1
  53. package/dist/extraction/grammars.d.ts +25 -1
  54. package/dist/extraction/grammars.d.ts.map +1 -1
  55. package/dist/extraction/grammars.js +177 -24
  56. package/dist/extraction/grammars.js.map +1 -1
  57. package/dist/extraction/index.d.ts +55 -2
  58. package/dist/extraction/index.d.ts.map +1 -1
  59. package/dist/extraction/index.js +923 -381
  60. package/dist/extraction/index.js.map +1 -1
  61. package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
  62. package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
  63. package/dist/extraction/languages/arkts-state-decorators.js +26 -0
  64. package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
  65. package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
  66. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
  67. package/dist/extraction/languages/arkts-viewtree.js +21 -6
  68. package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
  69. package/dist/extraction/languages/arkts.d.ts +98 -2
  70. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  71. package/dist/extraction/languages/arkts.js +1290 -61
  72. package/dist/extraction/languages/arkts.js.map +1 -1
  73. package/dist/extraction/languages/c-cpp.d.ts +98 -0
  74. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  75. package/dist/extraction/languages/c-cpp.js +563 -1
  76. package/dist/extraction/languages/c-cpp.js.map +1 -1
  77. package/dist/extraction/languages/cfquery.d.ts +12 -0
  78. package/dist/extraction/languages/cfquery.d.ts.map +1 -0
  79. package/dist/extraction/languages/cfquery.js +28 -0
  80. package/dist/extraction/languages/cfquery.js.map +1 -0
  81. package/dist/extraction/languages/cfscript.d.ts +3 -0
  82. package/dist/extraction/languages/cfscript.d.ts.map +1 -0
  83. package/dist/extraction/languages/cfscript.js +73 -0
  84. package/dist/extraction/languages/cfscript.js.map +1 -0
  85. package/dist/extraction/languages/cobol.d.ts +33 -0
  86. package/dist/extraction/languages/cobol.d.ts.map +1 -0
  87. package/dist/extraction/languages/cobol.js +499 -0
  88. package/dist/extraction/languages/cobol.js.map +1 -0
  89. package/dist/extraction/languages/erlang.d.ts +3 -0
  90. package/dist/extraction/languages/erlang.d.ts.map +1 -0
  91. package/dist/extraction/languages/erlang.js +350 -0
  92. package/dist/extraction/languages/erlang.js.map +1 -0
  93. package/dist/extraction/languages/index.d.ts.map +1 -1
  94. package/dist/extraction/languages/index.js +16 -0
  95. package/dist/extraction/languages/index.js.map +1 -1
  96. package/dist/extraction/languages/nix.d.ts +3 -0
  97. package/dist/extraction/languages/nix.d.ts.map +1 -0
  98. package/dist/extraction/languages/nix.js +294 -0
  99. package/dist/extraction/languages/nix.js.map +1 -0
  100. package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
  101. package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
  102. package/dist/extraction/languages/ohos-api-consumer.js +283 -0
  103. package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
  104. package/dist/extraction/languages/solidity.d.ts +3 -0
  105. package/dist/extraction/languages/solidity.d.ts.map +1 -0
  106. package/dist/extraction/languages/solidity.js +293 -0
  107. package/dist/extraction/languages/solidity.js.map +1 -0
  108. package/dist/extraction/languages/terraform.d.ts +3 -0
  109. package/dist/extraction/languages/terraform.d.ts.map +1 -0
  110. package/dist/extraction/languages/terraform.js +641 -0
  111. package/dist/extraction/languages/terraform.js.map +1 -0
  112. package/dist/extraction/languages/vbnet.d.ts +11 -0
  113. package/dist/extraction/languages/vbnet.d.ts.map +1 -0
  114. package/dist/extraction/languages/vbnet.js +141 -0
  115. package/dist/extraction/languages/vbnet.js.map +1 -0
  116. package/dist/extraction/mybatis-extractor.d.ts +30 -10
  117. package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  118. package/dist/extraction/mybatis-extractor.js +140 -38
  119. package/dist/extraction/mybatis-extractor.js.map +1 -1
  120. package/dist/extraction/parse-pool.d.ts +152 -0
  121. package/dist/extraction/parse-pool.d.ts.map +1 -0
  122. package/dist/extraction/parse-pool.js +387 -0
  123. package/dist/extraction/parse-pool.js.map +1 -0
  124. package/dist/extraction/parse-worker.js +9 -2
  125. package/dist/extraction/parse-worker.js.map +1 -1
  126. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  127. package/dist/extraction/tree-sitter-helpers.js +1 -0
  128. package/dist/extraction/tree-sitter-helpers.js.map +1 -1
  129. package/dist/extraction/tree-sitter-types.d.ts +20 -1
  130. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  131. package/dist/extraction/tree-sitter.d.ts +59 -0
  132. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  133. package/dist/extraction/tree-sitter.js +952 -37
  134. package/dist/extraction/tree-sitter.js.map +1 -1
  135. package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
  136. package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
  137. package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
  138. package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
  139. package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
  140. package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
  141. package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
  142. package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
  143. package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
  144. package/dist/graph/traversal.d.ts.map +1 -1
  145. package/dist/graph/traversal.js +77 -18
  146. package/dist/graph/traversal.js.map +1 -1
  147. package/dist/index.d.ts +85 -2
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +426 -9
  150. package/dist/index.js.map +1 -1
  151. package/dist/installer/index.d.ts +42 -0
  152. package/dist/installer/index.d.ts.map +1 -1
  153. package/dist/installer/index.js +97 -2
  154. package/dist/installer/index.js.map +1 -1
  155. package/dist/mcp/daemon-paths.d.ts +30 -3
  156. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  157. package/dist/mcp/daemon-paths.js +50 -10
  158. package/dist/mcp/daemon-paths.js.map +1 -1
  159. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  160. package/dist/mcp/daemon-registry.js +7 -3
  161. package/dist/mcp/daemon-registry.js.map +1 -1
  162. package/dist/mcp/daemon.d.ts +73 -3
  163. package/dist/mcp/daemon.d.ts.map +1 -1
  164. package/dist/mcp/daemon.js +258 -40
  165. package/dist/mcp/daemon.js.map +1 -1
  166. package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  167. package/dist/mcp/dynamic-boundaries.js +2 -1
  168. package/dist/mcp/dynamic-boundaries.js.map +1 -1
  169. package/dist/mcp/early-ppid.d.ts +26 -0
  170. package/dist/mcp/early-ppid.d.ts.map +1 -0
  171. package/dist/mcp/early-ppid.js +29 -0
  172. package/dist/mcp/early-ppid.js.map +1 -0
  173. package/dist/mcp/engine.d.ts +16 -0
  174. package/dist/mcp/engine.d.ts.map +1 -1
  175. package/dist/mcp/engine.js +100 -2
  176. package/dist/mcp/engine.js.map +1 -1
  177. package/dist/mcp/index.d.ts.map +1 -1
  178. package/dist/mcp/index.js +66 -49
  179. package/dist/mcp/index.js.map +1 -1
  180. package/dist/mcp/liveness-watchdog.d.ts +18 -1
  181. package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  182. package/dist/mcp/liveness-watchdog.js +73 -13
  183. package/dist/mcp/liveness-watchdog.js.map +1 -1
  184. package/dist/mcp/memory-budget.d.ts +30 -0
  185. package/dist/mcp/memory-budget.d.ts.map +1 -0
  186. package/dist/mcp/memory-budget.js +123 -0
  187. package/dist/mcp/memory-budget.js.map +1 -0
  188. package/dist/mcp/ppid-watchdog.d.ts +18 -0
  189. package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
  190. package/dist/mcp/ppid-watchdog.js +37 -0
  191. package/dist/mcp/ppid-watchdog.js.map +1 -1
  192. package/dist/mcp/proxy.d.ts.map +1 -1
  193. package/dist/mcp/proxy.js +88 -4
  194. package/dist/mcp/proxy.js.map +1 -1
  195. package/dist/mcp/query-cache.d.ts +56 -0
  196. package/dist/mcp/query-cache.d.ts.map +1 -0
  197. package/dist/mcp/query-cache.js +261 -0
  198. package/dist/mcp/query-cache.js.map +1 -0
  199. package/dist/mcp/query-pool.d.ts +128 -0
  200. package/dist/mcp/query-pool.d.ts.map +1 -0
  201. package/dist/mcp/query-pool.js +374 -0
  202. package/dist/mcp/query-pool.js.map +1 -0
  203. package/dist/mcp/query-worker.d.ts +24 -0
  204. package/dist/mcp/query-worker.d.ts.map +1 -0
  205. package/dist/mcp/query-worker.js +87 -0
  206. package/dist/mcp/query-worker.js.map +1 -0
  207. package/dist/mcp/server-instructions.d.ts +4 -29
  208. package/dist/mcp/server-instructions.d.ts.map +1 -1
  209. package/dist/mcp/server-instructions.js +39 -86
  210. package/dist/mcp/server-instructions.js.map +1 -1
  211. package/dist/mcp/session.d.ts +14 -0
  212. package/dist/mcp/session.d.ts.map +1 -1
  213. package/dist/mcp/session.js +28 -1
  214. package/dist/mcp/session.js.map +1 -1
  215. package/dist/mcp/startup-handshake.d.ts +44 -0
  216. package/dist/mcp/startup-handshake.d.ts.map +1 -0
  217. package/dist/mcp/startup-handshake.js +73 -0
  218. package/dist/mcp/startup-handshake.js.map +1 -0
  219. package/dist/mcp/tools.d.ts +179 -2
  220. package/dist/mcp/tools.d.ts.map +1 -1
  221. package/dist/mcp/tools.js +3027 -192
  222. package/dist/mcp/tools.js.map +1 -1
  223. package/dist/mcp/transport.d.ts.map +1 -1
  224. package/dist/mcp/transport.js +18 -0
  225. package/dist/mcp/transport.js.map +1 -1
  226. package/dist/project-config.d.ts +58 -0
  227. package/dist/project-config.d.ts.map +1 -1
  228. package/dist/project-config.js +141 -2
  229. package/dist/project-config.js.map +1 -1
  230. package/dist/reasoning/login.js +1 -1
  231. package/dist/reasoning/login.js.map +1 -1
  232. package/dist/resolution/c-fnptr-synthesizer.d.ts +2 -29
  233. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  234. package/dist/resolution/c-fnptr-synthesizer.js +866 -148
  235. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  236. package/dist/resolution/callback-synthesizer.d.ts +9 -1
  237. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  238. package/dist/resolution/callback-synthesizer.js +1232 -183
  239. package/dist/resolution/callback-synthesizer.js.map +1 -1
  240. package/dist/resolution/cooperative-yield.d.ts +32 -0
  241. package/dist/resolution/cooperative-yield.d.ts.map +1 -0
  242. package/dist/resolution/cooperative-yield.js +42 -0
  243. package/dist/resolution/cooperative-yield.js.map +1 -0
  244. package/dist/resolution/frameworks/cics.d.ts +20 -0
  245. package/dist/resolution/frameworks/cics.d.ts.map +1 -0
  246. package/dist/resolution/frameworks/cics.js +90 -0
  247. package/dist/resolution/frameworks/cics.js.map +1 -0
  248. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  249. package/dist/resolution/frameworks/index.js +3 -0
  250. package/dist/resolution/frameworks/index.js.map +1 -1
  251. package/dist/resolution/frameworks/java.d.ts.map +1 -1
  252. package/dist/resolution/frameworks/java.js +14 -6
  253. package/dist/resolution/frameworks/java.js.map +1 -1
  254. package/dist/resolution/frameworks/terraform.d.ts +38 -0
  255. package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
  256. package/dist/resolution/frameworks/terraform.js +277 -0
  257. package/dist/resolution/frameworks/terraform.js.map +1 -0
  258. package/dist/resolution/goframe-synthesizer.d.ts +2 -1
  259. package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
  260. package/dist/resolution/goframe-synthesizer.js +8 -3
  261. package/dist/resolution/goframe-synthesizer.js.map +1 -1
  262. package/dist/resolution/import-resolver.d.ts +7 -0
  263. package/dist/resolution/import-resolver.d.ts.map +1 -1
  264. package/dist/resolution/import-resolver.js +129 -4
  265. package/dist/resolution/import-resolver.js.map +1 -1
  266. package/dist/resolution/index.d.ts +65 -3
  267. package/dist/resolution/index.d.ts.map +1 -1
  268. package/dist/resolution/index.js +477 -59
  269. package/dist/resolution/index.js.map +1 -1
  270. package/dist/resolution/name-matcher.d.ts +22 -3
  271. package/dist/resolution/name-matcher.d.ts.map +1 -1
  272. package/dist/resolution/name-matcher.js +657 -39
  273. package/dist/resolution/name-matcher.js.map +1 -1
  274. package/dist/resolution/strip-comments.d.ts +1 -1
  275. package/dist/resolution/strip-comments.d.ts.map +1 -1
  276. package/dist/resolution/strip-comments.js +49 -0
  277. package/dist/resolution/strip-comments.js.map +1 -1
  278. package/dist/resolution/types.d.ts +29 -0
  279. package/dist/resolution/types.d.ts.map +1 -1
  280. package/dist/resolution/workspace-packages.d.ts +10 -0
  281. package/dist/resolution/workspace-packages.d.ts.map +1 -1
  282. package/dist/resolution/workspace-packages.js +142 -4
  283. package/dist/resolution/workspace-packages.js.map +1 -1
  284. package/dist/search/identifier-segments.d.ts +60 -0
  285. package/dist/search/identifier-segments.d.ts.map +1 -0
  286. package/dist/search/identifier-segments.js +176 -0
  287. package/dist/search/identifier-segments.js.map +1 -0
  288. package/dist/search/query-utils.d.ts +201 -0
  289. package/dist/search/query-utils.d.ts.map +1 -1
  290. package/dist/search/query-utils.js +919 -2
  291. package/dist/search/query-utils.js.map +1 -1
  292. package/dist/spec/config.d.ts +7 -11
  293. package/dist/spec/config.d.ts.map +1 -1
  294. package/dist/spec/config.js +77 -48
  295. package/dist/spec/config.js.map +1 -1
  296. package/dist/spec/db/fragment-node.d.ts +1 -0
  297. package/dist/spec/db/fragment-node.d.ts.map +1 -1
  298. package/dist/spec/db/fragment-node.js +8 -0
  299. package/dist/spec/db/fragment-node.js.map +1 -1
  300. package/dist/spec/db/fts.d.ts +14 -0
  301. package/dist/spec/db/fts.d.ts.map +1 -1
  302. package/dist/spec/db/fts.js +43 -4
  303. package/dist/spec/db/fts.js.map +1 -1
  304. package/dist/spec/db/schema.d.ts +6 -3
  305. package/dist/spec/db/schema.d.ts.map +1 -1
  306. package/dist/spec/db/schema.js +36 -4
  307. package/dist/spec/db/schema.js.map +1 -1
  308. package/dist/spec/db/schema.sql +11 -1
  309. package/dist/spec/evolve/logic-checker.d.ts +2 -2
  310. package/dist/spec/evolve/logic-checker.d.ts.map +1 -1
  311. package/dist/spec/evolve/logic-checker.js +3 -27
  312. package/dist/spec/evolve/logic-checker.js.map +1 -1
  313. package/dist/spec/evolve/pipeline.d.ts +21 -19
  314. package/dist/spec/evolve/pipeline.d.ts.map +1 -1
  315. package/dist/spec/evolve/pipeline.js +284 -31
  316. package/dist/spec/evolve/pipeline.js.map +1 -1
  317. package/dist/spec/evolve/spec-rewriter.d.ts +2 -2
  318. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
  319. package/dist/spec/evolve/spec-rewriter.js +7 -31
  320. package/dist/spec/evolve/spec-rewriter.js.map +1 -1
  321. package/dist/spec/graph/queries.d.ts +97 -0
  322. package/dist/spec/graph/queries.d.ts.map +1 -1
  323. package/dist/spec/graph/queries.js +225 -0
  324. package/dist/spec/graph/queries.js.map +1 -1
  325. package/dist/spec/llm/client.d.ts +29 -0
  326. package/dist/spec/llm/client.d.ts.map +1 -0
  327. package/dist/spec/llm/client.js +123 -0
  328. package/dist/spec/llm/client.js.map +1 -0
  329. package/dist/spec/llm/index.d.ts +3 -0
  330. package/dist/spec/llm/index.d.ts.map +1 -0
  331. package/dist/spec/llm/index.js +11 -0
  332. package/dist/spec/llm/index.js.map +1 -0
  333. package/dist/spec/llm/prompts.d.ts +13 -0
  334. package/dist/spec/llm/prompts.d.ts.map +1 -0
  335. package/dist/spec/llm/prompts.js +75 -0
  336. package/dist/spec/llm/prompts.js.map +1 -0
  337. package/dist/spec/mining/git-scanner.d.ts +12 -0
  338. package/dist/spec/mining/git-scanner.d.ts.map +1 -1
  339. package/dist/spec/mining/git-scanner.js +41 -0
  340. package/dist/spec/mining/git-scanner.js.map +1 -1
  341. package/dist/spec/mining/pipeline.d.ts.map +1 -1
  342. package/dist/spec/mining/pipeline.js +14 -1
  343. package/dist/spec/mining/pipeline.js.map +1 -1
  344. package/dist/spec/utils.d.ts +2 -1
  345. package/dist/spec/utils.d.ts.map +1 -1
  346. package/dist/spec/utils.js +3 -1
  347. package/dist/spec/utils.js.map +1 -1
  348. package/dist/sync/git-hooks.d.ts.map +1 -1
  349. package/dist/sync/git-hooks.js +2 -0
  350. package/dist/sync/git-hooks.js.map +1 -1
  351. package/dist/sync/watcher.d.ts +10 -5
  352. package/dist/sync/watcher.d.ts.map +1 -1
  353. package/dist/sync/watcher.js +51 -14
  354. package/dist/sync/watcher.js.map +1 -1
  355. package/dist/sync/worktree.d.ts +9 -0
  356. package/dist/sync/worktree.d.ts.map +1 -1
  357. package/dist/sync/worktree.js +45 -0
  358. package/dist/sync/worktree.js.map +1 -1
  359. package/dist/types.d.ts +25 -2
  360. package/dist/types.d.ts.map +1 -1
  361. package/dist/types.js +10 -0
  362. package/dist/types.js.map +1 -1
  363. package/dist/ui/shimmer-progress.d.ts +2 -0
  364. package/dist/ui/shimmer-progress.d.ts.map +1 -1
  365. package/dist/ui/shimmer-progress.js +19 -2
  366. package/dist/ui/shimmer-progress.js.map +1 -1
  367. package/dist/upgrade/index.d.ts +32 -0
  368. package/dist/upgrade/index.d.ts.map +1 -1
  369. package/dist/upgrade/index.js +163 -15
  370. package/dist/upgrade/index.js.map +1 -1
  371. package/dist/upgrade/remove-binary.d.ts +87 -0
  372. package/dist/upgrade/remove-binary.d.ts.map +1 -0
  373. package/dist/upgrade/remove-binary.js +289 -0
  374. package/dist/upgrade/remove-binary.js.map +1 -0
  375. package/dist/upgrade/update-check.d.ts +92 -0
  376. package/dist/upgrade/update-check.d.ts.map +1 -0
  377. package/dist/upgrade/update-check.js +258 -0
  378. package/dist/upgrade/update-check.js.map +1 -0
  379. package/package.json +4 -4
  380. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
  381. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
  382. package/scripts/agent-eval/run-all.sh +6 -0
  383. package/scripts/build-bundle.sh +5 -0
  384. package/scripts/exp_boundary_eval/README.md +247 -0
  385. package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
  386. package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
  387. package/scripts/exp_boundary_eval/_utils.py +1116 -0
  388. package/scripts/exp_boundary_eval/analyze.py +1313 -0
  389. package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
  390. package/scripts/exp_boundary_eval/run_all.py +378 -0
  391. package/scripts/exp_boundary_eval/run_one.py +165 -0
  392. package/scripts/exp_boundary_eval/run_session.py +158 -0
  393. package/scripts/exp_boundary_eval/setup.py +120 -0
  394. package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
  395. package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
  396. package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
  397. package/scripts/npm-shim.js +32 -3
  398. package/scripts/ohos-sdk-publish.mjs +133 -0
  399. package/scripts/qa_eval/README.md +0 -407
  400. package/scripts/qa_eval/_test_deveco_probe.py +0 -41
  401. package/scripts/qa_eval/agent_runner.py +0 -526
  402. package/scripts/qa_eval/data/.gitignore +0 -4
  403. package/scripts/qa_eval/data/test-set.jsonl +0 -2
  404. package/scripts/qa_eval/eval_metrics.py +0 -274
  405. package/scripts/qa_eval/external_agent.py +0 -976
  406. package/scripts/qa_eval/llm_config.py +0 -92
  407. package/scripts/qa_eval/memory_monitor.py +0 -132
  408. package/scripts/qa_eval/my_answer_accuracy.py +0 -187
  409. package/scripts/qa_eval/requirements.txt +0 -2
  410. package/scripts/qa_eval/run_pipeline.py +0 -804
  411. package/scripts/qa_eval/stats_efficiency.py +0 -279
  412. package/scripts/qa_eval/stats_scores.py +0 -207
@@ -46,14 +46,17 @@ exports.hashContent = hashContent;
46
46
  exports.buildDefaultIgnore = buildDefaultIgnore;
47
47
  exports.buildScopeIgnore = buildScopeIgnore;
48
48
  exports.discoverEmbeddedRepoRoots = discoverEmbeddedRepoRoots;
49
+ exports.findUnindexedIgnoredRepos = findUnindexedIgnoredRepos;
49
50
  exports.scanDirectory = scanDirectory;
50
51
  exports.scanDirectoryAsync = scanDirectoryAsync;
51
52
  const fs = __importStar(require("fs"));
52
53
  const fsp = __importStar(require("fs/promises"));
53
54
  const path = __importStar(require("path"));
55
+ const os = __importStar(require("os"));
54
56
  const crypto = __importStar(require("crypto"));
55
57
  const child_process_1 = require("child_process");
56
58
  const tree_sitter_1 = require("./tree-sitter");
59
+ const parse_pool_1 = require("./parse-pool");
57
60
  const grammars_1 = require("./grammars");
58
61
  const project_config_1 = require("../project-config");
59
62
  const directory_1 = require("../directory");
@@ -61,6 +64,8 @@ const errors_1 = require("../errors");
61
64
  const utils_1 = require("../utils");
62
65
  const ignore_1 = __importDefault(require("ignore"));
63
66
  const frameworks_1 = require("../resolution/frameworks");
67
+ const context_1 = require("./context");
68
+ const arkts_1 = require("./languages/arkts");
64
69
  /**
65
70
  * Number of files to read in parallel during indexing.
66
71
  * File reads are I/O-bound; batching overlaps I/O wait with CPU parse work.
@@ -155,12 +160,34 @@ const DEFAULT_IGNORE_DIRS = new Set([
155
160
  // Generic cache
156
161
  '.cache',
157
162
  ]);
163
+ /**
164
+ * Android resource directory types. A `res/` tree holds ONLY non-code resources —
165
+ * layouts, drawables, value bags (strings/colors/styles), menus, navigation
166
+ * graphs — split into one typed subdirectory per kind, optionally density/locale/
167
+ * version-qualified (`values-es`, `drawable-hdpi`, `layout-v21`, …). None of it
168
+ * yields an extractable code symbol, yet on an Android app it DOMINATES the tree
169
+ * (one report: 26k XML files = 97% of the project, 0 symbols), bloating the DB,
170
+ * slowing indexing, and skewing both the file count and `codegraph_explore`
171
+ * results (#1047). So these are excluded by default. The structure is
172
+ * self-identifying — a non-Android project has no `res/layout/` etc., so it's
173
+ * untouched — and the only XML that DOES produce symbols (MyBatis mappers) lives
174
+ * under `src/main/resources/`, never `res/`, so nothing useful is dropped.
175
+ * `res/raw/` is deliberately NOT here: it holds arbitrary bundled assets that can
176
+ * be code-ish (a `.sql` schema, a `.js`), so we leave it indexed. Override any of
177
+ * these with a `.gitignore` negation (e.g. `!res/values/`).
178
+ */
179
+ const ANDROID_RES_TYPES = [
180
+ 'anim', 'animator', 'color', 'drawable', 'font', 'layout',
181
+ 'menu', 'mipmap', 'navigation', 'transition', 'values', 'xml',
182
+ ];
158
183
  /** Gitignore-style patterns for the `ignore` matcher: the dirs above plus a few globs. */
159
184
  const DEFAULT_IGNORE_PATTERNS = [
160
185
  ...Array.from(DEFAULT_IGNORE_DIRS, (d) => `${d}/`),
161
186
  '*.egg-info/', // Python packaging metadata
162
187
  'cmake-build-*/', // CLion / CMake build trees
163
188
  'bazel-*/', // Bazel output symlink trees
189
+ // Android resource dirs at any depth, with their qualifier variants (#1047).
190
+ ...ANDROID_RES_TYPES.map((t) => `**/res/${t}*/`),
164
191
  ];
165
192
  /** True if `buf` decodes as strict UTF-8 (no invalid byte sequences). */
166
193
  function isValidUtf8(buf) {
@@ -265,6 +292,173 @@ function loadIncludeIgnoredMatcher(rootDir) {
265
292
  const patterns = (0, project_config_1.loadIncludeIgnoredPatterns)(rootDir);
266
293
  return patterns.length > 0 ? (0, ignore_1.default)().add(patterns) : null;
267
294
  }
295
+ /**
296
+ * Matcher for the project's `codegraph.json` `exclude` patterns — paths to keep
297
+ * OUT of the index even when git-tracked, which `.gitignore` cannot do (#999).
298
+ * The escape hatch for a committed vendor/theme/SDK directory. Returns `null`
299
+ * when nothing is excluded (the zero-config default → no overhead). Matched
300
+ * against project-root-relative paths, so it applies uniformly across the whole
301
+ * workspace, including inside embedded repos (excluding `static/` means gone
302
+ * everywhere). Built once per scan/sync/scope operation from the scan root.
303
+ */
304
+ function loadExcludeMatcher(rootDir) {
305
+ const patterns = (0, project_config_1.loadExcludePatterns)(rootDir);
306
+ return patterns.length > 0 ? (0, ignore_1.default)().add(patterns) : null;
307
+ }
308
+ /**
309
+ * Matcher for the project's `homegraph.json` `include` patterns — first-party
310
+ * source to force INTO the index even when `.gitignore` drops it (the general
311
+ * whitelist `includeIgnored` never was — that one only revives *embedded git
312
+ * repos*). The case it exists for: a project under a second VCS (SVN/Perforce)
313
+ * `.gitignore`s its own real source so it stays out of Git, yet we still want it
314
+ * indexed. Returns `null` when nothing is force-included (the zero-config
315
+ * default → no overhead, no extra walk). Built once per scan/sync/scope
316
+ * operation from the scan root.
317
+ */
318
+ function loadIncludeMatcher(rootDir) {
319
+ const patterns = (0, project_config_1.loadIncludePatterns)(rootDir);
320
+ return patterns.length > 0 ? (0, ignore_1.default)().add(patterns) : null;
321
+ }
322
+ /** Glob metacharacters that end the static (literal) prefix of an `include` pattern. */
323
+ const GLOB_META = /[*?[\]{}!]/;
324
+ /**
325
+ * The static directory prefix of each `include` pattern — the literal leading
326
+ * path up to the first glob segment — trailing-slashed, used to (a) walk only
327
+ * the opted-in subtrees in `collectIncludedFiles` and (b) let `ScopeIgnore` keep
328
+ * the watcher descending toward them. `Tools/` stays `Tools/`; a recursive
329
+ * `Tools/**` glob yields `Tools/`; `src/local/file.ts` yields `src/local/` (the
330
+ * file's dir); a pattern that starts with a glob (like a leading `**`) yields
331
+ * `''`, meaning "no static root — walk the whole tree". Duplicates and roots
332
+ * nested under a broader root are collapsed so each subtree is walked once.
333
+ */
334
+ function includeStaticRoots(patterns) {
335
+ const roots = new Set();
336
+ for (const pattern of patterns) {
337
+ let p = pattern.replace(/^\/+/, '');
338
+ const trailingSlash = p.endsWith('/');
339
+ if (trailingSlash)
340
+ p = p.slice(0, -1);
341
+ const segs = p.split('/').filter(Boolean);
342
+ const lead = [];
343
+ for (const s of segs) {
344
+ if (GLOB_META.test(s))
345
+ break;
346
+ lead.push(s);
347
+ }
348
+ const hadWildcard = lead.length < segs.length;
349
+ // A wholly-literal pattern with no trailing slash names a file (or a dir we
350
+ // can't tell apart) — drop its last segment so we walk the containing dir
351
+ // and let the matcher pick the file. A trailing slash or a glob means the
352
+ // remaining `lead` is already the directory to walk.
353
+ if (!hadWildcard && !trailingSlash && lead.length > 0)
354
+ lead.pop();
355
+ if (lead.length === 0) {
356
+ roots.clear();
357
+ roots.add('');
358
+ return ['']; // a top-level glob forces a whole-tree walk; nothing narrower matters
359
+ }
360
+ roots.add(lead.join('/') + '/');
361
+ }
362
+ // Collapse roots nested under a broader one (e.g. drop `a/b/` if `a/` is present).
363
+ const all = [...roots];
364
+ return all.filter((r) => !all.some((other) => other !== r && r.startsWith(other)));
365
+ }
366
+ /**
367
+ * Actively discover the source files an `include` whitelist forces in. `git
368
+ * ls-files` never lists gitignored files, so a filtered filesystem walk of just
369
+ * the opted-in subtrees (`includeStaticRoots`) is the only way to find them.
370
+ * Returns project-root-relative, normalized source-file paths.
371
+ *
372
+ * A file is collected when it MATCHES `include`, is NOT hit by `exclude` (an
373
+ * explicit exclude always wins), is a recognized source file, and does not live
374
+ * under a built-in default-ignored dir (`node_modules`, `dist`, …), `.git`, or
375
+ * HomeGraph's data dir — those are never resurfaced, mirroring `ScopeIgnore`.
376
+ * `.gitignore` is deliberately NOT consulted: overriding it is the whole point.
377
+ */
378
+ function collectIncludedFiles(rootDir, include, exclude, roots, overrides) {
379
+ const out = new Set();
380
+ const defaults = defaultsOnlyIgnore();
381
+ const visited = new Set();
382
+ const consider = (abs, rel, isDir) => {
383
+ if (isDir) {
384
+ if (defaults.ignores(rel + '/'))
385
+ return; // never node_modules/dist/… via include
386
+ // An explicit `exclude` always wins over `include`; prune the whole subtree
387
+ // here so a large excluded dir (a committed frontend's own vendored deps,
388
+ // build output, …) is never walked — the per-file guard below still catches
389
+ // anything a directory pattern doesn't, so this is a pure efficiency win.
390
+ if (exclude && exclude.ignores(rel + '/'))
391
+ return;
392
+ walk(abs);
393
+ }
394
+ else {
395
+ if (defaults.ignores(rel))
396
+ return;
397
+ if (!include.ignores(rel))
398
+ return;
399
+ if (exclude && exclude.ignores(rel))
400
+ return;
401
+ if (!(0, grammars_1.isSourceFile)(rel, overrides))
402
+ return;
403
+ out.add(rel);
404
+ }
405
+ };
406
+ function walk(absDir) {
407
+ let realDir;
408
+ try {
409
+ realDir = fs.realpathSync(absDir);
410
+ }
411
+ catch {
412
+ return;
413
+ }
414
+ if (visited.has(realDir))
415
+ return; // symlink-cycle guard
416
+ visited.add(realDir);
417
+ let entries;
418
+ try {
419
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
420
+ }
421
+ catch {
422
+ return;
423
+ }
424
+ for (const entry of entries) {
425
+ if (entry.name === '.git' || (0, directory_1.isHomeGraphDataDir)(entry.name))
426
+ continue;
427
+ const abs = path.join(absDir, entry.name);
428
+ const rel = (0, utils_1.normalizePath)(path.relative(rootDir, abs));
429
+ if (!rel || rel.startsWith('..'))
430
+ continue;
431
+ if (entry.isSymbolicLink()) {
432
+ try {
433
+ const st = fs.statSync(fs.realpathSync(abs));
434
+ consider(abs, rel, st.isDirectory());
435
+ }
436
+ catch {
437
+ // broken symlink — skip
438
+ }
439
+ continue;
440
+ }
441
+ consider(abs, rel, entry.isDirectory());
442
+ }
443
+ }
444
+ for (const root of roots) {
445
+ walk(root === '' ? rootDir : path.join(rootDir, root));
446
+ }
447
+ return out;
448
+ }
449
+ /**
450
+ * The included source files (`homegraph.json` `include`) for a scan root, or an
451
+ * empty set when nothing is force-included. Centralizes loading the matcher,
452
+ * roots, exclude, and overrides so both enumeration paths (git and filesystem
453
+ * walk) add the same files.
454
+ */
455
+ function collectIncludedFilesForRoot(rootDir) {
456
+ const include = loadIncludeMatcher(rootDir);
457
+ if (!include)
458
+ return new Set();
459
+ const roots = includeStaticRoots((0, project_config_1.loadIncludePatterns)(rootDir));
460
+ return collectIncludedFiles(rootDir, include, loadExcludeMatcher(rootDir), roots, (0, project_config_1.loadExtensionOverrides)(rootDir));
461
+ }
268
462
  /**
269
463
  * `git ls-files --directory` collapses a wholly-untracked/ignored directory into
270
464
  * one entry — and when the command's own cwd is such a directory (the indexed
@@ -404,14 +598,55 @@ function findNestedGitRepos(absDir, relPrefix) {
404
598
  */
405
599
  class ScopeIgnore {
406
600
  rootMatcher;
601
+ exclude;
602
+ include;
603
+ includeRoots;
407
604
  embedded;
408
605
  defaults = defaultsOnlyIgnore();
409
- constructor(rootMatcher, embedded) {
606
+ constructor(rootMatcher, embedded,
607
+ /**
608
+ * Project `homegraph.json` `exclude` patterns (#999), matched against the
609
+ * full root-relative path. Wins over everything else — an explicit user
610
+ * exclude applies even to tracked files and even inside embedded repos.
611
+ */
612
+ exclude = null,
613
+ /**
614
+ * Project `homegraph.json` `include` patterns — first-party source forced
615
+ * INTO the index despite `.gitignore`. When a path matches, it is NOT
616
+ * ignored (so the watcher watches it), overriding `.gitignore`/`rootMatcher`
617
+ * — but never `exclude` (checked first) and never a built-in default-ignored
618
+ * dir. `includeRoots` are the static prefixes so a gitignored ANCESTOR
619
+ * directory of an included subtree still isn't pruned by the directory
620
+ * walker/watcher.
621
+ */
622
+ include = null, includeRoots = []) {
410
623
  this.rootMatcher = rootMatcher;
624
+ this.exclude = exclude;
625
+ this.include = include;
626
+ this.includeRoots = includeRoots;
411
627
  // Longest root first so paths in nested embedded repos hit the innermost matcher.
412
628
  this.embedded = [...embedded].sort((a, b) => b.root.length - a.root.length);
413
629
  }
414
630
  ignores(rel) {
631
+ // User `exclude` (#999) is checked first and against the full root-relative
632
+ // path: it must drop git-TRACKED paths (which `.gitignore` can't) and apply
633
+ // everywhere, including ancestors of embedded repos.
634
+ if (this.exclude && this.exclude.ignores(rel))
635
+ return true;
636
+ // User `include`: force first-party source in despite `.gitignore`. Never
637
+ // resurfaces a built-in default-ignored dir (node_modules/dist/…), so an
638
+ // include pattern can't accidentally pull in dependency/build trees.
639
+ if (this.include && !this.defaults.ignores(rel)) {
640
+ if (rel.endsWith('/')) {
641
+ // A directory on (or leading to) an included subtree must stay walkable
642
+ // so the watcher/walker descends to reach the forced-in files.
643
+ if (this.includeRoots.some((r) => r.startsWith(rel) || rel.startsWith(r)))
644
+ return false;
645
+ }
646
+ else if (this.include.ignores(rel)) {
647
+ return false;
648
+ }
649
+ }
415
650
  for (const { root, matcher } of this.embedded) {
416
651
  if (rel.startsWith(root)) {
417
652
  const inner = rel.slice(root.length);
@@ -438,7 +673,37 @@ exports.ScopeIgnore = ScopeIgnore;
438
673
  */
439
674
  function buildScopeIgnore(rootDir, embeddedRoots) {
440
675
  const roots = embeddedRoots ? [...embeddedRoots] : discoverEmbeddedRepoRoots(rootDir);
441
- return new ScopeIgnore(buildDefaultIgnore(rootDir), roots.map((root) => ({ root, matcher: buildDefaultIgnore(path.join(rootDir, root)) })));
676
+ const include = loadIncludeMatcher(rootDir);
677
+ return new ScopeIgnore(buildDefaultIgnore(rootDir), roots.map((root) => ({ root, matcher: buildDefaultIgnore(path.join(rootDir, root)) })), loadExcludeMatcher(rootDir), include, include ? includeStaticRoots((0, project_config_1.loadIncludePatterns)(rootDir)) : []);
678
+ }
679
+ /**
680
+ * Whether an embedded repo found as a tracked gitlink (mode 160000, #1031/#1033)
681
+ * must be SKIPPED rather than indexed. A gitlink is tracked, so `.gitignore`
682
+ * can't untrack it — but the discovery passes for it must still honor the same
683
+ * scope rules as every other path, or a gitignored reference/data dir full of
684
+ * `git add`ed clones gets pulled into the index against the user's stated intent
685
+ * (#1065). Two reasons to skip:
686
+ * 1. It sits in a built-in default-ignored location — an npm git-dependency
687
+ * under `node_modules` is never project code; not even an explicit opt-in
688
+ * revives it (matches `findIgnoredEmbeddedRepos`).
689
+ * 2. The parent repo's own `.gitignore` covers its path and the project did
690
+ * NOT opt that path in via `homegraph.json` `includeIgnored`. The gitignore
691
+ * rule is the user's stated intent to keep that path out of scope, exactly
692
+ * as for an UNtracked embedded repo — respect it by default, opt back in
693
+ * with `includeIgnored` (#514, #970, #976).
694
+ * `relDir` is repoDir-relative (trailing-slashed); `prefix` is repoDir's
695
+ * scan-root-relative path so the `includeIgnored` pattern is matched on the full
696
+ * scan-root-relative path. `defaults` is `defaultsOnlyIgnore()` and `repoIgnore`
697
+ * is `buildDefaultIgnore(repoDir)` (defaults + the repo's own `.gitignore`),
698
+ * both passed in so they're built once per repo level rather than per gitlink.
699
+ */
700
+ function gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored) {
701
+ if (defaults.ignores(relDir))
702
+ return true; // default-ignored — never index, opt-in can't revive
703
+ if (!repoIgnore.ignores(relDir))
704
+ return false; // not ignored at all — index as before (#1031/#1033)
705
+ // Gitignored by the repo's own rules — skip unless the project opted it in.
706
+ return !includeIgnored?.ignores((0, utils_1.normalizePath)(prefix + relDir));
442
707
  }
443
708
  /**
444
709
  * Standalone discovery of every embedded repo root under `rootDir` (relative,
@@ -470,6 +735,25 @@ function discoverEmbeddedRepoRoots(rootDir) {
470
735
  }
471
736
  }
472
737
  catch { /* untracked listing failed — ignored-side discovery still runs */ }
738
+ // Unexpanded gitlinks (mode 160000) with a real checkout on disk — embedded
739
+ // repos `git add`ed without `.gitmodules`, or submodules not active here. The
740
+ // untracked listing above can't see them (they're tracked), so find them the
741
+ // same way collectGitFiles does, keeping watcher scope == indexer scope.
742
+ // (#1031, #1033)
743
+ try {
744
+ const gitOpts = { cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
745
+ const repoIgnore = buildDefaultIgnore(repoAbs);
746
+ for (const rel of listGitlinkRels(gitOpts)) {
747
+ const relDir = rel.endsWith('/') ? rel : rel + '/';
748
+ // A gitlink under a gitignored path is respected (not indexed) unless the
749
+ // project opted it in — same rule as the untracked-ignored kind (#1065).
750
+ if (gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored))
751
+ continue;
752
+ if (classifyGitDir(path.join(repoAbs, rel)) === 'embedded')
753
+ candidates.push(relDir);
754
+ }
755
+ }
756
+ catch { /* staged listing failed — other discovery still runs */ }
473
757
  candidates.push(...findIgnoredEmbeddedRepos(repoAbs, includeIgnored, prefix));
474
758
  for (const rel of candidates) {
475
759
  const full = (0, utils_1.normalizePath)(prefix + rel);
@@ -480,6 +764,50 @@ function discoverEmbeddedRepoRoots(rootDir) {
480
764
  visit(rootDir, '');
481
765
  return out;
482
766
  }
767
+ /**
768
+ * Cap on how many skipped gitignored repos the CLI hint enumerates — a huge
769
+ * gitignored data dir full of clones must never turn the hint scan into a long
770
+ * walk. Enough to make the point; the caller says "+N more" past this.
771
+ */
772
+ const UNINDEXED_IGNORED_REPO_HINT_CAP = 100;
773
+ /**
774
+ * The INVERSE of the gitignored side of {@link discoverEmbeddedRepoRoots}:
775
+ * nested git repositories under a gitignored directory that the project has NOT
776
+ * opted into via `homegraph.json` `includeIgnored`. These are real repos the
777
+ * default `init`/`index` deliberately skips because `.gitignore` excludes them
778
+ * (#970, #976) — most visibly the "super-repo `.gitignore`s its child repos"
779
+ * layout (#1156), where `init` at the parent correctly indexes ~nothing while
780
+ * `init` inside each child works. The CLI uses this to turn that silent empty
781
+ * index into an actionable hint: it names the skipped repos and offers to opt
782
+ * them in. Paths are `rootDir`-relative and trailing-slashed (valid
783
+ * `includeIgnored` patterns as-is). Returns `[]` for a non-git root (a
784
+ * filesystem walk already descends into nested repos there), skips built-in
785
+ * default-ignored dirs (`node_modules`, …), and is bounded so it never stalls
786
+ * on a giant ignored tree.
787
+ */
788
+ function findUnindexedIgnoredRepos(rootDir) {
789
+ try {
790
+ (0, child_process_1.execFileSync)('git', ['rev-parse', '--git-dir'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
791
+ }
792
+ catch {
793
+ return [];
794
+ }
795
+ const defaults = defaultsOnlyIgnore();
796
+ const includeIgnored = loadIncludeIgnoredMatcher(rootDir);
797
+ const repos = [];
798
+ for (const dir of listIgnoredDirs(rootDir)) {
799
+ if (defaults.ignores(dir))
800
+ continue; // node_modules etc. — never project code
801
+ if (includeIgnored?.ignores((0, utils_1.normalizePath)(dir)))
802
+ continue; // already opted in — nothing to nag about
803
+ for (const repo of findNestedGitRepos(path.join(rootDir, dir), dir)) {
804
+ repos.push(repo);
805
+ if (repos.length >= UNINDEXED_IGNORED_REPO_HINT_CAP)
806
+ return repos;
807
+ }
808
+ }
809
+ return repos;
810
+ }
483
811
  /**
484
812
  * Discover embedded repos hidden by `repoDir`'s OWN gitignore rules: for each
485
813
  * gitignored directory, search for nested `.git` roots. Returns repo paths
@@ -510,24 +838,65 @@ function findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix) {
510
838
  return repos;
511
839
  }
512
840
  /**
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.
841
+ * Collect tracked paths and unexpanded gitlinks (mode 160000) from `git ls-files`.
516
842
  *
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.
843
+ * Prefer one `-s --recurse-submodules` pass (git ≥2.38). Older git rejects that
844
+ * combo ("unsupported mode"), so fall back to `-c --recurse-submodules` for
845
+ * expanded submodule files plus a plain `-s` pass for gitlink detection (#1031).
530
846
  */
847
+ function listTrackedGitFiles(gitOpts, prefix, files) {
848
+ const gitlinkRels = [];
849
+ const ingestStaged = (tracked) => {
850
+ for (const entry of tracked.split('\0')) {
851
+ if (!entry)
852
+ continue;
853
+ const tab = entry.indexOf('\t');
854
+ if (tab === -1)
855
+ continue; // --stage always emits "<mode> <object> <stage>\t<path>"
856
+ const rel = entry.slice(tab + 1);
857
+ if (entry.slice(0, 6) === '160000') {
858
+ gitlinkRels.push(rel); // an unexpanded gitlink — recursed into below, not a source file itself
859
+ continue;
860
+ }
861
+ files.add((0, utils_1.normalizePath)(prefix + rel));
862
+ }
863
+ };
864
+ try {
865
+ ingestStaged((0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], gitOpts));
866
+ return gitlinkRels;
867
+ }
868
+ catch {
869
+ // Older git: submodule expansion and gitlink mode detection need two passes.
870
+ const cached = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-c', '--recurse-submodules'], gitOpts);
871
+ for (const rel of cached.split('\0')) {
872
+ if (rel)
873
+ files.add((0, utils_1.normalizePath)(prefix + rel));
874
+ }
875
+ ingestStaged((0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s'], gitOpts));
876
+ return gitlinkRels;
877
+ }
878
+ }
879
+ /** Unexpanded gitlink paths (mode 160000) from `git ls-files --stage`. */
880
+ function listGitlinkRels(gitOpts) {
881
+ const rels = [];
882
+ const ingest = (staged) => {
883
+ for (const entry of staged.split('\0')) {
884
+ if (!entry || entry.slice(0, 6) !== '160000')
885
+ continue;
886
+ const tab = entry.indexOf('\t');
887
+ if (tab === -1)
888
+ continue;
889
+ rels.push(entry.slice(tab + 1));
890
+ }
891
+ };
892
+ try {
893
+ ingest((0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], gitOpts));
894
+ }
895
+ catch {
896
+ ingest((0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s'], gitOpts));
897
+ }
898
+ return rels;
899
+ }
531
900
  function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored = null) {
532
901
  const gitOpts = { cwd: repoDir, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
533
902
  // Tracked files. --recurse-submodules pulls in files from active submodules,
@@ -535,15 +904,24 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
535
904
  // Without this, monorepos using submodules index 0 files. (See issue #147.)
536
905
  // Note: --recurse-submodules only supports -c/--cached and --stage modes — it
537
906
  // can't be combined with -o, so untracked files are gathered separately below.
907
+ //
908
+ // We use --stage (-s) rather than -c so each entry carries its file mode. That
909
+ // lets us spot gitlink entries (mode 160000) that --recurse-submodules did NOT
910
+ // expand: a nested repo `git add`ed without a `.gitmodules` entry, or a
911
+ // submodule that isn't active/initialized in this checkout. Such a gitlink
912
+ // falls through every pass — it's tracked, so the untracked `-o` listing below
913
+ // never reports it, and --recurse-submodules only expands ACTIVE submodules —
914
+ // so its source would be silently skipped, leaving only the super-repo's own
915
+ // files indexed. We collect those gitlinks here and recurse into them below.
916
+ // (An active submodule is expanded inline by --recurse-submodules and so never
917
+ // surfaces as a 160000 entry — only the unhandled gitlinks do.) (#1031, #1033)
918
+ //
538
919
  // -z gives NUL-separated, unquoted output so non-ASCII (e.g. CJK) paths
539
920
  // survive verbatim. Without it git octal-escapes and double-quotes such paths
540
921
  // (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
- }
922
+ // on disk → those files are silently dropped from the index. (#541) With -s the
923
+ // path follows a TAB after the `<mode> <object> <stage>` prefix.
924
+ const gitlinkRels = listTrackedGitFiles(gitOpts, prefix, files);
547
925
  // Untracked files (submodules manage their own untracked state). Embedded git
548
926
  // repos surface here as a single "subdir/" entry that git refuses to descend
549
927
  // into — recurse into those as their own repos so their source gets indexed.
@@ -568,6 +946,32 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
568
946
  }
569
947
  files.add((0, utils_1.normalizePath)(prefix + rel));
570
948
  }
949
+ // Gitlink entries (mode 160000) that --recurse-submodules left unexpanded —
950
+ // an embedded repo `git add`ed without `.gitmodules`, or a submodule not
951
+ // active/initialized in this checkout. When such a gitlink has a real working
952
+ // tree on disk it is distinct first-party code we must index as its own
953
+ // embedded repo: the tracked pass skipped its contents and the untracked pass
954
+ // never sees it (it's tracked, not "other"). A gitlink with no checkout on disk
955
+ // (an uninitialized submodule — empty dir, no `.git`) has nothing to index and
956
+ // is left alone, as is a submodule worktree (a duplicate view, #945). (#1031, #1033)
957
+ if (gitlinkRels.length > 0) {
958
+ const defaults = defaultsOnlyIgnore();
959
+ const repoIgnore = buildDefaultIgnore(repoDir);
960
+ for (const rel of gitlinkRels) {
961
+ const relDir = rel.endsWith('/') ? rel : rel + '/';
962
+ // A gitlink under a gitignored path is respected (not indexed) unless the
963
+ // project opted it in via `includeIgnored` — keep tracked gitlinks under
964
+ // the same scope rule as the untracked-ignored kind below (#1065).
965
+ if (gitlinkEmbeddedRepoSkipped(relDir, prefix, defaults, repoIgnore, includeIgnored))
966
+ continue;
967
+ const childDir = path.join(repoDir, rel);
968
+ // 'embedded' = a real .git checkout on disk; 'worktree' and 'none' are skipped.
969
+ if (classifyGitDir(childDir) !== 'embedded')
970
+ continue;
971
+ embeddedRoots?.add((0, utils_1.normalizePath)(prefix + relDir));
972
+ collectGitFiles(childDir, prefix + relDir, files, embeddedRoots, includeIgnored);
973
+ }
974
+ }
571
975
  // Embedded repos hidden by THIS repo's ignore rules (`/packages/` in a
572
976
  // super-repo .gitignore) never appear in any listing above. By default they
573
977
  // stay hidden — `.gitignore` is respected (#970, #976). They are recursed into
@@ -611,7 +1015,11 @@ function getGitVisibleFiles(rootDir) {
611
1015
  // not the parent's: the parent's .gitignore hides the child repo from git,
612
1016
  // not from the index. (#514)
613
1017
  const ig = buildScopeIgnore(rootDir, embeddedRoots);
614
- return new Set([...files].filter((f) => !ig.ignores(f)));
1018
+ const visible = new Set([...files].filter((f) => !ig.ignores(f)));
1019
+ // Force-include first-party source whitelisted in `homegraph.json` `include`.
1020
+ for (const f of collectIncludedFilesForRoot(rootDir))
1021
+ visible.add(f);
1022
+ return visible;
615
1023
  }
616
1024
  catch {
617
1025
  return null;
@@ -637,14 +1045,14 @@ function getGitChangedFiles(rootDir) {
637
1045
  // Custom extension → language overrides from the project's homegraph.json,
638
1046
  // so change detection sees the same custom-extension files the full index does.
639
1047
  const overrides = (0, project_config_1.loadExtensionOverrides)(rootDir);
640
- collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir));
1048
+ collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir));
641
1049
  return changes;
642
1050
  }
643
1051
  catch {
644
1052
  return null;
645
1053
  }
646
1054
  }
647
- function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null) {
1055
+ function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null, exclude = null) {
648
1056
  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
1057
  // This repo's own ignore rules — built-in defaults (#407) plus its .gitignore.
650
1058
  // Change detection must exclude the SAME files the full index does, but git
@@ -683,6 +1091,12 @@ function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null
683
1091
  // index — match against the repo-relative path, same as the full scan. (#766)
684
1092
  if (ig.ignores(rel))
685
1093
  continue;
1094
+ // User `codegraph.json` `exclude` (#999) is project-root-relative, so it's
1095
+ // matched against the full path — sync must not re-add a tracked file the
1096
+ // full index now keeps out. Deletions above stay unfiltered so a file that
1097
+ // WAS indexed before an exclude was added still cleans itself out.
1098
+ if (exclude && exclude.ignores(filePath))
1099
+ continue;
686
1100
  if (statusCode === '??') {
687
1101
  out.added.push(filePath);
688
1102
  }
@@ -697,11 +1111,11 @@ function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null
697
1111
  // and they are left alone (#970, #976), mirroring the full-index scan.
698
1112
  for (const rel of untrackedDirs) {
699
1113
  for (const repoRel of findNestedGitRepos(path.join(repoDir, rel), rel)) {
700
- collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, overrides, includeIgnored);
1114
+ collectGitStatus(path.join(repoDir, repoRel), prefix + repoRel, out, overrides, includeIgnored, exclude);
701
1115
  }
702
1116
  }
703
1117
  for (const rel of findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix)) {
704
- collectGitStatus(path.join(repoDir, rel), prefix + rel, out, overrides, includeIgnored);
1118
+ collectGitStatus(path.join(repoDir, rel), prefix + rel, out, overrides, includeIgnored, exclude);
705
1119
  }
706
1120
  }
707
1121
  /**
@@ -860,9 +1274,60 @@ function scanDirectoryWalk(rootDir, onProgress) {
860
1274
  }
861
1275
  // Seed a base matcher with the built-in default ignores (merged with the root
862
1276
  // .gitignore so a negation can override). Nested .gitignores still layer per-dir.
863
- walk(rootDir, [{ dir: rootDir, ig: buildDefaultIgnore(rootDir) }]);
1277
+ const baseMatchers = [{ dir: rootDir, ig: buildDefaultIgnore(rootDir) }];
1278
+ // Project `codegraph.json` `exclude` patterns (#999), rooted at the project so
1279
+ // `isIgnored` matches them against root-relative paths — same coverage the
1280
+ // git path gets via ScopeIgnore, for non-git projects.
1281
+ const exclude = loadExcludeMatcher(rootDir);
1282
+ if (exclude)
1283
+ baseMatchers.push({ dir: rootDir, ig: exclude });
1284
+ walk(rootDir, baseMatchers);
1285
+ // Force-include first-party source whitelisted in `homegraph.json` `include`.
1286
+ const included = collectIncludedFilesForRoot(rootDir);
1287
+ if (included.size > 0) {
1288
+ const seen = new Set(files);
1289
+ for (const f of included) {
1290
+ if (!seen.has(f)) {
1291
+ files.push(f);
1292
+ seen.add(f);
1293
+ }
1294
+ }
1295
+ }
864
1296
  return files;
865
1297
  }
1298
+ /**
1299
+ * Extraction orchestrator
1300
+ */
1301
+ /**
1302
+ * Resurrect a resolution edge that is about to be dropped (its target symbol
1303
+ * was removed, renamed, or its whole file deleted) as the ORIGINAL unresolved
1304
+ * reference that created it, read from the refName/refKind stamp
1305
+ * `createEdges` writes into edge metadata. Inserted as status='pending', the
1306
+ * ref is consumed by the same sync's resolution sweep: it rebinds to an
1307
+ * alternative definition if one exists, or parks as status='failed' where the
1308
+ * #1240 retry finds it if the symbol later reappears.
1309
+ *
1310
+ * Returns null — drop silently, the pre-#1240 behavior — for edges without a
1311
+ * refName stamp (created before the stamp existed, or synthesized): rebuilding
1312
+ * a ref from the target's plain node name would strip the receiver/qualifier
1313
+ * context the original text carried (`h.greet` → `greet`) and could rebind
1314
+ * somewhere a full re-index never would. Silent beats wrong.
1315
+ */
1316
+ function resurrectRefFromDroppedEdge(e) {
1317
+ const refName = e.metadata?.refName;
1318
+ if (typeof refName !== 'string' || refName.length === 0)
1319
+ return null;
1320
+ const refKind = typeof e.metadata?.refKind === 'string' ? e.metadata.refKind : e.kind;
1321
+ return {
1322
+ fromNodeId: e.source,
1323
+ referenceName: refName,
1324
+ referenceKind: refKind,
1325
+ line: e.line ?? 0,
1326
+ column: e.column ?? 0,
1327
+ filePath: e.sourceFilePath,
1328
+ language: e.sourceLanguage,
1329
+ };
1330
+ }
866
1331
  /**
867
1332
  * Extraction orchestrator
868
1333
  */
@@ -992,268 +1457,101 @@ class ExtractionOrchestrator {
992
1457
  // between runs is picked up without restarting the process.
993
1458
  this.detectedFrameworkNames = null;
994
1459
  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 });
1460
+ (0, arkts_1.resetArkTSBatch)();
1461
+ (0, context_1.setArkTSBatchProgressCallback)((batchProgress) => {
1462
+ onProgress?.({
1463
+ phase: 'arkts-batch',
1464
+ current: batchProgress.current,
1465
+ total: batchProgress.total,
1466
+ currentFile: batchProgress.currentFile,
1467
+ subphase: batchProgress.subphase,
1152
1468
  });
1153
- }
1154
- for (let i = 0; i < files.length; i += FILE_IO_BATCH_SIZE) {
1469
+ });
1470
+ try {
1155
1471
  if (signal?.aborted) {
1156
- if (parseWorker)
1157
- parseWorker.terminate().catch(() => { });
1158
1472
  return {
1159
1473
  success: false,
1160
- filesIndexed,
1161
- filesSkipped,
1162
- filesErrored,
1163
- nodesCreated: totalNodes,
1164
- edgesCreated: totalEdges,
1165
- errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
1474
+ filesIndexed: 0,
1475
+ filesSkipped: 0,
1476
+ filesErrored: 0,
1477
+ nodesCreated: 0,
1478
+ edgesCreated: 0,
1479
+ errors: [{ message: 'Aborted', severity: 'error' }],
1166
1480
  durationMs: Date.now() - startTime,
1167
1481
  };
1168
1482
  }
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,
1483
+ // Phase 2: Parse files in a worker thread (keeps main thread unblocked for UI)
1484
+ const total = files.length;
1485
+ let processed = 0;
1486
+ // Emit parsing phase immediately so the progress bar appears during worker setup.
1487
+ // The yield lets the shimmer worker flush the phase transition to stdout before
1488
+ // the main thread starts synchronous grammar detection work.
1489
+ onProgress?.({
1490
+ phase: 'parsing',
1491
+ current: 0,
1492
+ total,
1493
+ });
1494
+ await new Promise(resolve => setImmediate(resolve));
1495
+ // Detect needed languages and load grammars in the parse worker
1496
+ const neededLanguages = [...new Set(files.map((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides)))];
1497
+ // .h files default to 'c' but may be C++ — ensure cpp grammar is loaded when c is needed
1498
+ if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
1499
+ neededLanguages.push('cpp');
1500
+ }
1501
+ // Parse files on a pool of worker threads (keeps the main thread free for UI
1502
+ // and uses every core). Falls back to in-process parsing when the compiled
1503
+ // worker is unavailable (e.g. running from source in tests).
1504
+ const parseWorkerPath = path.join(__dirname, 'parse-worker.js');
1505
+ const useWorker = fs.existsSync(parseWorkerPath);
1506
+ let pool = null;
1507
+ if (useWorker) {
1508
+ // CODEGRAPH_PARSE_WORKERS: explicit worker count; 1 = the old single-worker
1509
+ // behaviour (the conservative rollback). Unset → clamp(cores-1, 1, 8).
1510
+ const poolSize = (0, parse_pool_1.resolveParsePoolSize)(process.env.CODEGRAPH_PARSE_WORKERS, os.cpus().length);
1511
+ pool = new parse_pool_1.ParseWorkerPool({
1512
+ languages: neededLanguages,
1513
+ size: poolSize,
1514
+ workerScriptPath: parseWorkerPath,
1515
+ recycleInterval: WORKER_RECYCLE_INTERVAL,
1516
+ parseTimeoutMs: PARSE_TIMEOUT_MS,
1517
+ log,
1211
1518
  });
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;
1519
+ log(`Parse worker pool: ${poolSize} worker(s)`);
1520
+ }
1521
+ else {
1522
+ // In-process fallback: load grammars locally and parse on the main thread.
1523
+ await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
1524
+ }
1525
+ /**
1526
+ * Parse one file: on the pool when available (the promise REJECTS on a worker
1527
+ * crash/timeout — the caller records it and the retry pass re-attempts), or
1528
+ * in-process synchronously as the no-worker fallback. The language is resolved
1529
+ * here on the main thread, where the codegraph.json overrides are loaded.
1530
+ */
1531
+ const parseFile = (filePath, content) => {
1532
+ const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1533
+ if (!pool || (0, grammars_1.requiresInProcessExtraction)(language)) {
1534
+ return Promise.resolve((0, tree_sitter_1.extractFromSource)(filePath, content, language, frameworkNames));
1256
1535
  }
1536
+ return pool.requestParse({ filePath, content, language, frameworkNames });
1537
+ };
1538
+ // --- Bounded rolling-window dispatch, ordered commit ---
1539
+ // Reads stay batched/parallel; parses run concurrently across the pool; the
1540
+ // SQLite store stays on the main thread (it isn't thread-safe). Crucially we
1541
+ // COMMIT results in original file order, not parse-completion order: the
1542
+ // resolution phase (run after indexing) resolves an ambiguous reference to one
1543
+ // of several same-named candidates by the nodes' DB insertion order, so a
1544
+ // stable commit order keeps the resulting graph deterministic — byte-identical
1545
+ // to the single-worker path — instead of drifting with parse timing. The
1546
+ // `completed` buffer holds at most ~windowSize out-of-order results, so memory
1547
+ // stays bounded.
1548
+ const windowSize = pool ? Math.max(4, pool.size * 2) : 1;
1549
+ const inFlight = new Set();
1550
+ const completed = new Map();
1551
+ let nextSeq = 0; // file-order sequence assigned at dispatch
1552
+ let nextToStore = 0; // cursor: next sequence to commit
1553
+ let aborted = false;
1554
+ const storeResult = (filePath, content, stats, result) => {
1257
1555
  processed++;
1258
1556
  // Store in database on main thread (SQLite is not thread-safe)
1259
1557
  if (result.nodes.length > 0 || result.errors.length === 0) {
@@ -1277,8 +1575,8 @@ class ExtractionOrchestrator {
1277
1575
  }
1278
1576
  else {
1279
1577
  // 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".
1578
+ // tracked at the file level — count them as indexed so the CLI doesn't
1579
+ // misleadingly report "No files found to index".
1282
1580
  const lang = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1283
1581
  if ((0, grammars_1.isFileLevelOnlyLanguage)(lang)) {
1284
1582
  filesIndexed++;
@@ -1287,102 +1585,230 @@ class ExtractionOrchestrator {
1287
1585
  filesSkipped++;
1288
1586
  }
1289
1587
  }
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;
1588
+ onProgress?.({ phase: 'parsing', current: processed, total, currentFile: filePath });
1589
+ };
1590
+ const recordParseFailure = (filePath, err) => {
1591
+ processed++;
1592
+ filesErrored++;
1593
+ errors.push({
1594
+ message: err instanceof Error ? err.message : String(err),
1595
+ filePath,
1596
+ severity: 'error',
1597
+ code: 'parse_error',
1598
+ });
1599
+ onProgress?.({ phase: 'parsing', current: processed, total });
1600
+ };
1601
+ // Commit buffered parses to the DB in file order, advancing the cursor over
1602
+ // contiguous completed results. Runs after each parse settles (and once more
1603
+ // after the drain). storeResult / recordParseFailure run here single-threaded,
1604
+ // so shared counters and SQLite writes never race despite parallel parsing.
1605
+ const flushOrdered = () => {
1606
+ if (aborted)
1607
+ return;
1608
+ while (completed.has(nextToStore)) {
1609
+ const item = completed.get(nextToStore);
1610
+ completed.delete(nextToStore);
1611
+ nextToStore++;
1612
+ if (item.ok)
1613
+ storeResult(item.filePath, item.content, item.stats, item.result);
1614
+ else
1615
+ recordParseFailure(item.filePath, item.err);
1325
1616
  }
1326
- let result;
1327
- try {
1328
- result = await requestParse(filePath, content);
1617
+ };
1618
+ // Dispatch one file's parse (parses run concurrently across the pool), tagged
1619
+ // with its file-order sequence so flushOrdered commits results in order. The
1620
+ // backpressure below bounds how far parsing runs ahead of the in-order commit.
1621
+ const feed = async (filePath, content, stats) => {
1622
+ const seq = nextSeq++;
1623
+ const p = (async () => {
1624
+ try {
1625
+ const result = await parseFile(filePath, content);
1626
+ completed.set(seq, { ok: true, filePath, content, stats, result });
1627
+ }
1628
+ catch (parseErr) {
1629
+ completed.set(seq, { ok: false, filePath, err: parseErr });
1630
+ }
1631
+ flushOrdered();
1632
+ })();
1633
+ const tracked = p.finally(() => { inFlight.delete(tracked); });
1634
+ inFlight.add(tracked);
1635
+ // Backpressure on the dispatched-but-not-yet-committed count (in-flight +
1636
+ // buffered), not just in-flight: a slow file sitting at the commit cursor
1637
+ // lets later parses finish and buffer, which would otherwise grow without
1638
+ // bound. Wait for parses to settle (each may advance the cursor) until the
1639
+ // window has room. `inFlight.size > 0` guards against an empty race — the
1640
+ // cursor file is always still in flight when the window is full.
1641
+ while (nextSeq - nextToStore >= windowSize && inFlight.size > 0) {
1642
+ await Promise.race(inFlight);
1329
1643
  }
1330
- catch {
1331
- stillFailing.push(errEntry);
1332
- continue;
1644
+ };
1645
+ const firstEtsFile = files.find((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts');
1646
+ if (firstEtsFile) {
1647
+ await (0, arkts_1.primeArkTSBatch)(this.rootDir, this.queries, firstEtsFile);
1648
+ errors.push(...(0, arkts_1.drainArkTSIndexNotices)());
1649
+ }
1650
+ for (let i = 0; i < files.length; i += FILE_IO_BATCH_SIZE) {
1651
+ if (signal?.aborted) {
1652
+ aborted = true;
1653
+ break;
1333
1654
  }
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)`);
1655
+ const batch = files.slice(i, i + FILE_IO_BATCH_SIZE);
1656
+ // Read files in parallel (with path validation before any I/O)
1657
+ const fileContents = await Promise.all(batch.map(async (fp) => {
1658
+ if ((0, arkts_1.isArkTSBatchPersisted)(fp)) {
1659
+ return {
1660
+ filePath: fp,
1661
+ content: null,
1662
+ stats: null,
1663
+ error: null,
1664
+ arktsBatchSkipped: true,
1665
+ };
1666
+ }
1667
+ try {
1668
+ // Indexing read: follow in-root symlinks the directory walk already
1669
+ // descended into (the `../` guard still applies) so files reached
1670
+ // via an in-root symlink-to-outside still index (#935).
1671
+ const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, fp, { allowSymlinkEscape: true });
1672
+ if (!fullPath) {
1673
+ (0, errors_1.logWarn)('Path traversal blocked in batch reader', { filePath: fp });
1674
+ return { filePath: fp, content: null, stats: null, error: new Error('Path traversal blocked') };
1675
+ }
1676
+ const content = await fsp.readFile(fullPath, 'utf-8');
1677
+ const stats = await fsp.stat(fullPath);
1678
+ return { filePath: fp, content, stats, error: null };
1679
+ }
1680
+ catch (err) {
1681
+ return { filePath: fp, content: null, stats: null, error: err };
1682
+ }
1683
+ }));
1684
+ // Dispatch each readable file into the bounded parse window; the window
1685
+ // stores results on the main thread as they arrive.
1686
+ for (const fileEntry of fileContents) {
1687
+ const { filePath, content, stats, error } = fileEntry;
1688
+ const arktsBatchSkipped = 'arktsBatchSkipped' in fileEntry && fileEntry.arktsBatchSkipped;
1689
+ if (signal?.aborted) {
1690
+ aborted = true;
1691
+ break;
1692
+ }
1693
+ if ((0, arkts_1.isArkTSBatchPersisted)(filePath) || arktsBatchSkipped) {
1694
+ processed++;
1695
+ filesIndexed++;
1696
+ onProgress?.({ phase: 'parsing', current: processed, total, currentFile: filePath });
1697
+ continue;
1698
+ }
1699
+ if (!(0, context_1.isArktsBatchRunning)()) {
1700
+ onProgress?.({
1701
+ phase: 'parsing',
1702
+ current: processed,
1703
+ total,
1704
+ currentFile: filePath,
1705
+ });
1706
+ }
1707
+ if (error || content === null || stats === null) {
1708
+ processed++;
1709
+ filesErrored++;
1710
+ errors.push({
1711
+ message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
1712
+ filePath,
1713
+ severity: 'error',
1714
+ code: 'read_error',
1715
+ });
1716
+ onProgress?.({ phase: 'parsing', current: processed, total });
1717
+ continue;
1718
+ }
1719
+ // Honour MAX_FILE_SIZE. Without this check, vendored generated
1720
+ // headers, minified bundles, and other multi-MB files get indexed,
1721
+ // wasting WASM heap and the worker recycle budget on inputs with no
1722
+ // useful symbols. The single-file extractFile path already enforces
1723
+ // this; the bulk path used to silently skip the check.
1724
+ if (stats.size > MAX_FILE_SIZE) {
1725
+ processed++;
1726
+ filesSkipped++;
1727
+ errors.push({
1728
+ message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
1729
+ filePath,
1730
+ severity: 'warning',
1731
+ code: 'size_exceeded',
1732
+ });
1733
+ onProgress?.({ phase: 'parsing', current: processed, total });
1734
+ continue;
1735
+ }
1736
+ // Parse on the pool (main thread stays unblocked). Errors/timeouts are
1737
+ // handled inside feed() → recordParseFailure, feeding the retry pass.
1738
+ await feed(filePath, content, stats);
1346
1739
  }
1740
+ if (aborted)
1741
+ break;
1742
+ }
1743
+ // Drain parses still in flight (skip on abort — we tear down below instead),
1744
+ // then commit any results the cursor hasn't reached yet.
1745
+ if (!aborted) {
1746
+ await Promise.all(inFlight);
1747
+ flushOrdered();
1748
+ }
1749
+ if (signal?.aborted || aborted) {
1750
+ if (pool)
1751
+ await pool.destroy();
1752
+ return {
1753
+ success: false,
1754
+ filesIndexed,
1755
+ filesSkipped,
1756
+ filesErrored,
1757
+ filesDiscovered: total,
1758
+ nodesCreated: totalNodes,
1759
+ edgesCreated: totalEdges,
1760
+ errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
1761
+ durationMs: Date.now() - startTime,
1762
+ };
1347
1763
  }
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) {
1764
+ // Report 100% so the progress bar doesn't hang at 99%
1765
+ onProgress?.({
1766
+ phase: 'parsing',
1767
+ current: total,
1768
+ total,
1769
+ });
1770
+ // Yield so the shimmer worker's buffered stdout writes can flush.
1771
+ // Worker thread stdout is proxied through the main thread's event loop,
1772
+ // so synchronous work here blocks the animation from rendering.
1773
+ await new Promise(resolve => setImmediate(resolve));
1774
+ // Retry pass: files that failed due to WASM memory corruption may succeed
1775
+ // on a fresh worker with a clean heap. Recycle before each attempt so
1776
+ // every file gets the absolute cleanest WASM state possible.
1777
+ const retryableErrors = errors.filter((e) => e.code === 'parse_error' && e.filePath &&
1778
+ (e.message.includes('Worker exited') || e.message.includes('memory access out of bounds')));
1779
+ if (retryableErrors.length > 0 && pool) {
1780
+ log(`Retrying ${retryableErrors.length} files that failed due to WASM memory errors...`);
1781
+ // Fresh WASM heaps for the retry phase. A retry that still crashes its
1782
+ // worker makes the pool respawn it, so later retries keep landing on clean
1783
+ // workers too.
1784
+ pool.recycleAll();
1785
+ const stillFailing = [];
1786
+ for (const errEntry of retryableErrors) {
1355
1787
  const filePath = errEntry.filePath;
1356
1788
  if (signal?.aborted)
1357
1789
  break;
1358
- recycleWorker();
1359
- let fullContent;
1790
+ let content;
1360
1791
  try {
1361
1792
  const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
1362
1793
  if (!fullPath)
1363
1794
  continue;
1364
- fullContent = await fsp.readFile(fullPath, 'utf-8');
1795
+ content = await fsp.readFile(fullPath, 'utf-8');
1365
1796
  }
1366
1797
  catch {
1367
1798
  continue;
1368
1799
  }
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
1800
  let result;
1376
1801
  try {
1377
- result = await requestParse(filePath, stripped);
1802
+ result = await parseFile(filePath, content);
1378
1803
  }
1379
1804
  catch {
1805
+ stillFailing.push(errEntry);
1380
1806
  continue;
1381
1807
  }
1382
1808
  if (result.nodes.length > 0 || result.errors.length === 0) {
1383
- const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
1809
+ const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1384
1810
  const stats = await fsp.stat(path.join(this.rootDir, filePath));
1385
- this.storeExtractionResult(filePath, fullContent, language, stats, result);
1811
+ this.storeExtractionResult(filePath, content, language, stats, result);
1386
1812
  const idx = errors.indexOf(errEntry);
1387
1813
  if (idx >= 0)
1388
1814
  errors.splice(idx, 1);
@@ -1390,26 +1816,77 @@ class ExtractionOrchestrator {
1390
1816
  filesIndexed++;
1391
1817
  totalNodes += result.nodes.length;
1392
1818
  totalEdges += result.edges.length;
1393
- log(`Retry (stripped) OK: ${filePath} (${result.nodes.length} nodes)`);
1819
+ log(`Retry OK: ${filePath} (${result.nodes.length} nodes)`);
1820
+ }
1821
+ }
1822
+ // Last resort: for files that still crash on a clean worker, strip
1823
+ // comment-only lines to reduce WASM memory pressure. Many compiler
1824
+ // test files are 90%+ comments (CHECK directives) that don't contribute
1825
+ // code nodes but consume parser memory.
1826
+ if (stillFailing.length > 0) {
1827
+ log(`${stillFailing.length} files still failing — retrying with comments stripped...`);
1828
+ pool.recycleAll();
1829
+ for (const errEntry of stillFailing) {
1830
+ const filePath = errEntry.filePath;
1831
+ if (signal?.aborted)
1832
+ break;
1833
+ let fullContent;
1834
+ try {
1835
+ const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
1836
+ if (!fullPath)
1837
+ continue;
1838
+ fullContent = await fsp.readFile(fullPath, 'utf-8');
1839
+ }
1840
+ catch {
1841
+ continue;
1842
+ }
1843
+ // Strip lines that are entirely comments (preserving line numbers
1844
+ // by replacing with empty lines so node positions stay correct)
1845
+ const stripped = fullContent
1846
+ .split('\n')
1847
+ .map(line => /^\s*\/\//.test(line) ? '' : line)
1848
+ .join('\n');
1849
+ let result;
1850
+ try {
1851
+ result = await parseFile(filePath, stripped);
1852
+ }
1853
+ catch {
1854
+ continue;
1855
+ }
1856
+ if (result.nodes.length > 0 || result.errors.length === 0) {
1857
+ const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
1858
+ const stats = await fsp.stat(path.join(this.rootDir, filePath));
1859
+ this.storeExtractionResult(filePath, fullContent, language, stats, result);
1860
+ const idx = errors.indexOf(errEntry);
1861
+ if (idx >= 0)
1862
+ errors.splice(idx, 1);
1863
+ filesErrored--;
1864
+ filesIndexed++;
1865
+ totalNodes += result.nodes.length;
1866
+ totalEdges += result.edges.length;
1867
+ log(`Retry (stripped) OK: ${filePath} (${result.nodes.length} nodes)`);
1868
+ }
1394
1869
  }
1395
1870
  }
1396
1871
  }
1872
+ // Shut down the parse worker pool.
1873
+ if (pool)
1874
+ await pool.destroy();
1875
+ return {
1876
+ success: filesIndexed > 0 || errors.filter((e) => e.severity === 'error').length === 0,
1877
+ filesIndexed,
1878
+ filesSkipped,
1879
+ filesErrored,
1880
+ filesDiscovered: total,
1881
+ nodesCreated: totalNodes,
1882
+ edgesCreated: totalEdges,
1883
+ errors,
1884
+ durationMs: Date.now() - startTime,
1885
+ };
1397
1886
  }
1398
- // Shut down parse worker and clear any pending timers
1399
- rejectAllPending('Indexing complete');
1400
- if (parseWorker) {
1401
- parseWorker.terminate().catch(() => { });
1887
+ finally {
1888
+ (0, context_1.setArkTSBatchProgressCallback)(null);
1402
1889
  }
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
1890
  }
1414
1891
  /**
1415
1892
  * Index specific files
@@ -1532,6 +2009,15 @@ class ExtractionOrchestrator {
1532
2009
  }
1533
2010
  // Detect language (honoring the project's homegraph.json extension overrides)
1534
2011
  const language = (0, grammars_1.detectLanguage)(relativePath, content, (0, project_config_1.loadExtensionOverrides)(this.rootDir));
2012
+ if ((0, arkts_1.isArkTSBatchPersisted)(relativePath)) {
2013
+ return {
2014
+ nodes: [],
2015
+ edges: [],
2016
+ unresolvedReferences: [],
2017
+ errors: [],
2018
+ durationMs: 0,
2019
+ };
2020
+ }
1535
2021
  if (!(0, grammars_1.isLanguageSupported)(language)) {
1536
2022
  return {
1537
2023
  nodes: [],
@@ -1616,15 +2102,24 @@ class ExtractionOrchestrator {
1616
2102
  newNodesByKindName.set(`${n.kind}\0${n.name}`, n.id);
1617
2103
  }
1618
2104
  const reinserted = [];
2105
+ const resurrected = [];
1619
2106
  for (const e of crossFileIncomingEdges) {
1620
2107
  const newTargetId = newNodesByKindName.get(`${e.targetKind}\0${e.targetName}`);
1621
2108
  if (newTargetId) {
1622
2109
  reinserted.push({ source: e.source, target: newTargetId, kind: e.kind, metadata: e.metadata, line: e.line, column: e.column, provenance: e.provenance });
1623
2110
  }
2111
+ else {
2112
+ const ref = resurrectRefFromDroppedEdge(e);
2113
+ if (ref)
2114
+ resurrected.push(ref);
2115
+ }
1624
2116
  }
1625
2117
  if (reinserted.length > 0) {
1626
2118
  this.queries.insertEdges(reinserted);
1627
2119
  }
2120
+ if (resurrected.length > 0) {
2121
+ this.queries.insertUnresolvedRefsBatch(resurrected);
2122
+ }
1628
2123
  }
1629
2124
  // Insert unresolved references in batch with denormalized filePath/language
1630
2125
  if (result.unresolvedReferences.length > 0) {
@@ -1701,6 +2196,17 @@ class ExtractionOrchestrator {
1701
2196
  let reconcileChecks = 0;
1702
2197
  for (const tracked of trackedFiles) {
1703
2198
  if (!currentSet.has(tracked.path) || !fs.existsSync(path.join(this.rootDir, tracked.path))) {
2199
+ // Before the cascade deletes them, resurrect incoming cross-file
2200
+ // resolution edges as their original refs (#1240 removal case).
2201
+ const incoming = this.queries.getCrossFileIncomingEdgesWithTarget(tracked.path);
2202
+ if (incoming.length > 0) {
2203
+ const resurrected = incoming
2204
+ .map((e) => resurrectRefFromDroppedEdge(e))
2205
+ .filter((r) => r !== null);
2206
+ if (resurrected.length > 0) {
2207
+ this.queries.insertUnresolvedRefsBatch(resurrected);
2208
+ }
2209
+ }
1704
2210
  this.queries.deleteFile(tracked.path);
1705
2211
  filesRemoved++;
1706
2212
  }
@@ -1714,6 +2220,11 @@ class ExtractionOrchestrator {
1714
2220
  // synchronous-stat loop that wedges the main thread on a large repo (#905).
1715
2221
  // Yield at the top of the body so the `continue` fast-paths below still hit it.
1716
2222
  if (++reconcileChecks % SYNC_RECONCILE_YIELD_INTERVAL === 0) {
2223
+ onProgress?.({
2224
+ phase: 'scanning',
2225
+ current: reconcileChecks,
2226
+ total: currentFiles.length,
2227
+ });
1717
2228
  await new Promise((resolve) => setImmediate(resolve));
1718
2229
  }
1719
2230
  const fullPath = path.join(this.rootDir, filePath);
@@ -1768,16 +2279,47 @@ class ExtractionOrchestrator {
1768
2279
  }
1769
2280
  // Index changed files
1770
2281
  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,
2282
+ const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
2283
+ const needsArkTS = filesToIndex.some((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts');
2284
+ if (needsArkTS) {
2285
+ (0, context_1.setArkTSBatchProgressCallback)((batchProgress) => {
2286
+ onProgress?.({
2287
+ phase: 'arkts-batch',
2288
+ current: batchProgress.current,
2289
+ total: batchProgress.total,
2290
+ currentFile: batchProgress.currentFile,
2291
+ subphase: batchProgress.subphase,
2292
+ });
1778
2293
  });
1779
- const result = await this.indexFile(filePath);
1780
- nodesUpdated += result.nodes.length;
2294
+ const firstEts = filesToIndex.find((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts');
2295
+ await (0, arkts_1.primeArkTSBatch)(this.rootDir, this.queries, firstEts);
2296
+ }
2297
+ try {
2298
+ for (let i = 0; i < filesToIndex.length; i++) {
2299
+ const filePath = filesToIndex[i];
2300
+ if ((0, arkts_1.isArkTSBatchPersisted)(filePath)) {
2301
+ onProgress?.({
2302
+ phase: 'parsing',
2303
+ current: i + 1,
2304
+ total,
2305
+ currentFile: filePath,
2306
+ });
2307
+ continue;
2308
+ }
2309
+ onProgress?.({
2310
+ phase: 'parsing',
2311
+ current: i + 1,
2312
+ total,
2313
+ currentFile: filePath,
2314
+ });
2315
+ const result = await this.indexFile(filePath);
2316
+ nodesUpdated += result.nodes.length;
2317
+ }
2318
+ }
2319
+ finally {
2320
+ if (needsArkTS) {
2321
+ (0, context_1.setArkTSBatchProgressCallback)(null);
2322
+ }
1781
2323
  }
1782
2324
  return {
1783
2325
  filesChecked,