homegraph 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/README.md +48 -11
  2. package/dist/bin/homegraph.js +295 -72
  3. package/dist/bin/homegraph.js.map +1 -1
  4. package/dist/context/index.d.ts.map +1 -1
  5. package/dist/context/index.js +32 -11
  6. package/dist/context/index.js.map +1 -1
  7. package/dist/db/index.d.ts +143 -1
  8. package/dist/db/index.d.ts.map +1 -1
  9. package/dist/db/index.js +293 -16
  10. package/dist/db/index.js.map +1 -1
  11. package/dist/db/queries.d.ts +43 -4
  12. package/dist/db/queries.d.ts.map +1 -1
  13. package/dist/db/queries.js +138 -14
  14. package/dist/db/queries.js.map +1 -1
  15. package/dist/db/sqlite-adapter.d.ts +24 -17
  16. package/dist/db/sqlite-adapter.d.ts.map +1 -1
  17. package/dist/db/sqlite-adapter.js +292 -36
  18. package/dist/db/sqlite-adapter.js.map +1 -1
  19. package/dist/db/wal-valve.d.ts +15 -2
  20. package/dist/db/wal-valve.d.ts.map +1 -1
  21. package/dist/db/wal-valve.js +88 -6
  22. package/dist/db/wal-valve.js.map +1 -1
  23. package/dist/extraction/arkts-batch-worker.js +3 -2
  24. package/dist/extraction/arkts-batch-worker.js.map +1 -1
  25. package/dist/extraction/default-ignore.d.ts +39 -0
  26. package/dist/extraction/default-ignore.d.ts.map +1 -0
  27. package/dist/extraction/default-ignore.js +221 -0
  28. package/dist/extraction/default-ignore.js.map +1 -0
  29. package/dist/extraction/index.d.ts +9 -10
  30. package/dist/extraction/index.d.ts.map +1 -1
  31. package/dist/extraction/index.js +227 -323
  32. package/dist/extraction/index.js.map +1 -1
  33. package/dist/extraction/languages/arkts.d.ts +90 -6
  34. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  35. package/dist/extraction/languages/arkts.js +1579 -128
  36. package/dist/extraction/languages/arkts.js.map +1 -1
  37. package/dist/extraction/store-worker.d.ts +20 -0
  38. package/dist/extraction/store-worker.d.ts.map +1 -0
  39. package/dist/extraction/store-worker.js +102 -0
  40. package/dist/extraction/store-worker.js.map +1 -0
  41. package/dist/extraction/store-writer.d.ts +48 -0
  42. package/dist/extraction/store-writer.d.ts.map +1 -0
  43. package/dist/extraction/store-writer.js +167 -0
  44. package/dist/extraction/store-writer.js.map +1 -0
  45. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  46. package/dist/extraction/tree-sitter.js +47 -0
  47. package/dist/extraction/tree-sitter.js.map +1 -1
  48. package/dist/extraction/wasm-runtime-flags.d.ts +14 -4
  49. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
  50. package/dist/extraction/wasm-runtime-flags.js +55 -7
  51. package/dist/extraction/wasm-runtime-flags.js.map +1 -1
  52. package/dist/index.d.ts +16 -5
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +135 -97
  55. package/dist/index.js.map +1 -1
  56. package/dist/mcp/query-worker.d.ts +2 -2
  57. package/dist/mcp/query-worker.js +2 -2
  58. package/dist/mcp/tools.d.ts.map +1 -1
  59. package/dist/mcp/tools.js +42 -26
  60. package/dist/mcp/tools.js.map +1 -1
  61. package/dist/resolution/callback-synthesizer.d.ts +52 -16
  62. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  63. package/dist/resolution/callback-synthesizer.js +234 -180
  64. package/dist/resolution/callback-synthesizer.js.map +1 -1
  65. package/dist/resolution/cooperative-yield.d.ts +10 -2
  66. package/dist/resolution/cooperative-yield.d.ts.map +1 -1
  67. package/dist/resolution/cooperative-yield.js +6 -4
  68. package/dist/resolution/cooperative-yield.js.map +1 -1
  69. package/dist/resolution/frameworks/go.d.ts.map +1 -1
  70. package/dist/resolution/frameworks/go.js +29 -6
  71. package/dist/resolution/frameworks/go.js.map +1 -1
  72. package/dist/resolution/import-resolver.d.ts +2 -3
  73. package/dist/resolution/import-resolver.d.ts.map +1 -1
  74. package/dist/resolution/import-resolver.js +166 -7
  75. package/dist/resolution/import-resolver.js.map +1 -1
  76. package/dist/resolution/index.d.ts +87 -2
  77. package/dist/resolution/index.d.ts.map +1 -1
  78. package/dist/resolution/index.js +739 -131
  79. package/dist/resolution/index.js.map +1 -1
  80. package/dist/resolution/memory-budget.d.ts +48 -0
  81. package/dist/resolution/memory-budget.d.ts.map +1 -0
  82. package/dist/resolution/memory-budget.js +162 -0
  83. package/dist/resolution/memory-budget.js.map +1 -0
  84. package/dist/resolution/name-matcher.d.ts +12 -1
  85. package/dist/resolution/name-matcher.d.ts.map +1 -1
  86. package/dist/resolution/name-matcher.js +439 -97
  87. package/dist/resolution/name-matcher.js.map +1 -1
  88. package/dist/resolution/resolver-pool.d.ts +106 -0
  89. package/dist/resolution/resolver-pool.d.ts.map +1 -0
  90. package/dist/resolution/resolver-pool.js +344 -0
  91. package/dist/resolution/resolver-pool.js.map +1 -0
  92. package/dist/resolution/resolver-worker.d.ts +17 -0
  93. package/dist/resolution/resolver-worker.d.ts.map +1 -0
  94. package/dist/resolution/resolver-worker.js +141 -0
  95. package/dist/resolution/resolver-worker.js.map +1 -0
  96. package/dist/resolution/types.d.ts +3 -0
  97. package/dist/resolution/types.d.ts.map +1 -1
  98. package/dist/spec/build/diff-parser.d.ts +55 -0
  99. package/dist/spec/build/diff-parser.d.ts.map +1 -0
  100. package/dist/spec/build/diff-parser.js +205 -0
  101. package/dist/spec/build/diff-parser.js.map +1 -0
  102. package/dist/spec/build/git-scanner.d.ts +93 -0
  103. package/dist/spec/build/git-scanner.d.ts.map +1 -0
  104. package/dist/spec/build/git-scanner.js +254 -0
  105. package/dist/spec/build/git-scanner.js.map +1 -0
  106. package/dist/spec/build/pipeline.d.ts +54 -0
  107. package/dist/spec/build/pipeline.d.ts.map +1 -0
  108. package/dist/spec/build/pipeline.js +170 -0
  109. package/dist/spec/build/pipeline.js.map +1 -0
  110. package/dist/spec/build/scan.d.ts +48 -0
  111. package/dist/spec/build/scan.d.ts.map +1 -0
  112. package/dist/spec/build/scan.js +89 -0
  113. package/dist/spec/build/scan.js.map +1 -0
  114. package/dist/spec/build/scope-resolver.d.ts +50 -0
  115. package/dist/spec/build/scope-resolver.d.ts.map +1 -0
  116. package/dist/spec/build/scope-resolver.js +105 -0
  117. package/dist/spec/build/scope-resolver.js.map +1 -0
  118. package/dist/spec/build/spec-extractor.d.ts +74 -0
  119. package/dist/spec/build/spec-extractor.d.ts.map +1 -0
  120. package/dist/spec/build/spec-extractor.js +344 -0
  121. package/dist/spec/build/spec-extractor.js.map +1 -0
  122. package/dist/spec/config.d.ts +35 -1
  123. package/dist/spec/config.d.ts.map +1 -1
  124. package/dist/spec/config.js +30 -5
  125. package/dist/spec/config.js.map +1 -1
  126. package/dist/spec/db/fts.d.ts.map +1 -1
  127. package/dist/spec/db/fts.js +19 -50
  128. package/dist/spec/db/fts.js.map +1 -1
  129. package/dist/spec/db/index.d.ts +5 -3
  130. package/dist/spec/db/index.d.ts.map +1 -1
  131. package/dist/spec/db/index.js +16 -1
  132. package/dist/spec/db/index.js.map +1 -1
  133. package/dist/spec/db/persist.d.ts +45 -0
  134. package/dist/spec/db/persist.d.ts.map +1 -0
  135. package/dist/spec/db/persist.js +66 -0
  136. package/dist/spec/db/persist.js.map +1 -0
  137. package/dist/spec/db/relations.d.ts +80 -1
  138. package/dist/spec/db/relations.d.ts.map +1 -1
  139. package/dist/spec/db/relations.js +175 -4
  140. package/dist/spec/db/relations.js.map +1 -1
  141. package/dist/spec/db/schema.d.ts +4 -0
  142. package/dist/spec/db/schema.d.ts.map +1 -1
  143. package/dist/spec/db/schema.js +4 -0
  144. package/dist/spec/db/schema.js.map +1 -1
  145. package/dist/spec/db/schema.sql +5 -1
  146. package/dist/spec/db/spec-node.d.ts +5 -0
  147. package/dist/spec/db/spec-node.d.ts.map +1 -1
  148. package/dist/spec/db/spec-node.js +11 -5
  149. package/dist/spec/db/spec-node.js.map +1 -1
  150. package/dist/spec/db/sql-utils.d.ts +11 -0
  151. package/dist/spec/db/sql-utils.d.ts.map +1 -0
  152. package/dist/spec/db/sql-utils.js +19 -0
  153. package/dist/spec/db/sql-utils.js.map +1 -0
  154. package/dist/spec/evolve/cluster-context.d.ts +30 -0
  155. package/dist/spec/evolve/cluster-context.d.ts.map +1 -0
  156. package/dist/spec/evolve/cluster-context.js +78 -0
  157. package/dist/spec/evolve/cluster-context.js.map +1 -0
  158. package/dist/spec/evolve/commit-spec-analyzer.d.ts +51 -0
  159. package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +1 -0
  160. package/dist/spec/evolve/commit-spec-analyzer.js +88 -0
  161. package/dist/spec/evolve/commit-spec-analyzer.js.map +1 -0
  162. package/dist/spec/evolve/commit-spec-persister.d.ts +53 -0
  163. package/dist/spec/evolve/commit-spec-persister.d.ts.map +1 -0
  164. package/dist/spec/evolve/commit-spec-persister.js +93 -0
  165. package/dist/spec/evolve/commit-spec-persister.js.map +1 -0
  166. package/dist/spec/evolve/impact-locator.d.ts +23 -7
  167. package/dist/spec/evolve/impact-locator.d.ts.map +1 -1
  168. package/dist/spec/evolve/impact-locator.js +58 -14
  169. package/dist/spec/evolve/impact-locator.js.map +1 -1
  170. package/dist/spec/evolve/pipeline.d.ts +62 -25
  171. package/dist/spec/evolve/pipeline.d.ts.map +1 -1
  172. package/dist/spec/evolve/pipeline.js +436 -457
  173. package/dist/spec/evolve/pipeline.js.map +1 -1
  174. package/dist/spec/evolve/spec-rewriter.d.ts +13 -13
  175. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
  176. package/dist/spec/evolve/spec-rewriter.js +45 -45
  177. package/dist/spec/evolve/spec-rewriter.js.map +1 -1
  178. package/dist/spec/git/commits.d.ts +85 -0
  179. package/dist/spec/git/commits.d.ts.map +1 -0
  180. package/dist/spec/git/commits.js +218 -0
  181. package/dist/spec/git/commits.js.map +1 -0
  182. package/dist/spec/git/exec.d.ts +13 -0
  183. package/dist/spec/git/exec.d.ts.map +1 -0
  184. package/dist/spec/git/exec.js +19 -0
  185. package/dist/spec/git/exec.js.map +1 -0
  186. package/dist/spec/git/index.d.ts +9 -0
  187. package/dist/spec/git/index.d.ts.map +1 -0
  188. package/dist/spec/git/index.js +22 -0
  189. package/dist/spec/git/index.js.map +1 -0
  190. package/dist/spec/git-utils.d.ts +8 -0
  191. package/dist/spec/git-utils.d.ts.map +1 -0
  192. package/dist/spec/git-utils.js +14 -0
  193. package/dist/spec/git-utils.js.map +1 -0
  194. package/dist/spec/graph/index.d.ts +8 -0
  195. package/dist/spec/graph/index.d.ts.map +1 -0
  196. package/dist/spec/graph/index.js +17 -0
  197. package/dist/spec/graph/index.js.map +1 -0
  198. package/dist/spec/graph/queries.d.ts +6 -35
  199. package/dist/spec/graph/queries.d.ts.map +1 -1
  200. package/dist/spec/graph/queries.js +71 -248
  201. package/dist/spec/graph/queries.js.map +1 -1
  202. package/dist/spec/llm/agent-client.d.ts +59 -0
  203. package/dist/spec/llm/agent-client.d.ts.map +1 -0
  204. package/dist/spec/llm/agent-client.js +213 -0
  205. package/dist/spec/llm/agent-client.js.map +1 -0
  206. package/dist/spec/llm/agents/claude-code.d.ts +32 -0
  207. package/dist/spec/llm/agents/claude-code.d.ts.map +1 -0
  208. package/dist/spec/llm/agents/claude-code.js +125 -0
  209. package/dist/spec/llm/agents/claude-code.js.map +1 -0
  210. package/dist/spec/llm/agents/codex.d.ts +32 -0
  211. package/dist/spec/llm/agents/codex.d.ts.map +1 -0
  212. package/dist/spec/llm/agents/codex.js +145 -0
  213. package/dist/spec/llm/agents/codex.js.map +1 -0
  214. package/dist/spec/llm/agents/detect-utils.d.ts +28 -0
  215. package/dist/spec/llm/agents/detect-utils.d.ts.map +1 -0
  216. package/dist/spec/llm/agents/detect-utils.js +91 -0
  217. package/dist/spec/llm/agents/detect-utils.js.map +1 -0
  218. package/dist/spec/llm/agents/deveco-code.d.ts +33 -0
  219. package/dist/spec/llm/agents/deveco-code.d.ts.map +1 -0
  220. package/dist/spec/llm/agents/deveco-code.js +165 -0
  221. package/dist/spec/llm/agents/deveco-code.js.map +1 -0
  222. package/dist/spec/llm/agents/index.d.ts +31 -0
  223. package/dist/spec/llm/agents/index.d.ts.map +1 -0
  224. package/dist/spec/llm/agents/index.js +65 -0
  225. package/dist/spec/llm/agents/index.js.map +1 -0
  226. package/dist/spec/llm/agents/types.d.ts +76 -0
  227. package/dist/spec/llm/agents/types.d.ts.map +1 -0
  228. package/dist/spec/llm/agents/types.js +15 -0
  229. package/dist/spec/llm/agents/types.js.map +1 -0
  230. package/dist/spec/llm/client.d.ts +31 -11
  231. package/dist/spec/llm/client.d.ts.map +1 -1
  232. package/dist/spec/llm/client.js +143 -81
  233. package/dist/spec/llm/client.js.map +1 -1
  234. package/dist/spec/llm/factory.d.ts +41 -0
  235. package/dist/spec/llm/factory.d.ts.map +1 -0
  236. package/dist/spec/llm/factory.js +80 -0
  237. package/dist/spec/llm/factory.js.map +1 -0
  238. package/dist/spec/llm/prompts.d.ts +24 -8
  239. package/dist/spec/llm/prompts.d.ts.map +1 -1
  240. package/dist/spec/llm/prompts.js +100 -50
  241. package/dist/spec/llm/prompts.js.map +1 -1
  242. package/dist/spec/llm/retry.d.ts +53 -0
  243. package/dist/spec/llm/retry.d.ts.map +1 -0
  244. package/dist/spec/llm/retry.js +153 -0
  245. package/dist/spec/llm/retry.js.map +1 -0
  246. package/dist/spec/mine/clusterer.d.ts +63 -0
  247. package/dist/spec/mine/clusterer.d.ts.map +1 -0
  248. package/dist/spec/mine/clusterer.js +904 -0
  249. package/dist/spec/mine/clusterer.js.map +1 -0
  250. package/dist/spec/mine/clustering/features.d.ts +38 -0
  251. package/dist/spec/mine/clustering/features.d.ts.map +1 -0
  252. package/dist/spec/mine/clustering/features.js +108 -0
  253. package/dist/spec/mine/clustering/features.js.map +1 -0
  254. package/dist/spec/mine/clustering/index.d.ts +62 -0
  255. package/dist/spec/mine/clustering/index.d.ts.map +1 -0
  256. package/dist/spec/mine/clustering/index.js +259 -0
  257. package/dist/spec/mine/clustering/index.js.map +1 -0
  258. package/dist/spec/mine/clustering/leiden.d.ts +61 -0
  259. package/dist/spec/mine/clustering/leiden.d.ts.map +1 -0
  260. package/dist/spec/mine/clustering/leiden.js +489 -0
  261. package/dist/spec/mine/clustering/leiden.js.map +1 -0
  262. package/dist/spec/mine/clustering/text-similarity.d.ts +22 -0
  263. package/dist/spec/mine/clustering/text-similarity.d.ts.map +1 -0
  264. package/dist/spec/mine/clustering/text-similarity.js +98 -0
  265. package/dist/spec/mine/clustering/text-similarity.js.map +1 -0
  266. package/dist/spec/mine/generator.d.ts +54 -0
  267. package/dist/spec/mine/generator.d.ts.map +1 -0
  268. package/dist/spec/mine/generator.js +267 -0
  269. package/dist/spec/mine/generator.js.map +1 -0
  270. package/dist/spec/mine/persist.d.ts +37 -0
  271. package/dist/spec/mine/persist.d.ts.map +1 -0
  272. package/dist/spec/mine/persist.js +170 -0
  273. package/dist/spec/mine/persist.js.map +1 -0
  274. package/dist/spec/mine/pipeline.d.ts +41 -0
  275. package/dist/spec/mine/pipeline.d.ts.map +1 -0
  276. package/dist/spec/mine/pipeline.js +236 -0
  277. package/dist/spec/mine/pipeline.js.map +1 -0
  278. package/dist/spec/mine/progress-handler.d.ts +22 -0
  279. package/dist/spec/mine/progress-handler.d.ts.map +1 -0
  280. package/dist/spec/mine/progress-handler.js +108 -0
  281. package/dist/spec/mine/progress-handler.js.map +1 -0
  282. package/dist/spec/mine/progress.d.ts +23 -0
  283. package/dist/spec/mine/progress.d.ts.map +1 -0
  284. package/dist/spec/mine/progress.js +12 -0
  285. package/dist/spec/mine/progress.js.map +1 -0
  286. package/dist/spec/mine/scanner.d.ts +59 -0
  287. package/dist/spec/mine/scanner.d.ts.map +1 -0
  288. package/dist/spec/mine/scanner.js +383 -0
  289. package/dist/spec/mine/scanner.js.map +1 -0
  290. package/dist/spec/types.d.ts +32 -0
  291. package/dist/spec/types.d.ts.map +1 -1
  292. package/dist/spec/ui/index.d.ts +9 -0
  293. package/dist/spec/ui/index.d.ts.map +1 -0
  294. package/dist/spec/ui/index.js +16 -0
  295. package/dist/spec/ui/index.js.map +1 -0
  296. package/dist/spec/ui/progress-handler.d.ts +33 -0
  297. package/dist/spec/ui/progress-handler.d.ts.map +1 -0
  298. package/dist/spec/ui/progress-handler.js +132 -0
  299. package/dist/spec/ui/progress-handler.js.map +1 -0
  300. package/dist/spec/ui/progress.d.ts +24 -0
  301. package/dist/spec/ui/progress.d.ts.map +1 -0
  302. package/dist/spec/ui/progress.js +13 -0
  303. package/dist/spec/ui/progress.js.map +1 -0
  304. package/dist/spec/utils/fs.d.ts +45 -0
  305. package/dist/spec/utils/fs.d.ts.map +1 -0
  306. package/dist/spec/utils/fs.js +163 -0
  307. package/dist/spec/utils/fs.js.map +1 -0
  308. package/dist/spec/utils/index.d.ts +12 -0
  309. package/dist/spec/utils/index.d.ts.map +1 -0
  310. package/dist/spec/utils/index.js +26 -0
  311. package/dist/spec/utils/index.js.map +1 -0
  312. package/dist/spec/utils/meta.d.ts +32 -0
  313. package/dist/spec/utils/meta.d.ts.map +1 -0
  314. package/dist/spec/utils/meta.js +122 -0
  315. package/dist/spec/utils/meta.js.map +1 -0
  316. package/dist/spec/utils/truncate.d.ts +86 -0
  317. package/dist/spec/utils/truncate.d.ts.map +1 -0
  318. package/dist/spec/utils/truncate.js +172 -0
  319. package/dist/spec/utils/truncate.js.map +1 -0
  320. package/dist/spec/utils.d.ts +4 -16
  321. package/dist/spec/utils.d.ts.map +1 -1
  322. package/dist/spec/utils.js +6 -58
  323. package/dist/spec/utils.js.map +1 -1
  324. package/dist/sync/watcher.d.ts +25 -1
  325. package/dist/sync/watcher.d.ts.map +1 -1
  326. package/dist/sync/watcher.js +76 -3
  327. package/dist/sync/watcher.js.map +1 -1
  328. package/dist/telemetry/index.js +3 -3
  329. package/dist/telemetry/index.js.map +1 -1
  330. package/dist/types.d.ts +3 -0
  331. package/dist/types.d.ts.map +1 -1
  332. package/package.json +7 -3
@@ -41,9 +41,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
41
41
  return (mod && mod.__esModule) ? mod : { "default": mod };
42
42
  };
43
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
- exports.loadAllGrammars = exports.loadGrammarsForLanguages = exports.initGrammars = exports.getSupportedLanguages = exports.isGrammarLoaded = exports.isLanguageSupported = exports.isSourceFile = exports.detectLanguage = exports.extractFromSource = exports.ExtractionOrchestrator = exports.ScopeIgnore = void 0;
44
+ exports.loadAllGrammars = exports.loadGrammarsForLanguages = exports.initGrammars = exports.getSupportedLanguages = exports.isGrammarLoaded = exports.isLanguageSupported = exports.isSourceFile = exports.detectLanguage = exports.extractFromSource = exports.ExtractionOrchestrator = exports.ScopeIgnore = exports.buildDefaultIgnore = void 0;
45
45
  exports.hashContent = hashContent;
46
- exports.buildDefaultIgnore = buildDefaultIgnore;
47
46
  exports.buildScopeIgnore = buildScopeIgnore;
48
47
  exports.discoverEmbeddedRepoRoots = discoverEmbeddedRepoRoots;
49
48
  exports.findUnindexedIgnoredRepos = findUnindexedIgnoredRepos;
@@ -57,12 +56,17 @@ const crypto = __importStar(require("crypto"));
57
56
  const child_process_1 = require("child_process");
58
57
  const tree_sitter_1 = require("./tree-sitter");
59
58
  const parse_pool_1 = require("./parse-pool");
59
+ const store_writer_1 = require("./store-writer");
60
+ const cooperative_yield_1 = require("../resolution/cooperative-yield");
60
61
  const grammars_1 = require("./grammars");
61
62
  const project_config_1 = require("../project-config");
62
63
  const directory_1 = require("../directory");
63
64
  const errors_1 = require("../errors");
64
65
  const utils_1 = require("../utils");
65
66
  const ignore_1 = __importDefault(require("ignore"));
67
+ const default_ignore_1 = require("./default-ignore");
68
+ var default_ignore_2 = require("./default-ignore");
69
+ Object.defineProperty(exports, "buildDefaultIgnore", { enumerable: true, get: function () { return default_ignore_2.buildDefaultIgnore; } });
66
70
  const frameworks_1 = require("../resolution/frameworks");
67
71
  const context_1 = require("./context");
68
72
  const arkts_1 = require("./languages/arkts");
@@ -109,176 +113,6 @@ function hashContent(content) {
109
113
  * symbols. 1 MB covers essentially all hand-written source.
110
114
  */
111
115
  const MAX_FILE_SIZE = 1024 * 1024;
112
- /**
113
- * Directory names that are dependency, build, cache, or tooling output across the
114
- * languages/frameworks HomeGraph supports — curated from the canonical
115
- * github/gitignore templates. Excluded by default so the graph reflects your code,
116
- * not third-party noise, without requiring a `.gitignore` (issue #407). The
117
- * exclusion applies uniformly (git or not, tracked or not); the only opt-in is an
118
- * explicit `.gitignore` negation (e.g. `!vendor/`). First-party-prone or generic
119
- * names (`packages`, `lib`, `app`, `bin`, `src`, `deps`, `env`, `tmp`, `storage`,
120
- * `Library`) are deliberately NOT listed, to avoid ever hiding real source.
121
- *
122
- * Only dirs that actually contain *indexable source* (or are enormous) earn a slot
123
- * — IDE/state dirs like `.idea`/`.vs` are omitted because HomeGraph indexes only
124
- * recognized source extensions, so they produce no symbols regardless.
125
- */
126
- const DEFAULT_IGNORE_DIRS = new Set([
127
- // JS / TS — dependency directories
128
- 'node_modules', 'bower_components', 'jspm_packages', 'web_modules',
129
- '.yarn', '.pnpm-store',
130
- // JS / TS — framework & bundler build / cache / deploy output
131
- '.next', '.nuxt', '.svelte-kit', '.turbo', '.vite', '.parcel-cache', '.angular',
132
- '.docusaurus', 'storybook-static', '.vinxi', '.nitro', 'out-tsc',
133
- '.vercel', '.netlify', '.wrangler',
134
- // Build output (common across ecosystems)
135
- 'dist', 'build', 'out', '.output',
136
- // Test / coverage
137
- 'coverage', '.nyc_output',
138
- // Python
139
- '__pycache__', '__pypackages__', '.venv', 'venv', '.pixi', '.pdm-build',
140
- '.mypy_cache', '.pytest_cache', '.ruff_cache', '.tox', '.nox', '.hypothesis',
141
- '.ipynb_checkpoints', '.eggs',
142
- // Rust / JVM (Maven, Gradle, Scala)
143
- 'target', '.gradle',
144
- // .NET
145
- 'obj',
146
- // Vendored deps (Go, PHP/Composer, Ruby/Bundler)
147
- 'vendor',
148
- // Swift / iOS
149
- '.build', 'Pods', 'Carthage', 'DerivedData', '.swiftpm',
150
- // Dart / Flutter
151
- '.dart_tool', '.pub-cache',
152
- // Native (Android NDK, C/C++ deps)
153
- '.cxx', '.externalNativeBuild', 'vcpkg_installed',
154
- // Scala tooling
155
- '.bloop', '.metals',
156
- // Lua / Luau (LuaRocks)
157
- 'lua_modules', '.luarocks',
158
- // Delphi / RAD Studio IDE backups (duplicate .pas source — would double-count)
159
- '__history', '__recovery',
160
- // Generic cache
161
- '.cache',
162
- ]);
163
- /**
164
- * Android resource directory types. A `res/` tree holds ONLY non-code resources —
165
- * layouts, drawables, value bags (strings/colors/styles), menus, navigation
166
- * graphs — split into one typed subdirectory per kind, optionally density/locale/
167
- * version-qualified (`values-es`, `drawable-hdpi`, `layout-v21`, …). None of it
168
- * yields an extractable code symbol, yet on an Android app it DOMINATES the tree
169
- * (one report: 26k XML files = 97% of the project, 0 symbols), bloating the DB,
170
- * slowing indexing, and skewing both the file count and `codegraph_explore`
171
- * results (#1047). So these are excluded by default. The structure is
172
- * self-identifying — a non-Android project has no `res/layout/` etc., so it's
173
- * untouched — and the only XML that DOES produce symbols (MyBatis mappers) lives
174
- * under `src/main/resources/`, never `res/`, so nothing useful is dropped.
175
- * `res/raw/` is deliberately NOT here: it holds arbitrary bundled assets that can
176
- * be code-ish (a `.sql` schema, a `.js`), so we leave it indexed. Override any of
177
- * these with a `.gitignore` negation (e.g. `!res/values/`).
178
- */
179
- const ANDROID_RES_TYPES = [
180
- 'anim', 'animator', 'color', 'drawable', 'font', 'layout',
181
- 'menu', 'mipmap', 'navigation', 'transition', 'values', 'xml',
182
- ];
183
- /** Gitignore-style patterns for the `ignore` matcher: the dirs above plus a few globs. */
184
- const DEFAULT_IGNORE_PATTERNS = [
185
- ...Array.from(DEFAULT_IGNORE_DIRS, (d) => `${d}/`),
186
- '*.egg-info/', // Python packaging metadata
187
- 'cmake-build-*/', // CLion / CMake build trees
188
- 'bazel-*/', // Bazel output symlink trees
189
- // Android resource dirs at any depth, with their qualifier variants (#1047).
190
- ...ANDROID_RES_TYPES.map((t) => `**/res/${t}*/`),
191
- ];
192
- /** True if `buf` decodes as strict UTF-8 (no invalid byte sequences). */
193
- function isValidUtf8(buf) {
194
- try {
195
- new TextDecoder('utf-8', { fatal: true }).decode(buf);
196
- return true;
197
- }
198
- catch {
199
- return false;
200
- }
201
- }
202
- /**
203
- * Read a `.gitignore` and return patterns safe to hand to the `ignore` matcher —
204
- * never throwing, even when the file isn't real gitignore text. Two failure
205
- * modes, both seen in the wild (issue #682):
206
- *
207
- * - The file isn't valid UTF-8 — e.g. transparently encrypted in place by
208
- * corporate DLP / endpoint-security software, leaving a UTF-16 header plus
209
- * ciphertext. None of it is meaningful patterns, so the whole file is skipped.
210
- * - The file is text but a single line can't be compiled to a regex by the
211
- * `ignore` library — `\\[` and friends throw "Unterminated character class".
212
- * Crucially the throw is LAZY (at match time, not `.add()`), so it would
213
- * otherwise escape mid-scan. That one pattern is dropped; the rest are kept.
214
- *
215
- * Either way a warning that NAMES the file is logged (the reporter couldn't tell
216
- * which `.gitignore` was at fault) and indexing continues instead of aborting.
217
- * Returns '' when there's nothing usable.
218
- */
219
- function readGitignorePatterns(giPath) {
220
- let buf;
221
- try {
222
- buf = fs.readFileSync(giPath);
223
- }
224
- catch {
225
- return ''; // unreadable (permissions / race) — treat as absent
226
- }
227
- // A NUL byte never appears in real gitignore text, and a fatal UTF-8 decode
228
- // catches the rest. Such a file isn't ignore patterns at all.
229
- if (buf.includes(0) || !isValidUtf8(buf)) {
230
- (0, errors_1.logWarn)('Ignoring a .gitignore that is not valid UTF-8 text — it may have been encrypted ' +
231
- 'in place by endpoint-security software. Indexing continues without it.', { file: giPath });
232
- return '';
233
- }
234
- const content = buf.toString('utf-8');
235
- // Fast path: one `.ignores()` call forces the library to compile EVERY rule,
236
- // so if it doesn't throw, the whole file is safe to use verbatim.
237
- try {
238
- (0, ignore_1.default)().add(content).ignores('.homegraph-probe');
239
- return content;
240
- }
241
- catch {
242
- // Fall through: a line is uncompilable — keep the good ones, drop the bad.
243
- }
244
- const kept = [];
245
- let dropped = 0;
246
- for (const line of content.split(/\r?\n/)) {
247
- try {
248
- (0, ignore_1.default)().add(line).ignores('.homegraph-probe');
249
- kept.push(line);
250
- }
251
- catch {
252
- dropped++;
253
- }
254
- }
255
- if (dropped > 0) {
256
- (0, errors_1.logWarn)(`Skipped ${dropped} unparseable pattern(s) in a .gitignore; the rest are applied.`, { file: giPath });
257
- }
258
- return kept.join('\n');
259
- }
260
- /**
261
- * An `ignore` matcher seeded with the built-in defaults, merged with the project's
262
- * root .gitignore so a negation there (e.g. `!vendor/`) overrides a default. Shared
263
- * by both enumeration paths so behavior is identical with or without git — and so
264
- * the defaults apply to tracked files too (committing a dependency dir doesn't make
265
- * it project code; the explicit `.gitignore` negation is the only opt-in).
266
- */
267
- function buildDefaultIgnore(rootDir) {
268
- const ig = (0, ignore_1.default)().add(DEFAULT_IGNORE_PATTERNS);
269
- const rootGitignore = path.join(rootDir, '.gitignore');
270
- if (fs.existsSync(rootGitignore))
271
- ig.add(readGitignorePatterns(rootGitignore));
272
- return ig;
273
- }
274
- /**
275
- * Defaults-only ignore matcher (no root `.gitignore` merged). Used wherever the
276
- * parent repo's own ignore rules must NOT apply — inside embedded child repos,
277
- * whose gitignore semantics their own `git ls-files` already enforced (#514).
278
- */
279
- function defaultsOnlyIgnore() {
280
- return (0, ignore_1.default)().add(DEFAULT_IGNORE_PATTERNS);
281
- }
282
116
  /**
283
117
  * Matcher for the project's `homegraph.json` `includeIgnored` patterns — the
284
118
  * explicit opt-in to index embedded git repos living inside gitignored
@@ -377,7 +211,7 @@ function includeStaticRoots(patterns) {
377
211
  */
378
212
  function collectIncludedFiles(rootDir, include, exclude, roots, overrides) {
379
213
  const out = new Set();
380
- const defaults = defaultsOnlyIgnore();
214
+ const defaults = (0, default_ignore_1.defaultsOnlyIgnore)();
381
215
  const visited = new Set();
382
216
  const consider = (abs, rel, isDir) => {
383
217
  if (isDir) {
@@ -544,7 +378,7 @@ function classifyGitDir(absDir) {
544
378
  */
545
379
  function findNestedGitRepos(absDir, relPrefix) {
546
380
  const found = [];
547
- const defaults = defaultsOnlyIgnore();
381
+ const defaults = (0, default_ignore_1.defaultsOnlyIgnore)();
548
382
  const queue = [
549
383
  { abs: absDir, rel: relPrefix, depth: 0 },
550
384
  ];
@@ -602,7 +436,7 @@ class ScopeIgnore {
602
436
  include;
603
437
  includeRoots;
604
438
  embedded;
605
- defaults = defaultsOnlyIgnore();
439
+ defaults = (0, default_ignore_1.defaultsOnlyIgnore)();
606
440
  constructor(rootMatcher, embedded,
607
441
  /**
608
442
  * Project `homegraph.json` `exclude` patterns (#999), matched against the
@@ -674,7 +508,7 @@ exports.ScopeIgnore = ScopeIgnore;
674
508
  function buildScopeIgnore(rootDir, embeddedRoots) {
675
509
  const roots = embeddedRoots ? [...embeddedRoots] : discoverEmbeddedRepoRoots(rootDir);
676
510
  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)) : []);
511
+ return new ScopeIgnore((0, default_ignore_1.buildDefaultIgnore)(rootDir), roots.map((root) => ({ root, matcher: (0, default_ignore_1.buildDefaultIgnore)(path.join(rootDir, root)) })), loadExcludeMatcher(rootDir), include, include ? includeStaticRoots((0, project_config_1.loadIncludePatterns)(rootDir)) : []);
678
512
  }
679
513
  /**
680
514
  * Whether an embedded repo found as a tracked gitlink (mode 160000, #1031/#1033)
@@ -722,7 +556,7 @@ function discoverEmbeddedRepoRoots(rootDir) {
722
556
  return [];
723
557
  }
724
558
  const out = [];
725
- const defaults = defaultsOnlyIgnore();
559
+ const defaults = (0, default_ignore_1.defaultsOnlyIgnore)();
726
560
  const includeIgnored = loadIncludeIgnoredMatcher(rootDir);
727
561
  const visit = (repoAbs, prefix) => {
728
562
  const candidates = [];
@@ -742,7 +576,7 @@ function discoverEmbeddedRepoRoots(rootDir) {
742
576
  // (#1031, #1033)
743
577
  try {
744
578
  const gitOpts = { cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true };
745
- const repoIgnore = buildDefaultIgnore(repoAbs);
579
+ const repoIgnore = (0, default_ignore_1.buildDefaultIgnore)(repoAbs);
746
580
  for (const rel of listGitlinkRels(gitOpts)) {
747
581
  const relDir = rel.endsWith('/') ? rel : rel + '/';
748
582
  // A gitlink under a gitignored path is respected (not indexed) unless the
@@ -792,7 +626,7 @@ function findUnindexedIgnoredRepos(rootDir) {
792
626
  catch {
793
627
  return [];
794
628
  }
795
- const defaults = defaultsOnlyIgnore();
629
+ const defaults = (0, default_ignore_1.defaultsOnlyIgnore)();
796
630
  const includeIgnored = loadIncludeIgnoredMatcher(rootDir);
797
631
  const repos = [];
798
632
  for (const dir of listIgnoredDirs(rootDir)) {
@@ -826,7 +660,7 @@ function findUnindexedIgnoredRepos(rootDir) {
826
660
  function findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix) {
827
661
  if (!includeIgnored)
828
662
  return [];
829
- const defaults = defaultsOnlyIgnore();
663
+ const defaults = (0, default_ignore_1.defaultsOnlyIgnore)();
830
664
  const repos = [];
831
665
  for (const dir of listIgnoredDirs(repoDir)) {
832
666
  if (defaults.ignores(dir))
@@ -938,7 +772,7 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
938
772
  const childDir = path.join(repoDir, rel);
939
773
  // A git worktree surfaces here as an opaque untracked dir too — skip it,
940
774
  // it's a duplicate working view of an already-indexed repo (#848).
941
- if (classifyGitDir(childDir) === 'embedded' && !defaultsOnlyIgnore().ignores(rel)) {
775
+ if (classifyGitDir(childDir) === 'embedded' && !(0, default_ignore_1.defaultsOnlyIgnore)().ignores(rel)) {
942
776
  embeddedRoots?.add((0, utils_1.normalizePath)(prefix + rel));
943
777
  collectGitFiles(childDir, prefix + rel, files, embeddedRoots, includeIgnored);
944
778
  }
@@ -955,8 +789,8 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
955
789
  // (an uninitialized submodule — empty dir, no `.git`) has nothing to index and
956
790
  // is left alone, as is a submodule worktree (a duplicate view, #945). (#1031, #1033)
957
791
  if (gitlinkRels.length > 0) {
958
- const defaults = defaultsOnlyIgnore();
959
- const repoIgnore = buildDefaultIgnore(repoDir);
792
+ const defaults = (0, default_ignore_1.defaultsOnlyIgnore)();
793
+ const repoIgnore = (0, default_ignore_1.buildDefaultIgnore)(repoDir);
960
794
  for (const rel of gitlinkRels) {
961
795
  const relDir = rel.endsWith('/') ? rel : rel + '/';
962
796
  // A gitlink under a gitignored path is respected (not indexed) unless the
@@ -1064,7 +898,7 @@ function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null
1064
898
  // repo-relative `rel` at each recursion level mirrors getGitVisibleFiles'
1065
899
  // ScopeIgnore: every embedded repo is judged by ITS OWN rules, never the
1066
900
  // parent's. (#766)
1067
- const ig = buildDefaultIgnore(repoDir);
901
+ const ig = (0, default_ignore_1.buildDefaultIgnore)(repoDir);
1068
902
  const untrackedDirs = [];
1069
903
  for (const line of output.split('\n')) {
1070
904
  if (line.length < 4)
@@ -1187,7 +1021,7 @@ function scanDirectoryWalk(rootDir, onProgress) {
1187
1021
  // readGitignorePatterns is defensive: a non-UTF-8 (DLP-encrypted) or
1188
1022
  // uncompilable .gitignore is skipped/filtered with a warning, never thrown
1189
1023
  // (issue #682) — so the per-file `.ignores()` calls below can't crash.
1190
- const patterns = readGitignorePatterns(giPath);
1024
+ const patterns = (0, default_ignore_1.readGitignorePatterns)(giPath);
1191
1025
  return patterns ? { dir, ig: (0, ignore_1.default)().add(patterns) } : null;
1192
1026
  };
1193
1027
  const isIgnored = (fullPath, isDir, matchers) => {
@@ -1274,7 +1108,7 @@ function scanDirectoryWalk(rootDir, onProgress) {
1274
1108
  }
1275
1109
  // Seed a base matcher with the built-in default ignores (merged with the root
1276
1110
  // .gitignore so a negation can override). Nested .gitignores still layer per-dir.
1277
- const baseMatchers = [{ dir: rootDir, ig: buildDefaultIgnore(rootDir) }];
1111
+ const baseMatchers = [{ dir: rootDir, ig: (0, default_ignore_1.buildDefaultIgnore)(rootDir) }];
1278
1112
  // Project `codegraph.json` `exclude` patterns (#999), rooted at the project so
1279
1113
  // `isIgnored` matches them against root-relative paths — same coverage the
1280
1114
  // git path gets via ScopeIgnore, for non-git projects.
@@ -1420,7 +1254,7 @@ class ExtractionOrchestrator {
1420
1254
  /**
1421
1255
  * Index all files in the project
1422
1256
  */
1423
- async indexAll(onProgress, signal, verbose) {
1257
+ async indexAll(onProgress, signal, verbose, walBackpressure, storeWriterOpts) {
1424
1258
  await (0, grammars_1.initGrammars)();
1425
1259
  const startTime = Date.now();
1426
1260
  const errors = [];
@@ -1522,6 +1356,15 @@ class ExtractionOrchestrator {
1522
1356
  // In-process fallback: load grammars locally and parse on the main thread.
1523
1357
  await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
1524
1358
  }
1359
+ const storeWorkerPath = path.join(__dirname, 'store-worker.js');
1360
+ let storeWriter = null;
1361
+ if (storeWriterOpts &&
1362
+ process.env.HOMEGRAPH_NO_STORE_WORKER !== '1' &&
1363
+ fs.existsSync(storeWorkerPath)) {
1364
+ storeWriter = new store_writer_1.StoreWriter(storeWorkerPath, storeWriterOpts.dbPath, storeWriterOpts.fastInit);
1365
+ log('Store writer thread active');
1366
+ }
1367
+ const STORE_WRITER_WINDOW = 64;
1525
1368
  /**
1526
1369
  * Parse one file: on the pool when available (the promise REJECTS on a worker
1527
1370
  * crash/timeout — the caller records it and the retry pass re-attempts), or
@@ -1551,12 +1394,21 @@ class ExtractionOrchestrator {
1551
1394
  let nextSeq = 0; // file-order sequence assigned at dispatch
1552
1395
  let nextToStore = 0; // cursor: next sequence to commit
1553
1396
  let aborted = false;
1554
- const storeResult = (filePath, content, stats, result) => {
1397
+ const commitYield = (0, cooperative_yield_1.createYielder)();
1398
+ const storeResult = async (filePath, content, stats, result) => {
1555
1399
  processed++;
1556
- // Store in database on main thread (SQLite is not thread-safe)
1400
+ const bp = walBackpressure?.();
1401
+ if (bp)
1402
+ await bp;
1557
1403
  if (result.nodes.length > 0 || result.errors.length === 0) {
1558
1404
  const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1559
- this.storeExtractionResult(filePath, content, language, stats, result);
1405
+ if (storeWriter) {
1406
+ storeWriter.send(this.buildFreshStoreBundle(filePath, content, language, stats, result));
1407
+ await storeWriter.waitBelow(STORE_WRITER_WINDOW);
1408
+ }
1409
+ else {
1410
+ await this.storeExtractionResult(filePath, content, language, stats, result, commitYield);
1411
+ }
1560
1412
  }
1561
1413
  if (result.errors.length > 0) {
1562
1414
  for (const err of result.errors) {
@@ -1574,9 +1426,6 @@ class ExtractionOrchestrator {
1574
1426
  filesErrored++;
1575
1427
  }
1576
1428
  else {
1577
- // Files with no symbols but no errors (yaml, twig, properties) are
1578
- // tracked at the file level — count them as indexed so the CLI doesn't
1579
- // misleadingly report "No files found to index".
1580
1429
  const lang = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1581
1430
  if ((0, grammars_1.isFileLevelOnlyLanguage)(lang)) {
1582
1431
  filesIndexed++;
@@ -1598,22 +1447,28 @@ class ExtractionOrchestrator {
1598
1447
  });
1599
1448
  onProgress?.({ phase: 'parsing', current: processed, total });
1600
1449
  };
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.
1450
+ let flushChain = Promise.resolve();
1451
+ let flushError = null;
1605
1452
  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);
1616
- }
1453
+ flushChain = flushChain.then(async () => {
1454
+ if (aborted || flushError)
1455
+ return;
1456
+ try {
1457
+ while (completed.has(nextToStore)) {
1458
+ const item = completed.get(nextToStore);
1459
+ completed.delete(nextToStore);
1460
+ nextToStore++;
1461
+ if (item.ok)
1462
+ await storeResult(item.filePath, item.content, item.stats, item.result);
1463
+ else
1464
+ recordParseFailure(item.filePath, item.err);
1465
+ }
1466
+ }
1467
+ catch (err) {
1468
+ flushError = err;
1469
+ }
1470
+ });
1471
+ return flushChain;
1617
1472
  };
1618
1473
  // Dispatch one file's parse (parses run concurrently across the pool), tagged
1619
1474
  // with its file-order sequence so flushOrdered commits results in order. The
@@ -1636,10 +1491,15 @@ class ExtractionOrchestrator {
1636
1491
  // buffered), not just in-flight: a slow file sitting at the commit cursor
1637
1492
  // lets later parses finish and buffer, which would otherwise grow without
1638
1493
  // 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);
1494
+ // window has room. When nothing is in flight but the window is still full,
1495
+ // the async commit chain is what's behind — await it so the cursor
1496
+ // advances (buffered items hold whole file contents, so this bound is
1497
+ // load-bearing for memory).
1498
+ while (nextSeq - nextToStore >= windowSize) {
1499
+ if (inFlight.size > 0)
1500
+ await Promise.race(inFlight);
1501
+ else
1502
+ await flushOrdered();
1643
1503
  }
1644
1504
  };
1645
1505
  const firstEtsFile = files.find((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts');
@@ -1744,9 +1604,25 @@ class ExtractionOrchestrator {
1744
1604
  // then commit any results the cursor hasn't reached yet.
1745
1605
  if (!aborted) {
1746
1606
  await Promise.all(inFlight);
1747
- flushOrdered();
1607
+ await flushOrdered();
1608
+ if (flushError) {
1609
+ if (storeWriter)
1610
+ await storeWriter.close();
1611
+ throw flushError;
1612
+ }
1613
+ if (storeWriter) {
1614
+ try {
1615
+ await storeWriter.drain();
1616
+ }
1617
+ finally {
1618
+ await storeWriter.close();
1619
+ storeWriter = null;
1620
+ }
1621
+ }
1748
1622
  }
1749
1623
  if (signal?.aborted || aborted) {
1624
+ if (storeWriter)
1625
+ await storeWriter.close();
1750
1626
  if (pool)
1751
1627
  await pool.destroy();
1752
1628
  return {
@@ -1808,7 +1684,7 @@ class ExtractionOrchestrator {
1808
1684
  if (result.nodes.length > 0 || result.errors.length === 0) {
1809
1685
  const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1810
1686
  const stats = await fsp.stat(path.join(this.rootDir, filePath));
1811
- this.storeExtractionResult(filePath, content, language, stats, result);
1687
+ await this.storeExtractionResult(filePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
1812
1688
  const idx = errors.indexOf(errEntry);
1813
1689
  if (idx >= 0)
1814
1690
  errors.splice(idx, 1);
@@ -1856,7 +1732,7 @@ class ExtractionOrchestrator {
1856
1732
  if (result.nodes.length > 0 || result.errors.length === 0) {
1857
1733
  const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
1858
1734
  const stats = await fsp.stat(path.join(this.rootDir, filePath));
1859
- this.storeExtractionResult(filePath, fullContent, language, stats, result);
1735
+ await this.storeExtractionResult(filePath, fullContent, language, stats, result, (0, cooperative_yield_1.createYielder)());
1860
1736
  const idx = errors.indexOf(errEntry);
1861
1737
  if (idx >= 0)
1862
1738
  errors.splice(idx, 1);
@@ -2034,108 +1910,77 @@ class ExtractionOrchestrator {
2034
1910
  const result = (0, tree_sitter_1.extractFromSource)(relativePath, content, language, frameworkNames);
2035
1911
  // Store in database
2036
1912
  if (result.nodes.length > 0 || result.errors.length === 0) {
2037
- this.storeExtractionResult(relativePath, content, language, stats, result);
1913
+ await this.storeExtractionResult(relativePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
2038
1914
  }
2039
1915
  return result;
2040
1916
  }
2041
1917
  /**
2042
1918
  * Store extraction result in database
2043
1919
  */
2044
- storeExtractionResult(filePath, content, language, stats, result) {
1920
+ async storeExtractionResult(filePath, content, language, stats, result, onYield) {
1921
+ const STORE_CHUNK = 2000;
2045
1922
  const contentHash = hashContent(content);
2046
- // Check if file already exists and hasn't changed
2047
1923
  const existingFile = this.queries.getFileByPath(filePath);
2048
1924
  if (existingFile && existingFile.contentHash === contentHash) {
2049
- return; // No changes
2050
- }
2051
- // Snapshot incoming cross-file edges BEFORE deleting this file's nodes.
2052
- // `deleteFile` cascades to delete every edge whose source OR target is a
2053
- // node in this file (edges.FK ... ON DELETE CASCADE). Edges whose SOURCE is
2054
- // in this file are re-emitted by the extractor below, but edges whose SOURCE
2055
- // is in a *different* (unchanged) file are not — they would be silently
2056
- // dropped, which is issue #899: re-indexing a callee file severs `calls`/
2057
- // `references` edges from callers that import it via module-attribute
2058
- // access (`pkg.mod.fn(...)`).
2059
- //
2060
- // We snapshot the edge plus the target node's (name, kind) so we can
2061
- // re-resolve to the re-indexed target's NEW id. Node ids are
2062
- // `sha256(filePath:kind:name:line)`, so any line shift in the callee file
2063
- // (e.g. a docstring-only edit above the symbol) changes every target id and
2064
- // a naive re-insert by old id would silently drop every edge. Matching by
2065
- // (filePath, kind, name) is stable across line shifts; if the symbol was
2066
- // renamed/removed, no match is found and the edge stays dropped (correct).
1925
+ return;
1926
+ }
2067
1927
  const crossFileIncomingEdges = existingFile
2068
1928
  ? this.queries.getCrossFileIncomingEdgesWithTarget(filePath)
2069
1929
  : [];
2070
- // Delete existing data for this file
2071
1930
  if (existingFile) {
2072
1931
  this.queries.deleteFile(filePath);
2073
1932
  }
2074
- // Filter out nodes with missing required fields before insertion.
2075
- // This prevents FK violations when edges reference nodes that would
2076
- // be silently skipped by insertNode() (see issue #42).
2077
1933
  const validNodes = result.nodes.filter((n) => n.id && n.kind && n.name && n.filePath && n.language);
2078
- // Insert nodes
2079
- if (validNodes.length > 0) {
2080
- this.queries.insertNodes(validNodes);
2081
- }
2082
- // Filter edges to only reference nodes that were actually inserted
2083
- if (result.edges.length > 0) {
2084
- const insertedIds = new Set(validNodes.map((n) => n.id));
2085
- const validEdges = result.edges.filter((e) => insertedIds.has(e.source) && insertedIds.has(e.target));
2086
- if (validEdges.length > 0) {
2087
- this.queries.insertEdges(validEdges);
1934
+ const insertedIds = new Set(validNodes.map((n) => n.id));
1935
+ const validEdges = result.edges.filter((e) => insertedIds.has(e.source) && insertedIds.has(e.target));
1936
+ const validRefs = result.unresolvedReferences
1937
+ .filter((ref) => insertedIds.has(ref.fromNodeId))
1938
+ .map((ref) => ({
1939
+ ...ref,
1940
+ filePath: ref.filePath ?? filePath,
1941
+ language: ref.language ?? language,
1942
+ }));
1943
+ const fitsOneChunk = validNodes.length <= STORE_CHUNK &&
1944
+ validEdges.length <= STORE_CHUNK &&
1945
+ validRefs.length <= STORE_CHUNK;
1946
+ if (fitsOneChunk) {
1947
+ this.queries.storeFileBundle({
1948
+ nodes: validNodes,
1949
+ edges: validEdges,
1950
+ refs: validRefs,
1951
+ file: {
1952
+ path: filePath,
1953
+ contentHash,
1954
+ language,
1955
+ size: stats.size,
1956
+ modifiedAt: stats.mtimeMs,
1957
+ indexedAt: Date.now(),
1958
+ nodeCount: result.nodes.length,
1959
+ errors: result.errors.length > 0 ? result.errors : undefined,
1960
+ },
1961
+ });
1962
+ if (crossFileIncomingEdges.length > 0) {
1963
+ this.reattachCrossFileEdges(crossFileIncomingEdges, validNodes);
1964
+ }
1965
+ return;
1966
+ }
1967
+ for (let i = 0; i < validNodes.length; i += STORE_CHUNK) {
1968
+ this.queries.insertNodes(validNodes.slice(i, i + STORE_CHUNK));
1969
+ await onYield?.();
1970
+ }
1971
+ if (validEdges.length > 0) {
1972
+ for (let i = 0; i < validEdges.length; i += STORE_CHUNK) {
1973
+ this.queries.insertEdges(validEdges.slice(i, i + STORE_CHUNK));
1974
+ await onYield?.();
2088
1975
  }
2089
1976
  }
2090
- // Re-insert cross-file incoming edges snapshotted before the delete,
2091
- // re-resolving each edge's target to the re-indexed node's new id by
2092
- // (filePath, kind, name). Node ids include the source line, so any line
2093
- // shift in the callee file (e.g. a docstring-only edit above the symbol)
2094
- // changes every target id and a naive re-insert by old id would drop them
2095
- // all. `insertEdges` still filters to endpoints that exist, so edges whose
2096
- // caller (source) was deleted, or whose callee (target) was renamed/removed
2097
- // during the re-index (no match in `newTargetIds`), are dropped. This
2098
- // closes the #899 edge-drop on `sync`.
2099
1977
  if (crossFileIncomingEdges.length > 0) {
2100
- const newNodesByKindName = new Map();
2101
- for (const n of validNodes) {
2102
- newNodesByKindName.set(`${n.kind}\0${n.name}`, n.id);
2103
- }
2104
- const reinserted = [];
2105
- const resurrected = [];
2106
- for (const e of crossFileIncomingEdges) {
2107
- const newTargetId = newNodesByKindName.get(`${e.targetKind}\0${e.targetName}`);
2108
- if (newTargetId) {
2109
- reinserted.push({ source: e.source, target: newTargetId, kind: e.kind, metadata: e.metadata, line: e.line, column: e.column, provenance: e.provenance });
2110
- }
2111
- else {
2112
- const ref = resurrectRefFromDroppedEdge(e);
2113
- if (ref)
2114
- resurrected.push(ref);
2115
- }
2116
- }
2117
- if (reinserted.length > 0) {
2118
- this.queries.insertEdges(reinserted);
2119
- }
2120
- if (resurrected.length > 0) {
2121
- this.queries.insertUnresolvedRefsBatch(resurrected);
2122
- }
1978
+ this.reattachCrossFileEdges(crossFileIncomingEdges, validNodes);
2123
1979
  }
2124
- // Insert unresolved references in batch with denormalized filePath/language
2125
- if (result.unresolvedReferences.length > 0) {
2126
- const insertedIds = new Set(validNodes.map((n) => n.id));
2127
- const refsWithContext = result.unresolvedReferences
2128
- .filter((ref) => insertedIds.has(ref.fromNodeId))
2129
- .map((ref) => ({
2130
- ...ref,
2131
- filePath: ref.filePath ?? filePath,
2132
- language: ref.language ?? language,
2133
- }));
2134
- if (refsWithContext.length > 0) {
2135
- this.queries.insertUnresolvedRefsBatch(refsWithContext);
2136
- }
1980
+ for (let i = 0; i < validRefs.length; i += STORE_CHUNK) {
1981
+ this.queries.insertUnresolvedRefsBatch(validRefs.slice(i, i + STORE_CHUNK));
1982
+ await onYield?.();
2137
1983
  }
2138
- // Insert file record
2139
1984
  const fileRecord = {
2140
1985
  path: filePath,
2141
1986
  contentHash,
@@ -2148,6 +1993,46 @@ class ExtractionOrchestrator {
2148
1993
  };
2149
1994
  this.queries.upsertFile(fileRecord);
2150
1995
  }
1996
+ buildFileRecord(filePath, content, language, stats, nodeCount, resultErrors) {
1997
+ return {
1998
+ path: filePath,
1999
+ contentHash: hashContent(content),
2000
+ language,
2001
+ size: stats.size,
2002
+ modifiedAt: stats.mtimeMs,
2003
+ indexedAt: Date.now(),
2004
+ nodeCount,
2005
+ errors: resultErrors.length > 0 ? resultErrors : undefined,
2006
+ };
2007
+ }
2008
+ buildFreshStoreBundle(filePath, content, language, stats, result) {
2009
+ return (0, store_writer_1.finalizeStoreBundle)(result, filePath, language, this.buildFileRecord(filePath, content, language, stats, result.nodes.length, result.errors));
2010
+ }
2011
+ reattachCrossFileEdges(crossFileIncomingEdges, validNodes) {
2012
+ const newNodesByKindName = new Map();
2013
+ for (const n of validNodes) {
2014
+ newNodesByKindName.set(`${n.kind}\0${n.name}`, n.id);
2015
+ }
2016
+ const reinserted = [];
2017
+ const resurrected = [];
2018
+ for (const e of crossFileIncomingEdges) {
2019
+ const newTargetId = newNodesByKindName.get(`${e.targetKind}\0${e.targetName}`);
2020
+ if (newTargetId) {
2021
+ reinserted.push({ source: e.source, target: newTargetId, kind: e.kind, metadata: e.metadata, line: e.line, column: e.column, provenance: e.provenance });
2022
+ }
2023
+ else {
2024
+ const ref = resurrectRefFromDroppedEdge(e);
2025
+ if (ref)
2026
+ resurrected.push(ref);
2027
+ }
2028
+ }
2029
+ if (reinserted.length > 0) {
2030
+ this.queries.insertEdges(reinserted);
2031
+ }
2032
+ if (resurrected.length > 0) {
2033
+ this.queries.insertUnresolvedRefsBatch(resurrected);
2034
+ }
2035
+ }
2151
2036
  /**
2152
2037
  * Sync the index with the current file state.
2153
2038
  *
@@ -2156,7 +2041,7 @@ class ExtractionOrchestrator {
2156
2041
  * changes. This works in non-git projects and catches committed changes from
2157
2042
  * `git pull`/`checkout`/`merge`/`rebase` that `git status` cannot see.
2158
2043
  */
2159
- async sync(onProgress) {
2044
+ async sync(onProgress, scopedPaths) {
2160
2045
  await (0, grammars_1.initGrammars)(); // Initialize WASM runtime (grammars loaded lazily below)
2161
2046
  const startTime = Date.now();
2162
2047
  let filesChecked = 0;
@@ -2171,33 +2056,36 @@ class ExtractionOrchestrator {
2171
2056
  total: 0,
2172
2057
  });
2173
2058
  const filesToIndex = [];
2174
- // === Filesystem reconcile (git-independent) ===
2175
- // The source of truth for "what changed" is the filesystem vs the indexed
2176
- // state — never git. We enumerate the current source files and reconcile
2177
- // each against the DB. A cheap (size, mtime) stat pre-filter skips unchanged
2178
- // files without reading or hashing them, so the expensive read+hash+parse
2179
- // only runs for files that actually changed. This catches edits/adds/deletes
2180
- // whether or not the project uses git, and crucially also catches committed
2181
- // changes from `git pull`/`checkout`/`merge`/`rebase` — which `git status`
2182
- // cannot see, because the working tree is clean afterward.
2183
- const currentFiles = await scanDirectoryAsync(this.rootDir);
2184
- filesChecked = currentFiles.length;
2059
+ let currentFiles;
2060
+ let trackedFiles;
2061
+ if (scopedPaths && scopedPaths.length > 0) {
2062
+ const unique = [...new Set(scopedPaths)];
2063
+ currentFiles = unique.filter((p) => fs.existsSync(path.join(this.rootDir, p)));
2064
+ trackedFiles = [];
2065
+ for (const p of unique) {
2066
+ const rec = this.queries.getFileByPath(p);
2067
+ if (rec)
2068
+ trackedFiles.push(rec);
2069
+ }
2070
+ filesChecked = unique.length;
2071
+ }
2072
+ else {
2073
+ currentFiles = await scanDirectoryAsync(this.rootDir);
2074
+ filesChecked = currentFiles.length;
2075
+ trackedFiles = this.queries.getAllFiles();
2076
+ }
2185
2077
  const currentSet = new Set(currentFiles);
2186
- const trackedFiles = this.queries.getAllFiles();
2187
2078
  const trackedMap = new Map();
2188
2079
  for (const f of trackedFiles) {
2189
2080
  trackedMap.set(f.path, f);
2190
2081
  }
2191
- // Removals: tracked in the DB but no longer a present source file. Check the
2192
- // filesystem directly — `scanDirectory` (via `git ls-files`) still lists a
2193
- // file deleted from disk but not yet staged, so set membership alone misses it.
2194
- // `reconcileChecks` drives the cooperative yield shared with the adds/mods loop
2195
- // below (see SYNC_RECONCILE_YIELD_INTERVAL / issue #905).
2082
+ const removedArkAnalyzerFiles = [];
2196
2083
  let reconcileChecks = 0;
2197
2084
  for (const tracked of trackedFiles) {
2198
2085
  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).
2086
+ if (tracked.path.startsWith('@')) {
2087
+ continue;
2088
+ }
2201
2089
  const incoming = this.queries.getCrossFileIncomingEdgesWithTarget(tracked.path);
2202
2090
  if (incoming.length > 0) {
2203
2091
  const resurrected = incoming
@@ -2207,6 +2095,9 @@ class ExtractionOrchestrator {
2207
2095
  this.queries.insertUnresolvedRefsBatch(resurrected);
2208
2096
  }
2209
2097
  }
2098
+ if ((0, arkts_1.isArkAnalyzerSourcePath)(tracked.path)) {
2099
+ removedArkAnalyzerFiles.push(tracked.path);
2100
+ }
2210
2101
  this.queries.deleteFile(tracked.path);
2211
2102
  filesRemoved++;
2212
2103
  }
@@ -2216,9 +2107,6 @@ class ExtractionOrchestrator {
2216
2107
  }
2217
2108
  // Adds / modifications.
2218
2109
  for (const filePath of currentFiles) {
2219
- // Same cooperative yield as the removals loop — this is the other O(files)
2220
- // synchronous-stat loop that wedges the main thread on a large repo (#905).
2221
- // Yield at the top of the body so the `continue` fast-paths below still hit it.
2222
2110
  if (++reconcileChecks % SYNC_RECONCILE_YIELD_INTERVAL === 0) {
2223
2111
  onProgress?.({
2224
2112
  phase: 'scanning',
@@ -2281,7 +2169,19 @@ class ExtractionOrchestrator {
2281
2169
  const total = filesToIndex.length;
2282
2170
  const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
2283
2171
  const needsArkTS = filesToIndex.some((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts');
2284
- if (needsArkTS) {
2172
+ const arktsChangedForBatch = [
2173
+ ...removedArkAnalyzerFiles,
2174
+ ...filesToIndex.filter((f) => (0, arkts_1.isArkAnalyzerSourcePath)(f) ||
2175
+ f.endsWith('module.json5') ||
2176
+ f.endsWith('oh-package.json5') ||
2177
+ f === 'build-profile.json5' ||
2178
+ f.endsWith('/build-profile.json5')),
2179
+ ];
2180
+ // Removals alone can still dirty a Harmony module (RTA for remaining files).
2181
+ const needsArkTSBatch = needsArkTS ||
2182
+ removedArkAnalyzerFiles.length > 0 ||
2183
+ arktsChangedForBatch.some((f) => f === 'build-profile.json5' || f.endsWith('/build-profile.json5'));
2184
+ if (needsArkTSBatch && arktsChangedForBatch.length > 0) {
2285
2185
  (0, context_1.setArkTSBatchProgressCallback)((batchProgress) => {
2286
2186
  onProgress?.({
2287
2187
  phase: 'arkts-batch',
@@ -2291,8 +2191,12 @@ class ExtractionOrchestrator {
2291
2191
  subphase: batchProgress.subphase,
2292
2192
  });
2293
2193
  });
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);
2194
+ const firstEts = filesToIndex.find((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides) === 'arkts') ??
2195
+ arktsChangedForBatch.find((f) => (0, arkts_1.isArkAnalyzerSourcePath)(f)) ??
2196
+ arktsChangedForBatch[0];
2197
+ await (0, arkts_1.primeArkTSBatch)(this.rootDir, this.queries, firstEts, {
2198
+ changedFiles: arktsChangedForBatch,
2199
+ });
2296
2200
  }
2297
2201
  try {
2298
2202
  for (let i = 0; i < filesToIndex.length; i++) {
@@ -2317,7 +2221,7 @@ class ExtractionOrchestrator {
2317
2221
  }
2318
2222
  }
2319
2223
  finally {
2320
- if (needsArkTS) {
2224
+ if (needsArkTSBatch) {
2321
2225
  (0, context_1.setArkTSBatchProgressCallback)(null);
2322
2226
  }
2323
2227
  }