@quolu/lattice 0.69.5 → 0.71.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 (605) hide show
  1. package/README.ja.md +5 -1
  2. package/package.json +1 -1
  3. package/sensor/dist/bin/lattice-sensor.js +746 -323
  4. package/sensor/dist/bin/viewer-gate.d.ts +17 -0
  5. package/sensor/dist/bin/viewer-gate.d.ts.map +1 -0
  6. package/sensor/dist/bin/viewer-gate.js +31 -0
  7. package/sensor/dist/bin/viewer-gate.js.map +1 -0
  8. package/sensor/dist/context/formatter.d.ts +7 -1
  9. package/sensor/dist/context/formatter.d.ts.map +1 -1
  10. package/sensor/dist/context/formatter.js +12 -6
  11. package/sensor/dist/context/formatter.js.map +1 -1
  12. package/sensor/dist/context/index.d.ts +12 -0
  13. package/sensor/dist/context/index.d.ts.map +1 -1
  14. package/sensor/dist/context/index.js +103 -16
  15. package/sensor/dist/context/index.js.map +1 -1
  16. package/sensor/dist/db/index.d.ts +23 -2
  17. package/sensor/dist/db/index.d.ts.map +1 -1
  18. package/sensor/dist/db/index.js +109 -31
  19. package/sensor/dist/db/index.js.map +1 -1
  20. package/sensor/dist/db/migrations.d.ts +1 -1
  21. package/sensor/dist/db/migrations.d.ts.map +1 -1
  22. package/sensor/dist/db/migrations.js +68 -1
  23. package/sensor/dist/db/migrations.js.map +1 -1
  24. package/sensor/dist/db/queries.d.ts +583 -6
  25. package/sensor/dist/db/queries.d.ts.map +1 -1
  26. package/sensor/dist/db/queries.js +1425 -57
  27. package/sensor/dist/db/queries.js.map +1 -1
  28. package/sensor/dist/db/schema.sql +27 -4
  29. package/sensor/dist/db/sqlite-adapter.d.ts +2 -0
  30. package/sensor/dist/db/sqlite-adapter.d.ts.map +1 -1
  31. package/sensor/dist/db/sqlite-adapter.js +56 -7
  32. package/sensor/dist/db/sqlite-adapter.js.map +1 -1
  33. package/sensor/dist/db/synthesis-stage.d.ts +12 -0
  34. package/sensor/dist/db/synthesis-stage.d.ts.map +1 -0
  35. package/sensor/dist/db/synthesis-stage.js +93 -0
  36. package/sensor/dist/db/synthesis-stage.js.map +1 -0
  37. package/sensor/dist/db/wal-valve.d.ts +32 -12
  38. package/sensor/dist/db/wal-valve.d.ts.map +1 -1
  39. package/sensor/dist/db/wal-valve.js +65 -22
  40. package/sensor/dist/db/wal-valve.js.map +1 -1
  41. package/sensor/dist/db/wsl-shared-index.d.ts +53 -0
  42. package/sensor/dist/db/wsl-shared-index.d.ts.map +1 -0
  43. package/sensor/dist/db/wsl-shared-index.js +121 -0
  44. package/sensor/dist/db/wsl-shared-index.js.map +1 -0
  45. package/sensor/dist/directory.d.ts +126 -2
  46. package/sensor/dist/directory.d.ts.map +1 -1
  47. package/sensor/dist/directory.js +285 -16
  48. package/sensor/dist/directory.js.map +1 -1
  49. package/sensor/dist/errors.d.ts +14 -0
  50. package/sensor/dist/errors.d.ts.map +1 -1
  51. package/sensor/dist/errors.js +16 -1
  52. package/sensor/dist/errors.js.map +1 -1
  53. package/sensor/dist/extraction/cfml-extractor.d.ts +1 -0
  54. package/sensor/dist/extraction/cfml-extractor.d.ts.map +1 -1
  55. package/sensor/dist/extraction/cfml-extractor.js +15 -3
  56. package/sensor/dist/extraction/cfml-extractor.js.map +1 -1
  57. package/sensor/dist/extraction/dfm-extractor.d.ts.map +1 -1
  58. package/sensor/dist/extraction/dfm-extractor.js +14 -10
  59. package/sensor/dist/extraction/dfm-extractor.js.map +1 -1
  60. package/sensor/dist/extraction/extraction-version.d.ts +1 -1
  61. package/sensor/dist/extraction/extraction-version.d.ts.map +1 -1
  62. package/sensor/dist/extraction/extraction-version.js +3 -1
  63. package/sensor/dist/extraction/extraction-version.js.map +1 -1
  64. package/sensor/dist/extraction/function-ref.d.ts +6 -4
  65. package/sensor/dist/extraction/function-ref.d.ts.map +1 -1
  66. package/sensor/dist/extraction/function-ref.js +35 -14
  67. package/sensor/dist/extraction/function-ref.js.map +1 -1
  68. package/sensor/dist/extraction/generated-detection.d.ts +48 -12
  69. package/sensor/dist/extraction/generated-detection.d.ts.map +1 -1
  70. package/sensor/dist/extraction/generated-detection.js +176 -11
  71. package/sensor/dist/extraction/generated-detection.js.map +1 -1
  72. package/sensor/dist/extraction/grammars.d.ts +38 -0
  73. package/sensor/dist/extraction/grammars.d.ts.map +1 -1
  74. package/sensor/dist/extraction/grammars.js +115 -3
  75. package/sensor/dist/extraction/grammars.js.map +1 -1
  76. package/sensor/dist/extraction/index.d.ts +218 -14
  77. package/sensor/dist/extraction/index.d.ts.map +1 -1
  78. package/sensor/dist/extraction/index.js +887 -175
  79. package/sensor/dist/extraction/index.js.map +1 -1
  80. package/sensor/dist/extraction/languages/c-cpp.d.ts +58 -0
  81. package/sensor/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  82. package/sensor/dist/extraction/languages/c-cpp.js +310 -19
  83. package/sensor/dist/extraction/languages/c-cpp.js.map +1 -1
  84. package/sensor/dist/extraction/languages/dart.d.ts.map +1 -1
  85. package/sensor/dist/extraction/languages/dart.js +8 -3
  86. package/sensor/dist/extraction/languages/dart.js.map +1 -1
  87. package/sensor/dist/extraction/languages/erlang.d.ts.map +1 -1
  88. package/sensor/dist/extraction/languages/erlang.js +58 -22
  89. package/sensor/dist/extraction/languages/erlang.js.map +1 -1
  90. package/sensor/dist/extraction/languages/javascript.js +1 -1
  91. package/sensor/dist/extraction/languages/javascript.js.map +1 -1
  92. package/sensor/dist/extraction/languages/kotlin.d.ts.map +1 -1
  93. package/sensor/dist/extraction/languages/kotlin.js +169 -36
  94. package/sensor/dist/extraction/languages/kotlin.js.map +1 -1
  95. package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
  96. package/sensor/dist/extraction/languages/lua.js +4 -1
  97. package/sensor/dist/extraction/languages/lua.js.map +1 -1
  98. package/sensor/dist/extraction/languages/objc.d.ts.map +1 -1
  99. package/sensor/dist/extraction/languages/objc.js +4 -0
  100. package/sensor/dist/extraction/languages/objc.js.map +1 -1
  101. package/sensor/dist/extraction/languages/python.d.ts.map +1 -1
  102. package/sensor/dist/extraction/languages/python.js +71 -0
  103. package/sensor/dist/extraction/languages/python.js.map +1 -1
  104. package/sensor/dist/extraction/languages/rust.d.ts +20 -0
  105. package/sensor/dist/extraction/languages/rust.d.ts.map +1 -1
  106. package/sensor/dist/extraction/languages/rust.js +50 -20
  107. package/sensor/dist/extraction/languages/rust.js.map +1 -1
  108. package/sensor/dist/extraction/languages/scala.d.ts.map +1 -1
  109. package/sensor/dist/extraction/languages/scala.js +18 -3
  110. package/sensor/dist/extraction/languages/scala.js.map +1 -1
  111. package/sensor/dist/extraction/languages/swift.d.ts +15 -0
  112. package/sensor/dist/extraction/languages/swift.d.ts.map +1 -1
  113. package/sensor/dist/extraction/languages/swift.js +50 -0
  114. package/sensor/dist/extraction/languages/swift.js.map +1 -1
  115. package/sensor/dist/extraction/languages/typescript.d.ts.map +1 -1
  116. package/sensor/dist/extraction/languages/typescript.js +9 -2
  117. package/sensor/dist/extraction/languages/typescript.js.map +1 -1
  118. package/sensor/dist/extraction/liquid-extractor.d.ts +18 -0
  119. package/sensor/dist/extraction/liquid-extractor.d.ts.map +1 -1
  120. package/sensor/dist/extraction/liquid-extractor.js +98 -22
  121. package/sensor/dist/extraction/liquid-extractor.js.map +1 -1
  122. package/sensor/dist/extraction/mybatis-extractor.d.ts.map +1 -1
  123. package/sensor/dist/extraction/mybatis-extractor.js +6 -4
  124. package/sensor/dist/extraction/mybatis-extractor.js.map +1 -1
  125. package/sensor/dist/extraction/parse-pool.d.ts +7 -0
  126. package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
  127. package/sensor/dist/extraction/parse-pool.js +20 -1
  128. package/sensor/dist/extraction/parse-pool.js.map +1 -1
  129. package/sensor/dist/extraction/syntax-tokens.d.ts +103 -0
  130. package/sensor/dist/extraction/syntax-tokens.d.ts.map +1 -0
  131. package/sensor/dist/extraction/syntax-tokens.js +381 -0
  132. package/sensor/dist/extraction/syntax-tokens.js.map +1 -0
  133. package/sensor/dist/extraction/tree-sitter-helpers.d.ts +5 -0
  134. package/sensor/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  135. package/sensor/dist/extraction/tree-sitter-helpers.js +17 -0
  136. package/sensor/dist/extraction/tree-sitter-helpers.js.map +1 -1
  137. package/sensor/dist/extraction/tree-sitter-types.d.ts +31 -3
  138. package/sensor/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  139. package/sensor/dist/extraction/tree-sitter.d.ts +101 -18
  140. package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
  141. package/sensor/dist/extraction/tree-sitter.js +928 -134
  142. package/sensor/dist/extraction/tree-sitter.js.map +1 -1
  143. package/sensor/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  144. package/sensor/dist/file-limits.d.ts +40 -0
  145. package/sensor/dist/file-limits.d.ts.map +1 -0
  146. package/sensor/dist/file-limits.js +147 -0
  147. package/sensor/dist/file-limits.js.map +1 -0
  148. package/sensor/dist/graph/branch-guards.d.ts +265 -0
  149. package/sensor/dist/graph/branch-guards.d.ts.map +1 -0
  150. package/sensor/dist/graph/branch-guards.js +2325 -0
  151. package/sensor/dist/graph/branch-guards.js.map +1 -0
  152. package/sensor/dist/graph/dead-code.d.ts +251 -0
  153. package/sensor/dist/graph/dead-code.d.ts.map +1 -0
  154. package/sensor/dist/graph/dead-code.js +776 -0
  155. package/sensor/dist/graph/dead-code.js.map +1 -0
  156. package/sensor/dist/graph/dynamic-boundary-report.d.ts +121 -0
  157. package/sensor/dist/graph/dynamic-boundary-report.d.ts.map +1 -0
  158. package/sensor/dist/graph/dynamic-boundary-report.js +283 -0
  159. package/sensor/dist/graph/dynamic-boundary-report.js.map +1 -0
  160. package/sensor/dist/graph/index.d.ts +4 -0
  161. package/sensor/dist/graph/index.d.ts.map +1 -1
  162. package/sensor/dist/graph/index.js +16 -1
  163. package/sensor/dist/graph/index.js.map +1 -1
  164. package/sensor/dist/graph/named-symbol-flow.d.ts +125 -0
  165. package/sensor/dist/graph/named-symbol-flow.d.ts.map +1 -0
  166. package/sensor/dist/graph/named-symbol-flow.js +552 -0
  167. package/sensor/dist/graph/named-symbol-flow.js.map +1 -0
  168. package/sensor/dist/graph/queries.d.ts.map +1 -1
  169. package/sensor/dist/graph/queries.js +2 -0
  170. package/sensor/dist/graph/queries.js.map +1 -1
  171. package/sensor/dist/graph/symbol-lookup.d.ts +83 -0
  172. package/sensor/dist/graph/symbol-lookup.d.ts.map +1 -0
  173. package/sensor/dist/graph/symbol-lookup.js +181 -0
  174. package/sensor/dist/graph/symbol-lookup.js.map +1 -0
  175. package/sensor/dist/graph/traversal.d.ts +9 -0
  176. package/sensor/dist/graph/traversal.d.ts.map +1 -1
  177. package/sensor/dist/graph/traversal.js +62 -36
  178. package/sensor/dist/graph/traversal.js.map +1 -1
  179. package/sensor/dist/graph/type-hierarchy.d.ts +155 -0
  180. package/sensor/dist/graph/type-hierarchy.d.ts.map +1 -0
  181. package/sensor/dist/graph/type-hierarchy.js +382 -0
  182. package/sensor/dist/graph/type-hierarchy.js.map +1 -0
  183. package/sensor/dist/index.d.ts +272 -10
  184. package/sensor/dist/index.d.ts.map +1 -1
  185. package/sensor/dist/index.js +510 -34
  186. package/sensor/dist/index.js.map +1 -1
  187. package/sensor/dist/mcp/answer-freshness.d.ts +10 -0
  188. package/sensor/dist/mcp/answer-freshness.d.ts.map +1 -0
  189. package/sensor/dist/mcp/answer-freshness.js +68 -0
  190. package/sensor/dist/mcp/answer-freshness.js.map +1 -0
  191. package/sensor/dist/mcp/daemon-manager.d.ts +1 -1
  192. package/sensor/dist/mcp/daemon-manager.d.ts.map +1 -1
  193. package/sensor/dist/mcp/daemon-manager.js +17 -1
  194. package/sensor/dist/mcp/daemon-manager.js.map +1 -1
  195. package/sensor/dist/mcp/daemon-paths.d.ts +8 -0
  196. package/sensor/dist/mcp/daemon-paths.d.ts.map +1 -1
  197. package/sensor/dist/mcp/daemon-paths.js +65 -2
  198. package/sensor/dist/mcp/daemon-paths.js.map +1 -1
  199. package/sensor/dist/mcp/daemon-registry.d.ts +17 -4
  200. package/sensor/dist/mcp/daemon-registry.d.ts.map +1 -1
  201. package/sensor/dist/mcp/daemon-registry.js +133 -23
  202. package/sensor/dist/mcp/daemon-registry.js.map +1 -1
  203. package/sensor/dist/mcp/daemon.d.ts +26 -13
  204. package/sensor/dist/mcp/daemon.d.ts.map +1 -1
  205. package/sensor/dist/mcp/daemon.js +126 -32
  206. package/sensor/dist/mcp/daemon.js.map +1 -1
  207. package/sensor/dist/mcp/dynamic-boundaries.d.ts +1 -10
  208. package/sensor/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
  209. package/sensor/dist/mcp/dynamic-boundaries.js +46 -42
  210. package/sensor/dist/mcp/dynamic-boundaries.js.map +1 -1
  211. package/sensor/dist/mcp/engine.d.ts +31 -9
  212. package/sensor/dist/mcp/engine.d.ts.map +1 -1
  213. package/sensor/dist/mcp/engine.js +190 -79
  214. package/sensor/dist/mcp/engine.js.map +1 -1
  215. package/sensor/dist/mcp/explore-dedup.d.ts +140 -0
  216. package/sensor/dist/mcp/explore-dedup.d.ts.map +1 -0
  217. package/sensor/dist/mcp/explore-dedup.js +239 -0
  218. package/sensor/dist/mcp/explore-dedup.js.map +1 -0
  219. package/sensor/dist/mcp/explore-diagnostics.d.ts +289 -0
  220. package/sensor/dist/mcp/explore-diagnostics.d.ts.map +1 -0
  221. package/sensor/dist/mcp/explore-diagnostics.js +558 -0
  222. package/sensor/dist/mcp/explore-diagnostics.js.map +1 -0
  223. package/sensor/dist/mcp/explore-session-state.d.ts +217 -0
  224. package/sensor/dist/mcp/explore-session-state.d.ts.map +1 -0
  225. package/sensor/dist/mcp/explore-session-state.js +322 -0
  226. package/sensor/dist/mcp/explore-session-state.js.map +1 -0
  227. package/sensor/dist/mcp/index-freshness-worker.d.ts +2 -0
  228. package/sensor/dist/mcp/index-freshness-worker.d.ts.map +1 -0
  229. package/sensor/dist/mcp/index-freshness-worker.js +30 -0
  230. package/sensor/dist/mcp/index-freshness-worker.js.map +1 -0
  231. package/sensor/dist/mcp/index-freshness.d.ts +7 -0
  232. package/sensor/dist/mcp/index-freshness.d.ts.map +1 -0
  233. package/sensor/dist/mcp/index-freshness.js +94 -0
  234. package/sensor/dist/mcp/index-freshness.js.map +1 -0
  235. package/sensor/dist/mcp/index.d.ts +3 -1
  236. package/sensor/dist/mcp/index.d.ts.map +1 -1
  237. package/sensor/dist/mcp/index.js +109 -12
  238. package/sensor/dist/mcp/index.js.map +1 -1
  239. package/sensor/dist/mcp/liveness-watchdog.d.ts +4 -1
  240. package/sensor/dist/mcp/liveness-watchdog.d.ts.map +1 -1
  241. package/sensor/dist/mcp/liveness-watchdog.js +11 -2
  242. package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
  243. package/sensor/dist/mcp/project-lifecycle.d.ts +9 -0
  244. package/sensor/dist/mcp/project-lifecycle.d.ts.map +1 -0
  245. package/sensor/dist/mcp/project-lifecycle.js +182 -0
  246. package/sensor/dist/mcp/project-lifecycle.js.map +1 -0
  247. package/sensor/dist/mcp/proxy.d.ts.map +1 -1
  248. package/sensor/dist/mcp/proxy.js +15 -1
  249. package/sensor/dist/mcp/proxy.js.map +1 -1
  250. package/sensor/dist/mcp/query-pool.d.ts +2 -2
  251. package/sensor/dist/mcp/query-pool.d.ts.map +1 -1
  252. package/sensor/dist/mcp/query-pool.js +9 -7
  253. package/sensor/dist/mcp/query-pool.js.map +1 -1
  254. package/sensor/dist/mcp/query-worker.js +2 -2
  255. package/sensor/dist/mcp/query-worker.js.map +1 -1
  256. package/sensor/dist/mcp/server-instructions.d.ts +2 -2
  257. package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
  258. package/sensor/dist/mcp/server-instructions.js +13 -6
  259. package/sensor/dist/mcp/server-instructions.js.map +1 -1
  260. package/sensor/dist/mcp/session.d.ts +16 -0
  261. package/sensor/dist/mcp/session.d.ts.map +1 -1
  262. package/sensor/dist/mcp/session.js +36 -13
  263. package/sensor/dist/mcp/session.js.map +1 -1
  264. package/sensor/dist/mcp/startup-handshake.d.ts +3 -1
  265. package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
  266. package/sensor/dist/mcp/startup-handshake.js +10 -2
  267. package/sensor/dist/mcp/startup-handshake.js.map +1 -1
  268. package/sensor/dist/mcp/tools.d.ts +496 -38
  269. package/sensor/dist/mcp/tools.d.ts.map +1 -1
  270. package/sensor/dist/mcp/tools.js +4021 -772
  271. package/sensor/dist/mcp/tools.js.map +1 -1
  272. package/sensor/dist/mcp/writer-lock.d.ts +61 -0
  273. package/sensor/dist/mcp/writer-lock.d.ts.map +1 -0
  274. package/sensor/dist/mcp/writer-lock.js +260 -0
  275. package/sensor/dist/mcp/writer-lock.js.map +1 -0
  276. package/sensor/dist/project-config.d.ts +23 -0
  277. package/sensor/dist/project-config.d.ts.map +1 -1
  278. package/sensor/dist/project-config.js +39 -2
  279. package/sensor/dist/project-config.js.map +1 -1
  280. package/sensor/dist/resolution/alias-binding.d.ts +56 -0
  281. package/sensor/dist/resolution/alias-binding.d.ts.map +1 -0
  282. package/sensor/dist/resolution/alias-binding.js +127 -0
  283. package/sensor/dist/resolution/alias-binding.js.map +1 -0
  284. package/sensor/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  285. package/sensor/dist/resolution/c-fnptr-synthesizer.js +21 -19
  286. package/sensor/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  287. package/sensor/dist/resolution/callback-synthesizer.d.ts +23 -1
  288. package/sensor/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  289. package/sensor/dist/resolution/callback-synthesizer.js +356 -155
  290. package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
  291. package/sensor/dist/resolution/cpp-constructor.d.ts +4 -0
  292. package/sensor/dist/resolution/cpp-constructor.d.ts.map +1 -0
  293. package/sensor/dist/resolution/cpp-constructor.js +123 -0
  294. package/sensor/dist/resolution/cpp-constructor.js.map +1 -0
  295. package/sensor/dist/resolution/cpp-macro-visibility.d.ts +12 -0
  296. package/sensor/dist/resolution/cpp-macro-visibility.d.ts.map +1 -0
  297. package/sensor/dist/resolution/cpp-macro-visibility.js +342 -0
  298. package/sensor/dist/resolution/cpp-macro-visibility.js.map +1 -0
  299. package/sensor/dist/resolution/expo-router-synthesizer.d.ts +32 -0
  300. package/sensor/dist/resolution/expo-router-synthesizer.d.ts.map +1 -0
  301. package/sensor/dist/resolution/expo-router-synthesizer.js +189 -0
  302. package/sensor/dist/resolution/expo-router-synthesizer.js.map +1 -0
  303. package/sensor/dist/resolution/frameworks/csharp.d.ts.map +1 -1
  304. package/sensor/dist/resolution/frameworks/csharp.js +96 -2
  305. package/sensor/dist/resolution/frameworks/csharp.js.map +1 -1
  306. package/sensor/dist/resolution/frameworks/expo-router.d.ts +189 -0
  307. package/sensor/dist/resolution/frameworks/expo-router.d.ts.map +1 -0
  308. package/sensor/dist/resolution/frameworks/expo-router.js +785 -0
  309. package/sensor/dist/resolution/frameworks/expo-router.js.map +1 -0
  310. package/sensor/dist/resolution/frameworks/express.d.ts.map +1 -1
  311. package/sensor/dist/resolution/frameworks/express.js +251 -16
  312. package/sensor/dist/resolution/frameworks/express.js.map +1 -1
  313. package/sensor/dist/resolution/frameworks/index.d.ts +5 -0
  314. package/sensor/dist/resolution/frameworks/index.d.ts.map +1 -1
  315. package/sensor/dist/resolution/frameworks/index.js +29 -1
  316. package/sensor/dist/resolution/frameworks/index.js.map +1 -1
  317. package/sensor/dist/resolution/frameworks/java.d.ts.map +1 -1
  318. package/sensor/dist/resolution/frameworks/java.js +79 -47
  319. package/sensor/dist/resolution/frameworks/java.js.map +1 -1
  320. package/sensor/dist/resolution/frameworks/laravel.d.ts.map +1 -1
  321. package/sensor/dist/resolution/frameworks/laravel.js +294 -4
  322. package/sensor/dist/resolution/frameworks/laravel.js.map +1 -1
  323. package/sensor/dist/resolution/frameworks/nestjs.d.ts.map +1 -1
  324. package/sensor/dist/resolution/frameworks/nestjs.js +6 -14
  325. package/sensor/dist/resolution/frameworks/nestjs.js.map +1 -1
  326. package/sensor/dist/resolution/frameworks/nextjs.d.ts +76 -0
  327. package/sensor/dist/resolution/frameworks/nextjs.d.ts.map +1 -0
  328. package/sensor/dist/resolution/frameworks/nextjs.js +328 -0
  329. package/sensor/dist/resolution/frameworks/nextjs.js.map +1 -0
  330. package/sensor/dist/resolution/frameworks/object-literal.d.ts +32 -0
  331. package/sensor/dist/resolution/frameworks/object-literal.d.ts.map +1 -0
  332. package/sensor/dist/resolution/frameworks/object-literal.js +139 -0
  333. package/sensor/dist/resolution/frameworks/object-literal.js.map +1 -0
  334. package/sensor/dist/resolution/frameworks/package-deps.d.ts +14 -0
  335. package/sensor/dist/resolution/frameworks/package-deps.d.ts.map +1 -0
  336. package/sensor/dist/resolution/frameworks/package-deps.js +78 -0
  337. package/sensor/dist/resolution/frameworks/package-deps.js.map +1 -0
  338. package/sensor/dist/resolution/frameworks/python.d.ts.map +1 -1
  339. package/sensor/dist/resolution/frameworks/python.js +166 -0
  340. package/sensor/dist/resolution/frameworks/python.js.map +1 -1
  341. package/sensor/dist/resolution/frameworks/react-native.d.ts +26 -0
  342. package/sensor/dist/resolution/frameworks/react-native.d.ts.map +1 -1
  343. package/sensor/dist/resolution/frameworks/react-native.js +153 -14
  344. package/sensor/dist/resolution/frameworks/react-native.js.map +1 -1
  345. package/sensor/dist/resolution/frameworks/react-router.d.ts +46 -0
  346. package/sensor/dist/resolution/frameworks/react-router.d.ts.map +1 -0
  347. package/sensor/dist/resolution/frameworks/react-router.js +187 -0
  348. package/sensor/dist/resolution/frameworks/react-router.js.map +1 -0
  349. package/sensor/dist/resolution/frameworks/react.d.ts +2 -1
  350. package/sensor/dist/resolution/frameworks/react.d.ts.map +1 -1
  351. package/sensor/dist/resolution/frameworks/react.js +188 -139
  352. package/sensor/dist/resolution/frameworks/react.js.map +1 -1
  353. package/sensor/dist/resolution/frameworks/svelte.d.ts.map +1 -1
  354. package/sensor/dist/resolution/frameworks/svelte.js +6 -1
  355. package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
  356. package/sensor/dist/resolution/frameworks/sveltekit-router.d.ts +36 -0
  357. package/sensor/dist/resolution/frameworks/sveltekit-router.d.ts.map +1 -0
  358. package/sensor/dist/resolution/frameworks/sveltekit-router.js +156 -0
  359. package/sensor/dist/resolution/frameworks/sveltekit-router.js.map +1 -0
  360. package/sensor/dist/resolution/frameworks/swift.d.ts.map +1 -1
  361. package/sensor/dist/resolution/frameworks/swift.js +140 -99
  362. package/sensor/dist/resolution/frameworks/swift.js.map +1 -1
  363. package/sensor/dist/resolution/frameworks/tanstack-router.d.ts +84 -0
  364. package/sensor/dist/resolution/frameworks/tanstack-router.d.ts.map +1 -0
  365. package/sensor/dist/resolution/frameworks/tanstack-router.js +437 -0
  366. package/sensor/dist/resolution/frameworks/tanstack-router.js.map +1 -0
  367. package/sensor/dist/resolution/frameworks/vue-router.d.ts +82 -0
  368. package/sensor/dist/resolution/frameworks/vue-router.d.ts.map +1 -0
  369. package/sensor/dist/resolution/frameworks/vue-router.js +377 -0
  370. package/sensor/dist/resolution/frameworks/vue-router.js.map +1 -0
  371. package/sensor/dist/resolution/import-resolver.d.ts +38 -0
  372. package/sensor/dist/resolution/import-resolver.d.ts.map +1 -1
  373. package/sensor/dist/resolution/import-resolver.js +394 -28
  374. package/sensor/dist/resolution/import-resolver.js.map +1 -1
  375. package/sensor/dist/resolution/index.d.ts +89 -25
  376. package/sensor/dist/resolution/index.d.ts.map +1 -1
  377. package/sensor/dist/resolution/index.js +573 -148
  378. package/sensor/dist/resolution/index.js.map +1 -1
  379. package/sensor/dist/resolution/js-builtins.d.ts +13 -0
  380. package/sensor/dist/resolution/js-builtins.d.ts.map +1 -0
  381. package/sensor/dist/resolution/js-builtins.js +46 -0
  382. package/sensor/dist/resolution/js-builtins.js.map +1 -0
  383. package/sensor/dist/resolution/name-matcher.d.ts +99 -22
  384. package/sensor/dist/resolution/name-matcher.d.ts.map +1 -1
  385. package/sensor/dist/resolution/name-matcher.js +2220 -86
  386. package/sensor/dist/resolution/name-matcher.js.map +1 -1
  387. package/sensor/dist/resolution/next-router-synthesizer.d.ts +27 -0
  388. package/sensor/dist/resolution/next-router-synthesizer.d.ts.map +1 -0
  389. package/sensor/dist/resolution/next-router-synthesizer.js +113 -0
  390. package/sensor/dist/resolution/next-router-synthesizer.js.map +1 -0
  391. package/sensor/dist/resolution/path-aliases.d.ts +6 -4
  392. package/sensor/dist/resolution/path-aliases.d.ts.map +1 -1
  393. package/sensor/dist/resolution/path-aliases.js +125 -17
  394. package/sensor/dist/resolution/path-aliases.js.map +1 -1
  395. package/sensor/dist/resolution/react-router-synthesizer.d.ts +33 -0
  396. package/sensor/dist/resolution/react-router-synthesizer.d.ts.map +1 -0
  397. package/sensor/dist/resolution/react-router-synthesizer.js +126 -0
  398. package/sensor/dist/resolution/react-router-synthesizer.js.map +1 -0
  399. package/sensor/dist/resolution/resolver-pool.d.ts +2 -0
  400. package/sensor/dist/resolution/resolver-pool.d.ts.map +1 -1
  401. package/sensor/dist/resolution/resolver-pool.js +4 -0
  402. package/sensor/dist/resolution/resolver-pool.js.map +1 -1
  403. package/sensor/dist/resolution/strip-comments.d.ts +6 -0
  404. package/sensor/dist/resolution/strip-comments.d.ts.map +1 -1
  405. package/sensor/dist/resolution/strip-comments.js +58 -0
  406. package/sensor/dist/resolution/strip-comments.js.map +1 -1
  407. package/sensor/dist/resolution/sveltekit-synthesizer.d.ts +58 -0
  408. package/sensor/dist/resolution/sveltekit-synthesizer.d.ts.map +1 -0
  409. package/sensor/dist/resolution/sveltekit-synthesizer.js +173 -0
  410. package/sensor/dist/resolution/sveltekit-synthesizer.js.map +1 -0
  411. package/sensor/dist/resolution/swift-type-visibility.d.ts +52 -0
  412. package/sensor/dist/resolution/swift-type-visibility.d.ts.map +1 -0
  413. package/sensor/dist/resolution/swift-type-visibility.js +328 -0
  414. package/sensor/dist/resolution/swift-type-visibility.js.map +1 -0
  415. package/sensor/dist/resolution/synth-utils.d.ts +26 -0
  416. package/sensor/dist/resolution/synth-utils.d.ts.map +1 -0
  417. package/sensor/dist/resolution/synth-utils.js +75 -0
  418. package/sensor/dist/resolution/synth-utils.js.map +1 -0
  419. package/sensor/dist/resolution/tanstack-router-synthesizer.d.ts +31 -0
  420. package/sensor/dist/resolution/tanstack-router-synthesizer.d.ts.map +1 -0
  421. package/sensor/dist/resolution/tanstack-router-synthesizer.js +114 -0
  422. package/sensor/dist/resolution/tanstack-router-synthesizer.js.map +1 -0
  423. package/sensor/dist/resolution/tier-synthesizer.d.ts +54 -0
  424. package/sensor/dist/resolution/tier-synthesizer.d.ts.map +1 -0
  425. package/sensor/dist/resolution/tier-synthesizer.js +1294 -0
  426. package/sensor/dist/resolution/tier-synthesizer.js.map +1 -0
  427. package/sensor/dist/resolution/types.d.ts +78 -2
  428. package/sensor/dist/resolution/types.d.ts.map +1 -1
  429. package/sensor/dist/resolution/types.js +64 -0
  430. package/sensor/dist/resolution/types.js.map +1 -1
  431. package/sensor/dist/resolution/vue-router-synthesizer.d.ts +30 -0
  432. package/sensor/dist/resolution/vue-router-synthesizer.d.ts.map +1 -0
  433. package/sensor/dist/resolution/vue-router-synthesizer.js +115 -0
  434. package/sensor/dist/resolution/vue-router-synthesizer.js.map +1 -0
  435. package/sensor/dist/search/identifier-segments.d.ts +10 -0
  436. package/sensor/dist/search/identifier-segments.d.ts.map +1 -1
  437. package/sensor/dist/search/identifier-segments.js +29 -0
  438. package/sensor/dist/search/identifier-segments.js.map +1 -1
  439. package/sensor/dist/search/query-paths.d.ts +106 -0
  440. package/sensor/dist/search/query-paths.d.ts.map +1 -0
  441. package/sensor/dist/search/query-paths.js +513 -0
  442. package/sensor/dist/search/query-paths.js.map +1 -0
  443. package/sensor/dist/search/query-utils.d.ts +22 -2
  444. package/sensor/dist/search/query-utils.d.ts.map +1 -1
  445. package/sensor/dist/search/query-utils.js +56 -16
  446. package/sensor/dist/search/query-utils.js.map +1 -1
  447. package/sensor/dist/sync/watch-policy.d.ts +14 -0
  448. package/sensor/dist/sync/watch-policy.d.ts.map +1 -1
  449. package/sensor/dist/sync/watch-policy.js +19 -0
  450. package/sensor/dist/sync/watch-policy.js.map +1 -1
  451. package/sensor/dist/sync/watcher.d.ts +74 -35
  452. package/sensor/dist/sync/watcher.d.ts.map +1 -1
  453. package/sensor/dist/sync/watcher.js +281 -99
  454. package/sensor/dist/sync/watcher.js.map +1 -1
  455. package/sensor/dist/telemetry/index.d.ts +4 -2
  456. package/sensor/dist/telemetry/index.d.ts.map +1 -1
  457. package/sensor/dist/telemetry/index.js +92 -41
  458. package/sensor/dist/telemetry/index.js.map +1 -1
  459. package/sensor/dist/types.d.ts +18 -2
  460. package/sensor/dist/types.d.ts.map +1 -1
  461. package/sensor/dist/types.js +2 -0
  462. package/sensor/dist/types.js.map +1 -1
  463. package/sensor/dist/ui-server/api/deadcode.d.ts +84 -0
  464. package/sensor/dist/ui-server/api/deadcode.d.ts.map +1 -0
  465. package/sensor/dist/ui-server/api/deadcode.js +136 -0
  466. package/sensor/dist/ui-server/api/deadcode.js.map +1 -0
  467. package/sensor/dist/ui-server/api/effects.d.ts +94 -0
  468. package/sensor/dist/ui-server/api/effects.d.ts.map +1 -0
  469. package/sensor/dist/ui-server/api/effects.js +527 -0
  470. package/sensor/dist/ui-server/api/effects.js.map +1 -0
  471. package/sensor/dist/ui-server/api/entrypoints.d.ts +92 -0
  472. package/sensor/dist/ui-server/api/entrypoints.d.ts.map +1 -0
  473. package/sensor/dist/ui-server/api/entrypoints.js +361 -0
  474. package/sensor/dist/ui-server/api/entrypoints.js.map +1 -0
  475. package/sensor/dist/ui-server/api/events.d.ts +183 -0
  476. package/sensor/dist/ui-server/api/events.d.ts.map +1 -0
  477. package/sensor/dist/ui-server/api/events.js +430 -0
  478. package/sensor/dist/ui-server/api/events.js.map +1 -0
  479. package/sensor/dist/ui-server/api/file.d.ts +66 -0
  480. package/sensor/dist/ui-server/api/file.d.ts.map +1 -0
  481. package/sensor/dist/ui-server/api/file.js +235 -0
  482. package/sensor/dist/ui-server/api/file.js.map +1 -0
  483. package/sensor/dist/ui-server/api/filecode.d.ts +126 -0
  484. package/sensor/dist/ui-server/api/filecode.d.ts.map +1 -0
  485. package/sensor/dist/ui-server/api/filecode.js +205 -0
  486. package/sensor/dist/ui-server/api/filecode.js.map +1 -0
  487. package/sensor/dist/ui-server/api/flow.d.ts +226 -0
  488. package/sensor/dist/ui-server/api/flow.d.ts.map +1 -0
  489. package/sensor/dist/ui-server/api/flow.js +612 -0
  490. package/sensor/dist/ui-server/api/flow.js.map +1 -0
  491. package/sensor/dist/ui-server/api/hierarchy.d.ts +81 -0
  492. package/sensor/dist/ui-server/api/hierarchy.d.ts.map +1 -0
  493. package/sensor/dist/ui-server/api/hierarchy.js +92 -0
  494. package/sensor/dist/ui-server/api/hierarchy.js.map +1 -0
  495. package/sensor/dist/ui-server/api/index.d.ts +90 -0
  496. package/sensor/dist/ui-server/api/index.d.ts.map +1 -0
  497. package/sensor/dist/ui-server/api/index.js +332 -0
  498. package/sensor/dist/ui-server/api/index.js.map +1 -0
  499. package/sensor/dist/ui-server/api/map.d.ts +256 -0
  500. package/sensor/dist/ui-server/api/map.d.ts.map +1 -0
  501. package/sensor/dist/ui-server/api/map.js +760 -0
  502. package/sensor/dist/ui-server/api/map.js.map +1 -0
  503. package/sensor/dist/ui-server/api/node.d.ts +81 -0
  504. package/sensor/dist/ui-server/api/node.d.ts.map +1 -0
  505. package/sensor/dist/ui-server/api/node.js +388 -0
  506. package/sensor/dist/ui-server/api/node.js.map +1 -0
  507. package/sensor/dist/ui-server/api/nodes.d.ts +25 -0
  508. package/sensor/dist/ui-server/api/nodes.d.ts.map +1 -0
  509. package/sensor/dist/ui-server/api/nodes.js +45 -0
  510. package/sensor/dist/ui-server/api/nodes.js.map +1 -0
  511. package/sensor/dist/ui-server/api/program.d.ts +139 -0
  512. package/sensor/dist/ui-server/api/program.d.ts.map +1 -0
  513. package/sensor/dist/ui-server/api/program.js +315 -0
  514. package/sensor/dist/ui-server/api/program.js.map +1 -0
  515. package/sensor/dist/ui-server/api/respond.d.ts +76 -0
  516. package/sensor/dist/ui-server/api/respond.d.ts.map +1 -0
  517. package/sensor/dist/ui-server/api/respond.js +186 -0
  518. package/sensor/dist/ui-server/api/respond.js.map +1 -0
  519. package/sensor/dist/ui-server/api/route-roots.d.ts +39 -0
  520. package/sensor/dist/ui-server/api/route-roots.d.ts.map +1 -0
  521. package/sensor/dist/ui-server/api/route-roots.js +96 -0
  522. package/sensor/dist/ui-server/api/route-roots.js.map +1 -0
  523. package/sensor/dist/ui-server/api/routes.d.ts +59 -0
  524. package/sensor/dist/ui-server/api/routes.d.ts.map +1 -0
  525. package/sensor/dist/ui-server/api/routes.js +108 -0
  526. package/sensor/dist/ui-server/api/routes.js.map +1 -0
  527. package/sensor/dist/ui-server/api/screens.d.ts +117 -0
  528. package/sensor/dist/ui-server/api/screens.d.ts.map +1 -0
  529. package/sensor/dist/ui-server/api/screens.js +611 -0
  530. package/sensor/dist/ui-server/api/screens.js.map +1 -0
  531. package/sensor/dist/ui-server/api/search.d.ts +29 -0
  532. package/sensor/dist/ui-server/api/search.d.ts.map +1 -0
  533. package/sensor/dist/ui-server/api/search.js +232 -0
  534. package/sensor/dist/ui-server/api/search.js.map +1 -0
  535. package/sensor/dist/ui-server/api/session.d.ts +52 -0
  536. package/sensor/dist/ui-server/api/session.d.ts.map +1 -0
  537. package/sensor/dist/ui-server/api/session.js +196 -0
  538. package/sensor/dist/ui-server/api/session.js.map +1 -0
  539. package/sensor/dist/ui-server/api/source.d.ts +200 -0
  540. package/sensor/dist/ui-server/api/source.d.ts.map +1 -0
  541. package/sensor/dist/ui-server/api/source.js +422 -0
  542. package/sensor/dist/ui-server/api/source.js.map +1 -0
  543. package/sensor/dist/ui-server/api/stats.d.ts +27 -0
  544. package/sensor/dist/ui-server/api/stats.d.ts.map +1 -0
  545. package/sensor/dist/ui-server/api/stats.js +164 -0
  546. package/sensor/dist/ui-server/api/stats.js.map +1 -0
  547. package/sensor/dist/ui-server/api/steps.d.ts +274 -0
  548. package/sensor/dist/ui-server/api/steps.d.ts.map +1 -0
  549. package/sensor/dist/ui-server/api/steps.js +1544 -0
  550. package/sensor/dist/ui-server/api/steps.js.map +1 -0
  551. package/sensor/dist/ui-server/api/trail-store.d.ts +142 -0
  552. package/sensor/dist/ui-server/api/trail-store.d.ts.map +1 -0
  553. package/sensor/dist/ui-server/api/trail-store.js +325 -0
  554. package/sensor/dist/ui-server/api/trail-store.js.map +1 -0
  555. package/sensor/dist/ui-server/api/trails.d.ts +163 -0
  556. package/sensor/dist/ui-server/api/trails.d.ts.map +1 -0
  557. package/sensor/dist/ui-server/api/trails.js +369 -0
  558. package/sensor/dist/ui-server/api/trails.js.map +1 -0
  559. package/sensor/dist/ui-server/api/when.d.ts +133 -0
  560. package/sensor/dist/ui-server/api/when.d.ts.map +1 -0
  561. package/sensor/dist/ui-server/api/when.js +275 -0
  562. package/sensor/dist/ui-server/api/when.js.map +1 -0
  563. package/sensor/dist/ui-server/api/wire.d.ts +179 -0
  564. package/sensor/dist/ui-server/api/wire.d.ts.map +1 -0
  565. package/sensor/dist/ui-server/api/wire.js +231 -0
  566. package/sensor/dist/ui-server/api/wire.js.map +1 -0
  567. package/sensor/dist/ui-server/assets.d.ts +40 -0
  568. package/sensor/dist/ui-server/assets.d.ts.map +1 -0
  569. package/sensor/dist/ui-server/assets.js +110 -0
  570. package/sensor/dist/ui-server/assets.js.map +1 -0
  571. package/sensor/dist/ui-server/constants.d.ts +32 -0
  572. package/sensor/dist/ui-server/constants.d.ts.map +1 -0
  573. package/sensor/dist/ui-server/constants.js +35 -0
  574. package/sensor/dist/ui-server/constants.js.map +1 -0
  575. package/sensor/dist/ui-server/highlight/index.d.ts +113 -0
  576. package/sensor/dist/ui-server/highlight/index.d.ts.map +1 -0
  577. package/sensor/dist/ui-server/highlight/index.js +307 -0
  578. package/sensor/dist/ui-server/highlight/index.js.map +1 -0
  579. package/sensor/dist/ui-server/highlight/languages.d.ts +36 -0
  580. package/sensor/dist/ui-server/highlight/languages.d.ts.map +1 -0
  581. package/sensor/dist/ui-server/highlight/languages.js +50 -0
  582. package/sensor/dist/ui-server/highlight/languages.js.map +1 -0
  583. package/sensor/dist/ui-server/index.d.ts +92 -0
  584. package/sensor/dist/ui-server/index.d.ts.map +1 -0
  585. package/sensor/dist/ui-server/index.js +399 -0
  586. package/sensor/dist/ui-server/index.js.map +1 -0
  587. package/sensor/dist/ui-server/open-browser.d.ts +31 -0
  588. package/sensor/dist/ui-server/open-browser.d.ts.map +1 -0
  589. package/sensor/dist/ui-server/open-browser.js +79 -0
  590. package/sensor/dist/ui-server/open-browser.js.map +1 -0
  591. package/sensor/dist/ui-server/security.d.ts +127 -0
  592. package/sensor/dist/ui-server/security.d.ts.map +1 -0
  593. package/sensor/dist/ui-server/security.js +332 -0
  594. package/sensor/dist/ui-server/security.js.map +1 -0
  595. package/sensor/dist/ui-server/static.d.ts +45 -0
  596. package/sensor/dist/ui-server/static.d.ts.map +1 -0
  597. package/sensor/dist/ui-server/static.js +165 -0
  598. package/sensor/dist/ui-server/static.js.map +1 -0
  599. package/sensor/dist/utils.d.ts +14 -2
  600. package/sensor/dist/utils.d.ts.map +1 -1
  601. package/sensor/dist/utils.js +25 -10
  602. package/sensor/dist/utils.js.map +1 -1
  603. package/src/cli-help.mjs +4 -2
  604. package/src/sensor-adapter.mjs +5 -1
  605. package/src/todo-cli.mjs +88 -11
@@ -41,12 +41,16 @@ 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.INDEXED_AT_COMMIT_KEY = exports.ScopeIgnore = exports.oversizeStamp = void 0;
45
45
  exports.hashContent = hashContent;
46
46
  exports.buildDefaultIgnore = buildDefaultIgnore;
47
+ exports.preloadLanguagesForFiles = preloadLanguagesForFiles;
47
48
  exports.buildScopeIgnore = buildScopeIgnore;
48
49
  exports.discoverEmbeddedRepoRoots = discoverEmbeddedRepoRoots;
49
50
  exports.findUnindexedIgnoredRepos = findUnindexedIgnoredRepos;
51
+ exports.getGitChangedFiles = getGitChangedFiles;
52
+ exports.getGitHeadSha = getGitHeadSha;
53
+ exports.canTrustGitFastPath = canTrustGitFastPath;
50
54
  exports.scanDirectory = scanDirectory;
51
55
  exports.scanDirectoryAsync = scanDirectoryAsync;
52
56
  const fs = __importStar(require("fs"));
@@ -60,6 +64,7 @@ const tree_sitter_1 = require("./tree-sitter");
60
64
  const parse_pool_1 = require("./parse-pool");
61
65
  const store_writer_1 = require("./store-writer");
62
66
  const kernel_1 = require("./kernel");
67
+ const generated_detection_1 = require("./generated-detection");
63
68
  const grammars_1 = require("./grammars");
64
69
  const project_config_1 = require("../project-config");
65
70
  const directory_1 = require("../directory");
@@ -68,6 +73,8 @@ const utils_1 = require("../utils");
68
73
  const ignore_1 = __importDefault(require("ignore"));
69
74
  const frameworks_1 = require("../resolution/frameworks");
70
75
  const cooperative_yield_1 = require("../resolution/cooperative-yield");
76
+ const file_limits_1 = require("../file-limits");
77
+ Object.defineProperty(exports, "oversizeStamp", { enumerable: true, get: function () { return file_limits_1.oversizeStamp; } });
71
78
  /**
72
79
  * Number of files to read in parallel during indexing.
73
80
  * File reads are I/O-bound; batching overlaps I/O wait with CPU parse work.
@@ -107,11 +114,26 @@ function hashContent(content) {
107
114
  return crypto.createHash('sha256').update(content).digest('hex');
108
115
  }
109
116
  /**
110
- * Skip files larger than this (bytes). Generated bundles, minified JS, and
111
- * vendored blobs blow the WASM heap and the worker-recycle budget for no useful
112
- * symbols. 1 MB covers essentially all hand-written source.
117
+ * What change detection hashes for a file: its text when it is under the size
118
+ * limit, the size stamp when it is over — an oversize file is never decoded.
119
+ * `null` when the bytes show a `.ts` file is an MPEG transport stream, not
120
+ * TypeScript (#1910): decided from the bytes already read, so it costs no I/O
121
+ * and runs only for a file that is new or changed — never once per `.ts` file
122
+ * at discovery, which on a slow disk is a random read per file per scan.
113
123
  */
114
- const MAX_FILE_SIZE = 1024 * 1024;
124
+ function readSourceOrStamp(fullPath) {
125
+ const { stats, bytes } = (0, file_limits_1.readBoundedSourceSync)(fullPath);
126
+ if (bytes === null)
127
+ return (0, file_limits_1.oversizeStamp)(stats.size);
128
+ return isMpegTsBytes(fullPath, bytes) ? null : bytes.toString('utf8');
129
+ }
130
+ /** Whether these bytes, read from `filePath`, are a `.ts` MPEG transport stream (#1910). */
131
+ function isMpegTsBytes(filePath, bytes) {
132
+ if (!(0, grammars_1.hasMpegTsExtension)(filePath) || !(0, grammars_1.isMpegTransportStream)(bytes.subarray(0, grammars_1.MPEG_TS_SNIFF_BYTES)))
133
+ return false;
134
+ (0, errors_1.logDebug)('Skipping MPEG transport stream named .ts — not TypeScript', { filePath });
135
+ return true;
136
+ }
115
137
  /**
116
138
  * Directory names that are dependency, build, cache, or tooling output across the
117
139
  * languages/frameworks LatticeSensor supports — curated from the canonical
@@ -191,6 +213,13 @@ const DEFAULT_IGNORE_PATTERNS = [
191
213
  'bazel-*/', // Bazel output symlink trees
192
214
  // Android resource dirs at any depth, with their qualifier variants (#1047).
193
215
  ...ANDROID_RES_TYPES.map((t) => `**/res/${t}*/`),
216
+ // `build` is also a legal JVM package segment. Keep it under conventional
217
+ // source roots (any source set: main, test, androidTest, ...) while continuing
218
+ // to exclude module/build output (#1642). Unignore only the directory, not its
219
+ // subtree: other defaults still apply.
220
+ '!**/src/*/java/**/build/',
221
+ '!**/src/*/kotlin/**/build/',
222
+ '!**/src/*/scala/**/build/',
194
223
  ];
195
224
  /** True if `buf` decodes as strict UTF-8 (no invalid byte sequences). */
196
225
  function isValidUtf8(buf) {
@@ -260,18 +289,119 @@ function readGitignorePatterns(giPath) {
260
289
  }
261
290
  return kept.join('\n');
262
291
  }
292
+ /**
293
+ * Resolve the repository GIT_DIR for `repoRoot` (a `.git` directory, or the
294
+ * target of a `.git` file pointer). Null when this isn't a git checkout.
295
+ */
296
+ function resolveGitDir(repoRoot) {
297
+ const gitPath = path.join(repoRoot, '.git');
298
+ let st;
299
+ try {
300
+ st = fs.statSync(gitPath);
301
+ }
302
+ catch {
303
+ return null;
304
+ }
305
+ if (st.isDirectory())
306
+ return gitPath;
307
+ if (!st.isFile())
308
+ return null;
309
+ try {
310
+ const raw = fs.readFileSync(gitPath, 'utf8').match(/^gitdir:\s*(.+)$/m)?.[1]?.trim();
311
+ if (!raw)
312
+ return null;
313
+ return path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(repoRoot, raw);
314
+ }
315
+ catch {
316
+ return null;
317
+ }
318
+ }
319
+ /** Expand a leading `~/` the way git does for `core.excludesFile`. */
320
+ function expandUserPath(p) {
321
+ if (p === '~')
322
+ return os.homedir();
323
+ if (p.startsWith('~/'))
324
+ return path.join(os.homedir(), p.slice(2));
325
+ return p;
326
+ }
327
+ /**
328
+ * Root-relative exclude patterns from git sources that are NOT the root
329
+ * `.gitignore`: `.git/info/exclude` and `core.excludesFile`. Same semantics as
330
+ * the root `.gitignore`, so they merge into {@link buildDefaultIgnore}. Without
331
+ * these, the watcher / FS-walk scope silently diverged from
332
+ * `git ls-files --exclude-standard` (#1728).
333
+ */
334
+ function readGitExcludeExtraPatterns(rootDir) {
335
+ const chunks = [];
336
+ const gitDir = resolveGitDir(rootDir);
337
+ if (gitDir) {
338
+ const excludePath = path.join(gitDir, 'info', 'exclude');
339
+ if (fs.existsSync(excludePath)) {
340
+ const patterns = readGitignorePatterns(excludePath);
341
+ if (patterns)
342
+ chunks.push(patterns);
343
+ }
344
+ }
345
+ try {
346
+ const configured = (0, child_process_1.execFileSync)('git', ['-C', rootDir, 'config', '--get', 'core.excludesFile'], { encoding: 'utf8', timeout: 5_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
347
+ if (configured) {
348
+ const abs = expandUserPath(configured);
349
+ if (fs.existsSync(abs)) {
350
+ const patterns = readGitignorePatterns(abs);
351
+ if (patterns)
352
+ chunks.push(patterns);
353
+ }
354
+ }
355
+ }
356
+ catch {
357
+ // No git, unset, or timeout — leave extras empty.
358
+ }
359
+ return chunks.join('\n');
360
+ }
361
+ /**
362
+ * Directories `git ls-files -o -i --exclude-standard --directory` reports as
363
+ * ignored-untracked. Seeded into {@link ScopeIgnore} so nested `.gitignore`
364
+ * effects (and any exclude-standard rule the flat matcher might miss) prune the
365
+ * watcher the same way the indexer skips them (#1728).
366
+ */
367
+ function listGitIgnoredDirectories(rootDir) {
368
+ try {
369
+ const out = (0, child_process_1.execFileSync)('git', ['-C', rootDir, 'ls-files', '-z', '-o', '-i', '--exclude-standard', '--directory'], {
370
+ encoding: 'utf8',
371
+ timeout: 60_000,
372
+ maxBuffer: 50 * 1024 * 1024,
373
+ stdio: ['ignore', 'pipe', 'ignore'],
374
+ });
375
+ const dirs = [];
376
+ for (const entry of out.split('\0')) {
377
+ if (!entry)
378
+ continue;
379
+ dirs.push(entry.endsWith('/') ? entry : `${entry}/`);
380
+ }
381
+ return dirs;
382
+ }
383
+ catch {
384
+ return [];
385
+ }
386
+ }
263
387
  /**
264
388
  * An `ignore` matcher seeded with the built-in defaults, merged with the project's
265
- * root .gitignore so a negation there (e.g. `!vendor/`) overrides a default. Shared
266
- * by both enumeration paths so behavior is identical with or without git — and so
267
- * the defaults apply to tracked files too (committing a dependency dir doesn't make
268
- * it project code; the explicit `.gitignore` negation is the only opt-in).
389
+ * root .gitignore so a negation there (e.g. `!vendor/`) overrides a default, plus
390
+ * git's other root-relative exclude files (`.git/info/exclude`, `core.excludesFile`)
391
+ * so watcher / FS-walk scope matches `git ls-files --exclude-standard` (#1728).
392
+ * Shared by both enumeration paths so behavior is identical with or without git —
393
+ * and so the defaults apply to tracked files too (committing a dependency dir
394
+ * doesn't make it project code; the explicit `.gitignore` negation is the only
395
+ * opt-in).
269
396
  */
270
397
  function buildDefaultIgnore(rootDir) {
271
398
  const ig = (0, ignore_1.default)().add(DEFAULT_IGNORE_PATTERNS);
272
399
  const rootGitignore = path.join(rootDir, '.gitignore');
273
400
  if (fs.existsSync(rootGitignore))
274
401
  ig.add(readGitignorePatterns(rootGitignore));
402
+ const extra = readGitExcludeExtraPatterns(rootDir);
403
+ if (extra)
404
+ ig.add(extra);
275
405
  return ig;
276
406
  }
277
407
  /**
@@ -590,15 +720,38 @@ function findNestedGitRepos(absDir, relPrefix) {
590
720
  }
591
721
  /**
592
722
  * Workspace-scope ignore matcher. Ordinary paths get the root's matcher
593
- * (built-in defaults + root `.gitignore`); paths inside an EMBEDDED repo get
594
- * that repo's own matcher (defaults + its root `.gitignore`) — the parent's
595
- * `.gitignore` hides a child repo from git, not from the index (#514). A
596
- * directory path (trailing slash) that is an ANCESTOR of an embedded root is
597
- * never ignored, so directory-pruning callers (the Linux per-directory
598
- * watcher) still descend to reach the embedded repos.
723
+ * (built-in defaults + root `.gitignore` + `.git/info/exclude` +
724
+ * `core.excludesFile`, plus directories `git ls-files --exclude-standard`
725
+ * reports as ignored); paths inside an EMBEDDED repo get that repo's own
726
+ * matcher — the parent's `.gitignore` hides a child repo from git, not from
727
+ * the index (#514). A directory path (trailing slash) that is an ANCESTOR of
728
+ * an embedded root is never ignored, so directory-pruning callers (the Linux
729
+ * per-directory watcher) still descend to reach the embedded repos.
599
730
  *
600
- * Single source of truth for indexer and watcher scope — they must not diverge.
731
+ * Shared by the indexer (scoped sync / skip checks) and the watcher so their
732
+ * scope cannot diverge from each other or from `git ls-files --exclude-standard`
733
+ * (#1728).
601
734
  */
735
+ /**
736
+ * The grammars to preload for a file set.
737
+ *
738
+ * Path-only detection calls every `.h` file C, but parse-time detection reads
739
+ * the source and can reclassify it as C++ or Objective-C (`detectLanguage`
740
+ * with a `source` argument). Workers only ever get the grammars named here, so
741
+ * a header that turns out to be Objective-C in a project with no `.m` file
742
+ * found no parser and failed with `Failed to get parser for language: objc`
743
+ * (#1628). C++ was already covered; Objective-C was not.
744
+ */
745
+ function preloadLanguagesForFiles(files, overrides) {
746
+ const languages = [...new Set(files.map((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides)))];
747
+ if (languages.includes('c')) {
748
+ for (const ambiguous of ['cpp', 'objc']) {
749
+ if (!languages.includes(ambiguous))
750
+ languages.push(ambiguous);
751
+ }
752
+ }
753
+ return languages;
754
+ }
602
755
  class ScopeIgnore {
603
756
  rootMatcher;
604
757
  exclude;
@@ -677,7 +830,14 @@ exports.ScopeIgnore = ScopeIgnore;
677
830
  function buildScopeIgnore(rootDir, embeddedRoots) {
678
831
  const roots = embeddedRoots ? [...embeddedRoots] : discoverEmbeddedRepoRoots(rootDir);
679
832
  const include = loadIncludeMatcher(rootDir);
680
- 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)) : []);
833
+ // Root matcher already has defaults + root `.gitignore` + info/exclude +
834
+ // core.excludesFile. Seed ignored-untracked directories from git so nested
835
+ // `.gitignore` effects prune the watcher identically to the indexer (#1728).
836
+ const rootMatcher = buildDefaultIgnore(rootDir);
837
+ for (const dir of listGitIgnoredDirectories(rootDir)) {
838
+ rootMatcher.add(dir);
839
+ }
840
+ return new ScopeIgnore(rootMatcher, roots.map((root) => ({ root, matcher: buildDefaultIgnore(path.join(rootDir, root)) })), loadExcludeMatcher(rootDir), include, include ? includeStaticRoots((0, project_config_1.loadIncludePatterns)(rootDir)) : []);
681
841
  }
682
842
  /**
683
843
  * Whether an embedded repo found as a tracked gitlink (mode 160000, #1031/#1033)
@@ -744,7 +904,7 @@ function discoverEmbeddedRepoRoots(rootDir) {
744
904
  // same way collectGitFiles does, keeping watcher scope == indexer scope.
745
905
  // (#1031, #1033)
746
906
  try {
747
- const staged = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], { cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
907
+ const staged = lsFilesStaged({ cwd: repoAbs, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
748
908
  const repoIgnore = buildDefaultIgnore(repoAbs);
749
909
  for (const entry of staged.split('\0')) {
750
910
  if (!entry || entry.slice(0, 6) !== '160000')
@@ -864,6 +1024,30 @@ function findIgnoredEmbeddedRepos(repoDir, includeIgnored, prefix) {
864
1024
  }
865
1025
  return repos;
866
1026
  }
1027
+ /**
1028
+ * `git ls-files -z -s`, expanding submodules where git allows it.
1029
+ *
1030
+ * `--recurse-submodules` could not be combined with `-s` before git 2.36:
1031
+ * `builtin/ls-files.c` listed `show_stage` among the modes that die, and the
1032
+ * check is unconditional — it does not look at whether the repo actually has
1033
+ * submodules, so every call fails on older git. Ubuntu 22.04 LTS (2.34.1) and
1034
+ * Debian 11 (2.30.2) are both below that line.
1035
+ *
1036
+ * Letting the throw escape cost far more than submodule expansion: it unwound
1037
+ * the whole git-visible pass, so `includeIgnored`, gitlink recursion and the
1038
+ * `lattice-sensor.json` include allowlist silently stopped applying and files went
1039
+ * missing from the index with no error (#1549). Retry without the flag instead
1040
+ * — `-s` is the part that matters here, since gitlink detection reads the mode
1041
+ * bits, and embedded repos are reached through the gitlink recursion anyway.
1042
+ */
1043
+ function lsFilesStaged(gitOpts) {
1044
+ try {
1045
+ return (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], gitOpts);
1046
+ }
1047
+ catch {
1048
+ return (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s'], gitOpts);
1049
+ }
1050
+ }
867
1051
  /**
868
1052
  * Collect git-visible files (tracked + untracked, .gitignore-respected) from the
869
1053
  * git repository rooted at `repoDir`, adding each to `files` with `prefix`
@@ -908,7 +1092,12 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
908
1092
  // on disk → those files are silently dropped from the index. (#541) With -s the
909
1093
  // path follows a TAB after the `<mode> <object> <stage>` prefix.
910
1094
  const gitlinkRels = [];
911
- const tracked = (0, child_process_1.execFileSync)('git', ['ls-files', '-z', '-s', '--recurse-submodules'], gitOpts);
1095
+ // Lattice owns `.lattice/` (its ToDo store, run worktrees, adapter registry):
1096
+ // the FS walks and the watcher never descend into it (isLatticeStateDir), and
1097
+ // git's listing must agree — an untracked script written there by a run is not
1098
+ // project source, and counting it made status report the index stale mid-run.
1099
+ const inLatticeState = (rel) => rel.split('/').some((part) => (0, directory_1.isLatticeStateDir)(part));
1100
+ const tracked = lsFilesStaged(gitOpts);
912
1101
  for (const entry of tracked.split('\0')) {
913
1102
  if (!entry)
914
1103
  continue;
@@ -916,6 +1105,8 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
916
1105
  if (tab === -1)
917
1106
  continue; // --stage always emits "<mode> <object> <stage>\t<path>"
918
1107
  const rel = entry.slice(tab + 1);
1108
+ if (inLatticeState(rel))
1109
+ continue;
919
1110
  if (entry.slice(0, 6) === '160000') {
920
1111
  gitlinkRels.push(rel); // an unexpanded gitlink — recursed into below, not a source file itself
921
1112
  continue;
@@ -929,6 +1120,8 @@ function collectGitFiles(repoDir, prefix, files, embeddedRoots, includeIgnored =
929
1120
  for (const rel of untracked.split('\0')) {
930
1121
  if (!rel)
931
1122
  continue;
1123
+ if (inLatticeState(rel))
1124
+ continue;
932
1125
  if (rel.endsWith('/')) {
933
1126
  // git only emits a trailing-slash directory entry for an embedded repo.
934
1127
  // Guard with a .git check anyway, and skip anything else exactly as git
@@ -999,6 +1192,10 @@ function getGitVisibleFiles(rootDir) {
999
1192
  // git check-ignore exits 0 if the path IS ignored, 1 if not
1000
1193
  (0, child_process_1.execFileSync)('git', ['check-ignore', '-q', path.resolve(rootDir)], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
1001
1194
  // Directory is gitignored by parent repo — fall back to filesystem walk
1195
+ (0, errors_1.logDebug)('project root is gitignored by a parent repo — falling back to filesystem walk', {
1196
+ rootDir,
1197
+ gitRoot,
1198
+ });
1002
1199
  return null;
1003
1200
  }
1004
1201
  catch {
@@ -1025,7 +1222,20 @@ function getGitVisibleFiles(rootDir) {
1025
1222
  visible.add(f);
1026
1223
  return visible;
1027
1224
  }
1028
- catch {
1225
+ catch (error) {
1226
+ // Any failure here (git missing, a `git rev-parse`/`ls-files` timeout or
1227
+ // buffer overrun under load, an unreadable repo, unsupported flag combo on
1228
+ // older git, etc.) silently sent every caller to `scanDirectoryWalk` with
1229
+ // zero signal that the fast git-delegated path was skipped — making reports
1230
+ // like #1567 (nested-`.gitignore`-excluded `node_modules` walked into)
1231
+ // hard to triage, since both ignore implementations look correct in
1232
+ // isolation but there was no way to tell which one ran. Log it under the
1233
+ // existing LATTICE_SENSOR_DEBUG gate so a future report can confirm or rule out
1234
+ // the fallback in one step.
1235
+ (0, errors_1.logDebug)('git-based file listing unavailable — falling back to filesystem walk', {
1236
+ rootDir,
1237
+ error: error instanceof Error ? error.message : String(error),
1238
+ });
1029
1239
  return null;
1030
1240
  }
1031
1241
  }
@@ -1043,21 +1253,102 @@ function getGitVisibleFiles(rootDir) {
1043
1253
  * case this cannot see (the child status that would report the deletions is gone
1044
1254
  * with it); a full `lattice sensor index` reconciles that.
1045
1255
  */
1046
- function getGitChangedFiles(rootDir) {
1256
+ function getGitChangedFiles(rootDir, sinceCommit) {
1047
1257
  try {
1258
+ // `git status` only ever describes the WORKING TREE, so a change that has
1259
+ // been committed leaves no entry and never enters the candidate set — the
1260
+ // hash comparison in getChangedFiles is correct but is never reached for
1261
+ // it, and `pendingChanges` reads 0 while the index is genuinely behind
1262
+ // (#1829). `sinceCommit` — the commit the index was last brought up to
1263
+ // date at — adds the other half: what has been committed since. Callers
1264
+ // that hold no such stamp still get exactly what they always did, the
1265
+ // working-tree changes.
1048
1266
  const changes = { modified: [], added: [], deleted: [] };
1049
1267
  // Custom extension → language overrides from the project's lattice-sensor.json,
1050
1268
  // so change detection sees the same custom-extension files the full index does.
1051
1269
  const overrides = (0, project_config_1.loadExtensionOverrides)(rootDir);
1052
- collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir));
1270
+ collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir), sinceCommit ?? undefined);
1053
1271
  return changes;
1054
1272
  }
1055
1273
  catch {
1056
1274
  return null;
1057
1275
  }
1058
1276
  }
1059
- function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null, exclude = null) {
1060
- 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 });
1277
+ /**
1278
+ * Metadata key: the commit the index was last brought up to date at. Written by
1279
+ * a full index AND by every successful sync — unlike the extraction stamp, which
1280
+ * a sync must not advance because it only touches a subset of files. This one is
1281
+ * about the tree; failed file paths remain explicit retry candidates. (#1829)
1282
+ */
1283
+ exports.INDEXED_AT_COMMIT_KEY = 'indexed_at_commit';
1284
+ /** HEAD's commit sha, or null in a non-git repo or one with no commits yet. */
1285
+ function getGitHeadSha(rootDir) {
1286
+ try {
1287
+ return (0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], {
1288
+ cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
1289
+ }).trim() || null;
1290
+ }
1291
+ catch {
1292
+ return null;
1293
+ }
1294
+ }
1295
+ /**
1296
+ * NUL-delimited status/path pairs for every path committed
1297
+ * between `sinceCommit` and HEAD. Empty when the stamp IS HEAD, which is the
1298
+ * common case — one cheap git call on the hot path.
1299
+ */
1300
+ function gitCommittedChangesSince(repoDir, sinceCommit) {
1301
+ // NUL framing preserves Unicode, quotes, tabs and newlines in Git paths.
1302
+ // Let command failures reach getGitChangedFiles: [] would falsely mean clean.
1303
+ const out = (0, child_process_1.execFileSync)('git', ['diff', '--relative', '--name-status', '--no-renames', '-z', sinceCommit, 'HEAD', '--', '.'], {
1304
+ cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
1305
+ });
1306
+ return out.split('\0');
1307
+ }
1308
+ /**
1309
+ * Can an INDEX trust the git fast path, given the commit it was built at?
1310
+ *
1311
+ * False means "fall back to the full scan" — the expensive path that compares
1312
+ * every file on disk against the DB, and the only correct read when git cannot
1313
+ * say what happened between the stamp and now:
1314
+ *
1315
+ * - stamp present but unknown to this repo (rebase, gc, shallow clone, a stamp
1316
+ * from a different checkout) — history moved under the index.
1317
+ * - stamp absent while the repo HAS commits — an index built before stamping
1318
+ * existed. One full scan; the next sync stamps it and the fast path returns.
1319
+ *
1320
+ * A repo with NO commits keeps the fast path with or without a stamp: every
1321
+ * file is untracked, so `git status` already sees all of them. Callers with no
1322
+ * index behind them (the exported `getGitChangedFiles`) never ask this — a
1323
+ * working-tree diff is the whole of what they wanted. (#1829)
1324
+ */
1325
+ function canTrustGitFastPath(rootDir, sinceCommit) {
1326
+ const head = getGitHeadSha(rootDir);
1327
+ if (head == null)
1328
+ return true; // no commits (or not a git repo — caller handles that)
1329
+ if (!sinceCommit)
1330
+ return false;
1331
+ if (sinceCommit === head)
1332
+ return true;
1333
+ try {
1334
+ (0, child_process_1.execFileSync)('git', ['cat-file', '-e', `${sinceCommit}^{commit}`], {
1335
+ cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
1336
+ });
1337
+ return true;
1338
+ }
1339
+ catch {
1340
+ return false;
1341
+ }
1342
+ }
1343
+ function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null, exclude = null, sinceCommit) {
1344
+ const output = (0, child_process_1.execFileSync)('git',
1345
+ // `-uall` lists individual untracked files instead of collapsing an
1346
+ // entirely-untracked directory into one `?? dir/` entry, which would
1347
+ // otherwise be dropped here (only embedded git repos are recursed into
1348
+ // below). Nested untracked git repos still collapse to `?? repo/` even
1349
+ // with `-uall` — git never crosses a repo boundary — so the recursion
1350
+ // still handles them. (#1213)
1351
+ ['status', '--porcelain', '--no-renames', '-z', '-uall'], { cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
1061
1352
  // This repo's own ignore rules — built-in defaults (#407) plus its .gitignore.
1062
1353
  // Change detection must exclude the SAME files the full index does, but git
1063
1354
  // status hides neither: it ignores nothing for *tracked* paths, and the
@@ -1069,45 +1360,68 @@ function collectGitStatus(repoDir, prefix, out, overrides, includeIgnored = null
1069
1360
  // ScopeIgnore: every embedded repo is judged by ITS OWN rules, never the
1070
1361
  // parent's. (#766)
1071
1362
  const ig = buildDefaultIgnore(repoDir);
1072
- const untrackedDirs = [];
1073
- for (const line of output.split('\n')) {
1074
- if (line.length < 4)
1075
- continue; // Minimum: "XY file"
1076
- const statusCode = line.substring(0, 2);
1077
- const rel = (0, utils_1.normalizePath)(line.substring(3));
1078
- // Untracked directory entries (trailing slash) may hide an embedded repo —
1079
- // collect for the recursion below instead of treating as a file.
1080
- if (statusCode === '??' && rel.endsWith('/')) {
1081
- untrackedDirs.push(rel);
1082
- continue;
1083
- }
1363
+ // One classifier for both candidate sources below, so a committed change is
1364
+ // filtered by exactly the rules a working-tree change is (#766, #999, #1829).
1365
+ const classify = (statusCode, rel) => {
1084
1366
  const filePath = (0, utils_1.normalizePath)(prefix + rel);
1085
1367
  if (!(0, grammars_1.isSourceFile)(filePath, overrides))
1086
- continue;
1368
+ return;
1087
1369
  if (statusCode.includes('D')) {
1088
1370
  // Deletions stay unfiltered: getChangedFiles acts on one only when the
1089
1371
  // path is already tracked in the DB, where removal is always correct — and
1090
1372
  // that lets a newly-excluded dir's stale rows clean themselves up. (#766)
1091
1373
  out.deleted.push(filePath);
1092
- continue;
1374
+ return;
1093
1375
  }
1376
+ // Lattice owns `.lattice/`: the full scan and git listing skip it, so a
1377
+ // change there is never pending (a run's untracked scripts would otherwise
1378
+ // read as an added file and mark the index stale).
1379
+ if (filePath.split('/').some((part) => (0, directory_1.isLatticeStateDir)(part)))
1380
+ return;
1094
1381
  // Added (`??`) / modified files inside an excluded dir must not enter the
1095
1382
  // index — match against the repo-relative path, same as the full scan. (#766)
1096
1383
  if (ig.ignores(rel))
1097
- continue;
1384
+ return;
1098
1385
  // User `lattice-sensor.json` `exclude` (#999) is project-root-relative, so it's
1099
1386
  // matched against the full path — sync must not re-add a tracked file the
1100
1387
  // full index now keeps out. Deletions above stay unfiltered so a file that
1101
1388
  // WAS indexed before an exclude was added still cleans itself out.
1102
1389
  if (exclude && exclude.ignores(filePath))
1103
- continue;
1390
+ return;
1104
1391
  if (statusCode === '??') {
1105
1392
  out.added.push(filePath);
1106
1393
  }
1107
1394
  else {
1108
- // M, MM, AM, A (staged), etc. — treat as modified
1395
+ // M, MM, AM, A (staged), etc. — treat as modified. getChangedFiles
1396
+ // re-decides added-vs-modified from the DB, so a committed `A` that the
1397
+ // index never saw still lands in `added`.
1109
1398
  out.modified.push(filePath);
1110
1399
  }
1400
+ };
1401
+ const untrackedDirs = [];
1402
+ for (const line of output.split('\0')) {
1403
+ if (line.length < 4)
1404
+ continue; // Minimum: "XY file"
1405
+ const statusCode = line.substring(0, 2);
1406
+ const rel = (0, utils_1.normalizePath)(line.substring(3));
1407
+ // Untracked directory entries (trailing slash) may hide an embedded repo —
1408
+ // collect for the recursion below instead of treating as a file.
1409
+ if (statusCode === '??' && rel.endsWith('/')) {
1410
+ untrackedDirs.push(rel);
1411
+ continue;
1412
+ }
1413
+ classify(statusCode, rel);
1414
+ }
1415
+ // Committed but unindexed: everything between the commit this index was last
1416
+ // brought up to date at and HEAD. `git status` cannot see these — committing
1417
+ // is precisely what removes a file from its output — so without this pass a
1418
+ // `git commit` makes a real pending change read as zero (#1829). The stamp
1419
+ // belongs to the ROOT repo, so the embedded-repo recursion below passes none.
1420
+ if (sinceCommit) {
1421
+ const fields = gitCommittedChangesSince(repoDir, sinceCommit);
1422
+ for (let i = 0; i + 1 < fields.length; i += 2) {
1423
+ classify(`${fields[i].charAt(0)} `, (0, utils_1.normalizePath)(fields[i + 1]));
1424
+ }
1111
1425
  }
1112
1426
  // Recurse embedded repos found under untracked dirs (at the dir itself or
1113
1427
  // nested deeper). Gitignored dirs are walked only for the directories the
@@ -1149,11 +1463,16 @@ function scanDirectory(rootDir, onProgress) {
1149
1463
  // Fallback: walk filesystem for non-git projects
1150
1464
  return scanDirectoryWalk(rootDir, onProgress);
1151
1465
  }
1152
- /**
1153
- * Async variant of scanDirectory that yields to the event loop periodically,
1154
- * allowing worker threads to receive and render progress messages.
1155
- */
1156
- async function scanDirectoryAsync(rootDir, onProgress) {
1466
+ /** Record one file the scan declined to index. */
1467
+ function tallySkip(stats, rel) {
1468
+ if (!stats)
1469
+ return;
1470
+ const ext = path.extname(rel).toLowerCase();
1471
+ if (!ext)
1472
+ return;
1473
+ stats.unsupportedByExtension.set(ext, (stats.unsupportedByExtension.get(ext) ?? 0) + 1);
1474
+ }
1475
+ async function scanDirectoryAsync(rootDir, onProgress, stats) {
1157
1476
  // Custom extension → language overrides from the project's lattice-sensor.json.
1158
1477
  const overrides = (0, project_config_1.loadExtensionOverrides)(rootDir);
1159
1478
  const gitFiles = getGitVisibleFiles(rootDir);
@@ -1170,15 +1489,18 @@ async function scanDirectoryAsync(rootDir, onProgress) {
1170
1489
  await new Promise(r => setImmediate(r));
1171
1490
  }
1172
1491
  }
1492
+ else {
1493
+ tallySkip(stats, filePath);
1494
+ }
1173
1495
  }
1174
1496
  return files;
1175
1497
  }
1176
- return scanDirectoryWalk(rootDir, onProgress);
1498
+ return scanDirectoryWalk(rootDir, onProgress, stats);
1177
1499
  }
1178
1500
  /**
1179
1501
  * Filesystem walk fallback for non-git projects.
1180
1502
  */
1181
- function scanDirectoryWalk(rootDir, onProgress) {
1503
+ function scanDirectoryWalk(rootDir, onProgress, stats) {
1182
1504
  const files = [];
1183
1505
  let count = 0;
1184
1506
  const visitedDirs = new Set();
@@ -1250,10 +1572,15 @@ function scanDirectoryWalk(rootDir, onProgress) {
1250
1572
  }
1251
1573
  }
1252
1574
  else if (stat.isFile()) {
1253
- if (!isIgnored(fullPath, false, active) && (0, grammars_1.isSourceFile)(relativePath, overrides)) {
1254
- files.push(relativePath);
1255
- count++;
1256
- onProgress?.(count, relativePath);
1575
+ if (!isIgnored(fullPath, false, active)) {
1576
+ if ((0, grammars_1.isSourceFile)(relativePath, overrides)) {
1577
+ files.push(relativePath);
1578
+ count++;
1579
+ onProgress?.(count, relativePath);
1580
+ }
1581
+ else {
1582
+ tallySkip(stats, relativePath);
1583
+ }
1257
1584
  }
1258
1585
  }
1259
1586
  }
@@ -1268,10 +1595,15 @@ function scanDirectoryWalk(rootDir, onProgress) {
1268
1595
  }
1269
1596
  }
1270
1597
  else if (entry.isFile()) {
1271
- if (!isIgnored(fullPath, false, active) && (0, grammars_1.isSourceFile)(relativePath, overrides)) {
1272
- files.push(relativePath);
1273
- count++;
1274
- onProgress?.(count, relativePath);
1598
+ if (!isIgnored(fullPath, false, active)) {
1599
+ if ((0, grammars_1.isSourceFile)(relativePath, overrides)) {
1600
+ files.push(relativePath);
1601
+ count++;
1602
+ onProgress?.(count, relativePath);
1603
+ }
1604
+ else {
1605
+ tallySkip(stats, relativePath);
1606
+ }
1275
1607
  }
1276
1608
  }
1277
1609
  }
@@ -1344,10 +1676,57 @@ class ExtractionOrchestrator {
1344
1676
  * hasn't run yet so single-file re-index paths can detect on the spot.
1345
1677
  */
1346
1678
  detectedFrameworkNames = null;
1679
+ /**
1680
+ * Scope matcher for SCOPED syncs, memoized on the mtimes of the two root
1681
+ * files it is derived from (`lattice-sensor.json`, `.gitignore`). See
1682
+ * {@link scopedSyncMatcher}.
1683
+ */
1684
+ scopedMatcher = null;
1347
1685
  constructor(rootDir, queries) {
1348
1686
  this.rootDir = rootDir;
1349
1687
  this.queries = queries;
1350
1688
  }
1689
+ /**
1690
+ * The scope matcher a scoped sync applies to the paths it was handed — the
1691
+ * same `buildScopeIgnore` the full scan uses, so an explicitly-passed path
1692
+ * that is OUT of scope (a user `exclude` in `lattice-sensor.json`, a `.gitignore`
1693
+ * rule, a built-in default) is treated exactly as the full walk would treat
1694
+ * it: absent, hence removed if tracked, never parsed (#1590).
1695
+ *
1696
+ * Memoized on the root config + root `.gitignore` mtimes: building the
1697
+ * matcher runs embedded-repo discovery (`git ls-files`), which would defeat
1698
+ * the scoped path's whole point (skipping O(repo) work) if paid per sync.
1699
+ * Two `stat`s per sync while nothing changed. An embedded repo created
1700
+ * between config edits joins the scoped matcher on the next full sync, the
1701
+ * same lifecycle the watcher's own matcher already has.
1702
+ */
1703
+ scopedSyncMatcher() {
1704
+ // Bust when any root-level exclude source the matcher reads may have
1705
+ // changed. Nested `.gitignore` edits force a full watcher sync, which
1706
+ // clears this cache (see the full-reconcile branch in sync()).
1707
+ const gitDir = resolveGitDir(this.rootDir);
1708
+ const key = [
1709
+ project_config_1.PROJECT_CONFIG_FILENAME,
1710
+ '.gitignore',
1711
+ gitDir ? path.join(gitDir, 'info', 'exclude') : '',
1712
+ ]
1713
+ .map((name) => {
1714
+ if (!name)
1715
+ return '-';
1716
+ try {
1717
+ return String(fs.statSync(path.isAbsolute(name) ? name : path.join(this.rootDir, name)).mtimeMs);
1718
+ }
1719
+ catch {
1720
+ return '-';
1721
+ }
1722
+ })
1723
+ .join('|');
1724
+ if (this.scopedMatcher && this.scopedMatcher.key === key)
1725
+ return this.scopedMatcher.matcher;
1726
+ const matcher = buildScopeIgnore(this.rootDir);
1727
+ this.scopedMatcher = { key, matcher };
1728
+ return matcher;
1729
+ }
1351
1730
  /**
1352
1731
  * Build a filesystem-backed ResolutionContext sufficient for framework
1353
1732
  * detection. Graph-query methods (getNodesByName etc.) return empty because
@@ -1381,7 +1760,9 @@ class ExtractionOrchestrator {
1381
1760
  if (!full)
1382
1761
  return null;
1383
1762
  try {
1384
- return fs.readFileSync(full, 'utf-8');
1763
+ // Framework detectors scan source by name; a file over the size
1764
+ // limit was never indexed and must not be decoded here either (#1910).
1765
+ return (0, file_limits_1.readBoundedSourceSync)(full).bytes?.toString('utf8') ?? null;
1385
1766
  }
1386
1767
  catch {
1387
1768
  return null;
@@ -1463,6 +1844,7 @@ class ExtractionOrchestrator {
1463
1844
  // early-run 5-10s single stalls were observed on 95k-file repos but never
1464
1845
  // attributed — these labels settle scan vs framework-detect vs grammars.
1465
1846
  const tScan = Date.now();
1847
+ const skipStats = { unsupportedByExtension: new Map() };
1466
1848
  const files = await scanDirectoryAsync(this.rootDir, (current, file) => {
1467
1849
  onProgress?.({
1468
1850
  phase: 'scanning',
@@ -1470,9 +1852,26 @@ class ExtractionOrchestrator {
1470
1852
  total: 0,
1471
1853
  currentFile: file,
1472
1854
  });
1473
- });
1855
+ }, skipStats);
1474
1856
  if (process.env.LATTICE_SENSOR_SYNTH_TIMINGS)
1475
1857
  console.error(`[phase-timing] scan: ${Date.now() - tScan}ms (${files.length} files)`);
1858
+ /** Only meaningful when nothing was indexable — see IndexResult (#1502). */
1859
+ const skipSummary = () => {
1860
+ let total = 0;
1861
+ for (const n of skipStats.unsupportedByExtension.values())
1862
+ total += n;
1863
+ if (total === 0)
1864
+ return {};
1865
+ const top = [...skipStats.unsupportedByExtension.entries()]
1866
+ .map(([ext, count]) => ({ ext, count }))
1867
+ .sort((a, b) => b.count - a.count || a.ext.localeCompare(b.ext))
1868
+ .slice(0, 5);
1869
+ return { filesSkippedUnsupported: total, topUnsupportedExtensions: top };
1870
+ };
1871
+ // A re-index over an existing DB skips unchanged-hash files at the store,
1872
+ // which would preserve wiped zero-node rows (#1541) — drop them first so
1873
+ // this run stores their files fresh. No-op on a fresh DB.
1874
+ this.healZeroNodeRows();
1476
1875
  // Detect frameworks once per indexAll run using the scanned file list.
1477
1876
  // Names are passed to each parse call so framework-specific extractors
1478
1877
  // (route nodes, middleware, etc.) run after the tree-sitter pass.
@@ -1508,11 +1907,7 @@ class ExtractionOrchestrator {
1508
1907
  });
1509
1908
  await new Promise(resolve => setImmediate(resolve));
1510
1909
  // Detect needed languages and load grammars in the parse worker
1511
- const neededLanguages = [...new Set(files.map((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides)))];
1512
- // .h files default to 'c' but may be C++ — ensure cpp grammar is loaded when c is needed
1513
- if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
1514
- neededLanguages.push('cpp');
1515
- }
1910
+ const neededLanguages = preloadLanguagesForFiles(files, overrides);
1516
1911
  // Parse files on a pool of worker threads (keeps the main thread free for UI
1517
1912
  // and uses every core). Falls back to in-process parsing when the compiled
1518
1913
  // worker is unavailable (e.g. running from source in tests).
@@ -1619,29 +2014,27 @@ class ExtractionOrchestrator {
1619
2014
  // Store: on the writer thread when active (fresh DB — bundles applied
1620
2015
  // in the same file order this chain dispatches them), else on the main
1621
2016
  // thread (SQLite connections are per-thread).
1622
- if (nodeCount > 0 || result.errors.length === 0) {
1623
- const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1624
- if (storeWriter) {
1625
- if (result.kernelBuffers) {
1626
- // Buffers go to the writer as-is; the worker decodes + finalizes.
1627
- // The main thread's only per-file work stays O(1) + the content hash.
1628
- storeWriter.send({
1629
- kernel: true,
1630
- filePath,
1631
- language,
1632
- buffers: result.kernelBuffers,
1633
- file: this.buildFileRecord(filePath, content, language, stats, nodeCount, result.errors),
1634
- });
1635
- }
1636
- else {
1637
- storeWriter.send(this.buildFreshStoreBundle(filePath, content, language, stats, result));
1638
- }
1639
- await storeWriter.waitBelow(STORE_WRITER_WINDOW);
2017
+ const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
2018
+ if (storeWriter) {
2019
+ if (result.kernelBuffers) {
2020
+ // Buffers go to the writer as-is; the worker decodes + finalizes.
2021
+ // The main thread's only per-file work stays O(1) + the content hash.
2022
+ storeWriter.send({
2023
+ kernel: true,
2024
+ filePath,
2025
+ language,
2026
+ buffers: result.kernelBuffers,
2027
+ file: this.buildFileRecord(filePath, content, language, stats, nodeCount, result.errors),
2028
+ });
1640
2029
  }
1641
2030
  else {
1642
- const materialized = (0, kernel_1.materializeKernelResult)(result, filePath, language);
1643
- await this.storeExtractionResult(filePath, content, language, stats, materialized, commitYield);
2031
+ storeWriter.send(this.buildFreshStoreBundle(filePath, content, language, stats, result));
1644
2032
  }
2033
+ await storeWriter.waitBelow(STORE_WRITER_WINDOW);
2034
+ }
2035
+ else {
2036
+ const materialized = (0, kernel_1.materializeKernelResult)(result, filePath, language);
2037
+ await this.storeExtractionResult(filePath, content, language, stats, materialized, commitYield);
1645
2038
  }
1646
2039
  if (result.errors.length > 0) {
1647
2040
  for (const err of result.errors) {
@@ -1672,16 +2065,19 @@ class ExtractionOrchestrator {
1672
2065
  }
1673
2066
  onProgress?.({ phase: 'parsing', current: processed, total, currentFile: filePath });
1674
2067
  };
1675
- const recordParseFailure = (filePath, err) => {
1676
- processed++;
1677
- filesErrored++;
1678
- errors.push({
1679
- message: err instanceof Error ? err.message : String(err),
1680
- filePath,
1681
- severity: 'error',
1682
- code: 'parse_error',
2068
+ const recordParseFailure = async (filePath, content, stats, err) => {
2069
+ await storeResult(filePath, content, stats, {
2070
+ nodes: [],
2071
+ edges: [],
2072
+ unresolvedReferences: [],
2073
+ errors: [{
2074
+ message: err instanceof Error ? err.message : String(err),
2075
+ filePath,
2076
+ severity: 'error',
2077
+ code: 'parse_error',
2078
+ }],
2079
+ durationMs: 0,
1683
2080
  });
1684
- onProgress?.({ phase: 'parsing', current: processed, total });
1685
2081
  };
1686
2082
  // Commit buffered parses to the DB in file order, advancing the cursor over
1687
2083
  // contiguous completed results. Runs after each parse settles (and once more
@@ -1706,7 +2102,7 @@ class ExtractionOrchestrator {
1706
2102
  if (item.ok)
1707
2103
  await storeResult(item.filePath, item.content, item.stats, item.result);
1708
2104
  else
1709
- recordParseFailure(item.filePath, item.err);
2105
+ await recordParseFailure(item.filePath, item.content, item.stats, item.err);
1710
2106
  }
1711
2107
  }
1712
2108
  catch (err) {
@@ -1726,7 +2122,7 @@ class ExtractionOrchestrator {
1726
2122
  completed.set(seq, { ok: true, filePath, content, stats, result });
1727
2123
  }
1728
2124
  catch (parseErr) {
1729
- completed.set(seq, { ok: false, filePath, err: parseErr });
2125
+ completed.set(seq, { ok: false, filePath, content, stats, err: parseErr });
1730
2126
  }
1731
2127
  flushOrdered();
1732
2128
  })();
@@ -1739,8 +2135,11 @@ class ExtractionOrchestrator {
1739
2135
  // window has room. When nothing is in flight but the window is still full,
1740
2136
  // the async commit chain is what's behind — await it so the cursor
1741
2137
  // advances (buffered items hold whole file contents, so this bound is
1742
- // load-bearing for memory).
1743
- while (nextSeq - nextToStore >= windowSize) {
2138
+ // load-bearing for memory). Once a store has failed the cursor never
2139
+ // moves again — flushOrdered returns at once — so stop waiting and let
2140
+ // the drain below rethrow; waiting would spin on microtasks forever,
2141
+ // pinning a core and starving every timer in the process.
2142
+ while (nextSeq - nextToStore >= windowSize && !flushError && !aborted) {
1744
2143
  if (inFlight.size > 0)
1745
2144
  await Promise.race(inFlight);
1746
2145
  else
@@ -1765,8 +2164,20 @@ class ExtractionOrchestrator {
1765
2164
  (0, errors_1.logWarn)('Path traversal blocked in batch reader', { filePath: fp });
1766
2165
  return { filePath: fp, content: null, stats: null, error: new Error('Path traversal blocked') };
1767
2166
  }
1768
- const content = await fsp.readFile(fullPath, 'utf-8');
1769
- const stats = await fsp.stat(fullPath);
2167
+ // Stat first: a file over the size limit is stored as skipped
2168
+ // without ever being read or decoded (#1910), so ten oversize
2169
+ // fixtures in one I/O batch no longer cost their size in RSS.
2170
+ const { stats, bytes } = await (0, file_limits_1.readBoundedSource)(fullPath);
2171
+ if (bytes === null) {
2172
+ return { filePath: fp, content: (0, file_limits_1.oversizeStamp)(stats.size), stats, error: null };
2173
+ }
2174
+ // Read bytes, not text: a `.ts` that is really an MPEG transport
2175
+ // stream (#1910) is recognised from its head here, at no extra I/O,
2176
+ // and never decoded or parsed.
2177
+ if (isMpegTsBytes(fp, bytes)) {
2178
+ return { filePath: fp, content: null, stats: null, error: null, skipped: true };
2179
+ }
2180
+ const content = bytes.toString('utf-8');
1770
2181
  return { filePath: fp, content, stats, error: null };
1771
2182
  }
1772
2183
  catch (err) {
@@ -1775,11 +2186,17 @@ class ExtractionOrchestrator {
1775
2186
  }));
1776
2187
  // Dispatch each readable file into the bounded parse window; the window
1777
2188
  // stores results on the main thread as they arrive.
1778
- for (const { filePath, content, stats, error } of fileContents) {
2189
+ for (const { filePath, content, stats, error, skipped } of fileContents) {
1779
2190
  if (signal?.aborted) {
1780
2191
  aborted = true;
1781
2192
  break;
1782
2193
  }
2194
+ if (skipped) {
2195
+ // Not a source file after all — counted as done, stored as nothing.
2196
+ processed++;
2197
+ onProgress?.({ phase: 'parsing', current: processed, total });
2198
+ continue;
2199
+ }
1783
2200
  if (error || content === null || stats === null) {
1784
2201
  processed++;
1785
2202
  filesErrored++;
@@ -1792,21 +2209,24 @@ class ExtractionOrchestrator {
1792
2209
  onProgress?.({ phase: 'parsing', current: processed, total });
1793
2210
  continue;
1794
2211
  }
1795
- // Honour MAX_FILE_SIZE. Without this check, vendored generated
2212
+ // Honour MAX_SOURCE_FILE_SIZE_BYTES. Without this check, vendored generated
1796
2213
  // headers, minified bundles, and other multi-MB files get indexed,
1797
2214
  // wasting WASM heap and the worker recycle budget on inputs with no
1798
2215
  // useful symbols. The single-file extractFile path already enforces
1799
2216
  // this; the bulk path used to silently skip the check.
1800
- if (stats.size > MAX_FILE_SIZE) {
1801
- processed++;
1802
- filesSkipped++;
1803
- errors.push({
1804
- message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
1805
- filePath,
1806
- severity: 'warning',
1807
- code: 'size_exceeded',
2217
+ if (stats.size > file_limits_1.MAX_SOURCE_FILE_SIZE_BYTES) {
2218
+ await storeResult(filePath, content, stats, {
2219
+ nodes: [],
2220
+ edges: [],
2221
+ unresolvedReferences: [],
2222
+ errors: [{
2223
+ message: `File exceeds max size (${stats.size} > ${file_limits_1.MAX_SOURCE_FILE_SIZE_BYTES})`,
2224
+ filePath,
2225
+ severity: 'warning',
2226
+ code: 'size_exceeded',
2227
+ }],
2228
+ durationMs: 0,
1808
2229
  });
1809
- onProgress?.({ phase: 'parsing', current: processed, total });
1810
2230
  continue;
1811
2231
  }
1812
2232
  // Parse on the pool (main thread stays unblocked). Errors/timeouts are
@@ -1824,6 +2244,9 @@ class ExtractionOrchestrator {
1824
2244
  if (flushError) {
1825
2245
  if (storeWriter)
1826
2246
  await storeWriter.close();
2247
+ // Its worker threads would otherwise outlive the failed index.
2248
+ if (pool)
2249
+ await pool.destroy();
1827
2250
  throw flushError;
1828
2251
  }
1829
2252
  // All bundles are posted; wait for the writer to apply them, then close
@@ -1852,6 +2275,7 @@ class ExtractionOrchestrator {
1852
2275
  filesSkipped,
1853
2276
  filesErrored,
1854
2277
  filesDiscovered: total,
2278
+ ...skipSummary(),
1855
2279
  nodesCreated: totalNodes,
1856
2280
  edgesCreated: totalEdges,
1857
2281
  errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
@@ -1894,7 +2318,11 @@ class ExtractionOrchestrator {
1894
2318
  const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
1895
2319
  if (!fullPath)
1896
2320
  continue;
1897
- content = await fsp.readFile(fullPath, 'utf-8');
2321
+ // Bounded like the first read: the file may have grown since (#1910).
2322
+ const bytes = (await (0, file_limits_1.readBoundedSource)(fullPath)).bytes;
2323
+ if (bytes === null)
2324
+ continue;
2325
+ content = bytes.toString('utf8');
1898
2326
  }
1899
2327
  catch {
1900
2328
  continue;
@@ -1907,8 +2335,15 @@ class ExtractionOrchestrator {
1907
2335
  stillFailing.push(errEntry);
1908
2336
  continue;
1909
2337
  }
2338
+ // The pool hands kernel results back as an undecoded buffer transport
2339
+ // (`nodes`/`edges` EMPTY, tables in kernelBuffers). The main loop
2340
+ // decodes or forwards to the store worker; this path stores directly,
2341
+ // so decode here — otherwise a kernel-language retry passes the gate
2342
+ // below via `errors.length === 0`, stores nothing, and the file is
2343
+ // permanently recorded as "(0 symbols)" with the error erased (#1541).
2344
+ const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
2345
+ result = (0, kernel_1.materializeKernelResult)(result, filePath, language);
1910
2346
  if (result.nodes.length > 0 || result.errors.length === 0) {
1911
- const language = (0, grammars_1.detectLanguage)(filePath, content, overrides);
1912
2347
  const stats = await fsp.stat(path.join(this.rootDir, filePath));
1913
2348
  await this.storeExtractionResult(filePath, content, language, stats, result, commitYield);
1914
2349
  const idx = errors.indexOf(errEntry);
@@ -1937,7 +2372,10 @@ class ExtractionOrchestrator {
1937
2372
  const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
1938
2373
  if (!fullPath)
1939
2374
  continue;
1940
- fullContent = await fsp.readFile(fullPath, 'utf-8');
2375
+ const bytes = (await (0, file_limits_1.readBoundedSource)(fullPath)).bytes;
2376
+ if (bytes === null)
2377
+ continue;
2378
+ fullContent = bytes.toString('utf8');
1941
2379
  }
1942
2380
  catch {
1943
2381
  continue;
@@ -1955,13 +2393,20 @@ class ExtractionOrchestrator {
1955
2393
  catch {
1956
2394
  continue;
1957
2395
  }
2396
+ // Same undecoded-transport hazard as the first retry pass (#1541).
2397
+ const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
2398
+ result = (0, kernel_1.materializeKernelResult)(result, filePath, language);
1958
2399
  if (result.nodes.length > 0 || result.errors.length === 0) {
1959
- const language = (0, grammars_1.detectLanguage)(filePath, fullContent, overrides);
1960
2400
  const stats = await fsp.stat(path.join(this.rootDir, filePath));
1961
2401
  await this.storeExtractionResult(filePath, fullContent, language, stats, result, commitYield);
1962
- const idx = errors.indexOf(errEntry);
1963
- if (idx >= 0)
1964
- errors.splice(idx, 1);
2402
+ // Salvaged from comment-stripped source: keep a visible trace in
2403
+ // the summary instead of erasing the failure outright — the
2404
+ // stored result may be missing whatever the failing parse choked
2405
+ // on, and a silently "clean" file here is how an index quietly
2406
+ // disagrees with a later per-file sync of the same bytes (#1565).
2407
+ errEntry.severity = 'warning';
2408
+ errEntry.code = 'salvaged_stripped';
2409
+ errEntry.message = `Indexed from comment-stripped source after repeated parse failures (symbols may be incomplete until the file is re-indexed): ${errEntry.message}`;
1965
2410
  filesErrored--;
1966
2411
  filesIndexed++;
1967
2412
  totalNodes += result.nodes.length;
@@ -1980,6 +2425,7 @@ class ExtractionOrchestrator {
1980
2425
  filesSkipped,
1981
2426
  filesErrored,
1982
2427
  filesDiscovered: total,
2428
+ ...skipSummary(),
1983
2429
  nodesCreated: totalNodes,
1984
2430
  edgesCreated: totalEdges,
1985
2431
  errors,
@@ -2050,8 +2496,14 @@ class ExtractionOrchestrator {
2050
2496
  let content;
2051
2497
  let stats;
2052
2498
  try {
2053
- stats = await fsp.stat(fullPath);
2054
- content = await fsp.readFile(fullPath, 'utf-8');
2499
+ // An oversize file is stored as skipped; its bytes are never needed (#1910).
2500
+ const read = await (0, file_limits_1.readBoundedSource)(fullPath);
2501
+ stats = read.stats;
2502
+ // An MPEG transport stream named `.ts` is not TypeScript (#1910).
2503
+ if (read.bytes !== null && isMpegTsBytes(relativePath, read.bytes)) {
2504
+ return { nodes: [], edges: [], unresolvedReferences: [], errors: [], durationMs: 0 };
2505
+ }
2506
+ content = read.bytes === null ? (0, file_limits_1.oversizeStamp)(stats.size) : read.bytes.toString('utf8');
2055
2507
  }
2056
2508
  catch (error) {
2057
2509
  return {
@@ -2088,15 +2540,16 @@ class ExtractionOrchestrator {
2088
2540
  durationMs: 0,
2089
2541
  };
2090
2542
  }
2543
+ const language = (0, grammars_1.detectLanguage)(relativePath, content, (0, project_config_1.loadExtensionOverrides)(this.rootDir));
2091
2544
  // Check file size
2092
- if (stats.size > MAX_FILE_SIZE) {
2093
- return {
2545
+ if (stats.size > file_limits_1.MAX_SOURCE_FILE_SIZE_BYTES) {
2546
+ const result = {
2094
2547
  nodes: [],
2095
2548
  edges: [],
2096
2549
  unresolvedReferences: [],
2097
2550
  errors: [
2098
2551
  {
2099
- message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
2552
+ message: `File exceeds max size (${stats.size} > ${file_limits_1.MAX_SOURCE_FILE_SIZE_BYTES})`,
2100
2553
  filePath: relativePath,
2101
2554
  severity: 'warning',
2102
2555
  code: 'size_exceeded',
@@ -2104,9 +2557,10 @@ class ExtractionOrchestrator {
2104
2557
  ],
2105
2558
  durationMs: 0,
2106
2559
  };
2560
+ await this.storeExtractionResult(relativePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
2561
+ return result;
2107
2562
  }
2108
2563
  // Detect language (honoring the project's lattice-sensor.json extension overrides)
2109
- const language = (0, grammars_1.detectLanguage)(relativePath, content, (0, project_config_1.loadExtensionOverrides)(this.rootDir));
2110
2564
  if (!(0, grammars_1.isLanguageSupported)(language)) {
2111
2565
  return {
2112
2566
  nodes: [],
@@ -2122,15 +2576,43 @@ class ExtractionOrchestrator {
2122
2576
  const frameworkNames = this.ensureDetectedFrameworks();
2123
2577
  const result = (0, tree_sitter_1.extractFromSource)(relativePath, content, language, frameworkNames);
2124
2578
  // Store in database
2125
- if (result.nodes.length > 0 || result.errors.length === 0) {
2126
- await this.storeExtractionResult(relativePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
2127
- }
2579
+ await this.storeExtractionResult(relativePath, content, language, stats, result, (0, cooperative_yield_1.createYielder)());
2128
2580
  return result;
2129
2581
  }
2130
2582
  /**
2131
2583
  * Store extraction result in database
2132
2584
  */
2585
+ /**
2586
+ * Delete file rows recorded with ZERO nodes so their files re-index.
2587
+ *
2588
+ * No extraction path stores an empty, error-free result for a
2589
+ * symbol-bearing language — even an empty file keeps its file node — so a
2590
+ * zero-node row is a wiped one (#1541: an interrupted parse's retry stored
2591
+ * an undecoded kernel transport). The wiped row's content hash matches the
2592
+ * on-disk bytes, so every hash-based reconcile skips the file forever;
2593
+ * deleting the row lets the normal add path repair it. File-level-only
2594
+ * languages (yaml, twig, properties) are left alone. Deleting a zero-node
2595
+ * row cascades nothing: it has no nodes, so no edges or refs either.
2596
+ */
2597
+ healZeroNodeRows() {
2598
+ for (const f of this.queries.getAllFiles()) {
2599
+ // A zero-node row WITH recorded errors is a deliberate skip marker
2600
+ // (#1557: oversized / repeatedly-unparseable files are persisted with
2601
+ // their reason so syncs stop retrying them) — leave those alone. The
2602
+ // #1541 wipe rows are the error-FREE zero-node rows.
2603
+ if (f.nodeCount === 0 &&
2604
+ !(0, grammars_1.isFileLevelOnlyLanguage)(f.language) &&
2605
+ (f.errors === undefined || f.errors.length === 0)) {
2606
+ this.queries.deleteFile(f.path);
2607
+ }
2608
+ }
2609
+ }
2133
2610
  async storeExtractionResult(filePath, content, language, stats, result, onYield) {
2611
+ // A kernel result can arrive as an undecoded buffer transport (empty
2612
+ // node/edge arrays, tables riding in kernelBuffers). Decode it before
2613
+ // storing — persisting the transport as-is records the file as having no
2614
+ // symbols at all (#1541). No-op for already-decoded results.
2615
+ result = (0, kernel_1.materializeKernelResult)(result, filePath, language);
2134
2616
  // Bulk inserts run in bounded sub-transactions with a yield between, so a
2135
2617
  // giant generated file (tens of thousands of symbols) can't block the
2136
2618
  // event loop — and the #850 watchdog heartbeat — for the whole store.
@@ -2139,7 +2621,12 @@ class ExtractionOrchestrator {
2139
2621
  // is unchanged: a partially-stored file has no record and re-indexes.
2140
2622
  const STORE_CHUNK = 2000;
2141
2623
  const contentHash = hashContent(content);
2142
- // Check if file already exists and hasn't changed
2624
+ // Check if file already exists and hasn't changed. A skip/failure MARKER
2625
+ // row (zero nodes + recorded errors, #1557) never blocks a store carrying
2626
+ // real content: markers are written BEFORE the retry pass under the same
2627
+ // content hash, so treating them as "no changes" would silently discard a
2628
+ // successful retry's symbols — a permanent empty file presented as
2629
+ // recovered (the #1541 wipe, reintroduced through the marker path).
2143
2630
  const existingFile = this.queries.getFileByPath(filePath);
2144
2631
  if (existingFile && existingFile.contentHash === contentHash
2145
2632
  && existingFile.extractionVersion >= extraction_version_1.EXTRACTION_VERSION) {
@@ -2147,8 +2634,16 @@ class ExtractionOrchestrator {
2147
2634
  // one. `>=` not `===`: rewriting a newer engine's row with this engine's
2148
2635
  // output is a downgrade, and two engines sharing an index would overwrite
2149
2636
  // each other forever.
2150
- return;
2637
+ const existingIsMarker = existingFile.nodeCount === 0 && (existingFile.errors?.length ?? 0) > 0;
2638
+ const incomingHasContent = result.nodes.length > 0;
2639
+ if (!existingIsMarker || !incomingHasContent) {
2640
+ return; // No changes
2641
+ }
2151
2642
  }
2643
+ // Re-decided on every re-index of a changed file, so a banner added (or
2644
+ // removed) by an edit is reflected on the next sync (#1500). Computed after
2645
+ // the unchanged-file early return so untouched files pay nothing.
2646
+ const generated = (0, generated_detection_1.detectGeneratedFile)(filePath, content);
2152
2647
  // Snapshot incoming cross-file edges BEFORE deleting this file's nodes.
2153
2648
  // `deleteFile` cascades to delete every edge whose source OR target is a
2154
2649
  // node in this file (edges.FK ... ON DELETE CASCADE). Edges whose SOURCE is
@@ -2209,6 +2704,7 @@ class ExtractionOrchestrator {
2209
2704
  nodeCount: result.nodes.length,
2210
2705
  extractionVersion: extraction_version_1.EXTRACTION_VERSION,
2211
2706
  errors: result.errors.length > 0 ? result.errors : undefined,
2707
+ generated,
2212
2708
  },
2213
2709
  });
2214
2710
  if (crossFileIncomingEdges.length > 0) {
@@ -2265,6 +2761,7 @@ class ExtractionOrchestrator {
2265
2761
  nodeCount: result.nodes.length,
2266
2762
  extractionVersion: extraction_version_1.EXTRACTION_VERSION,
2267
2763
  errors: result.errors.length > 0 ? result.errors : undefined,
2764
+ generated,
2268
2765
  };
2269
2766
  this.queries.upsertFile(fileRecord);
2270
2767
  }
@@ -2285,6 +2782,10 @@ class ExtractionOrchestrator {
2285
2782
  nodeCount,
2286
2783
  extractionVersion: extraction_version_1.EXTRACTION_VERSION,
2287
2784
  errors: resultErrors.length > 0 ? resultErrors : undefined,
2785
+ // Decided here, once, while the content is already in memory — never at
2786
+ // query time (#1500). The header scan short-circuits on a single
2787
+ // substring test for ~every hand-written file.
2788
+ generated: (0, generated_detection_1.detectGeneratedFile)(filePath, content),
2288
2789
  };
2289
2790
  }
2290
2791
  buildFreshStoreBundle(filePath, content, language, stats, result) {
@@ -2322,6 +2823,64 @@ class ExtractionOrchestrator {
2322
2823
  this.queries.insertUnresolvedRefsBatch(resurrected);
2323
2824
  }
2324
2825
  }
2826
+ /**
2827
+ * Re-open, for re-resolution, every resolution edge whose answer this sync
2828
+ * may have changed — the fix for index drift (CG-33).
2829
+ *
2830
+ * Incremental sync re-resolves only the references IN the changed files, but
2831
+ * resolution's answer is a function of the WHOLE graph: a reference binds to
2832
+ * one of the same-named definitions project-wide, so adding or removing a
2833
+ * definition of `pct` can change which `pct` every other file's `pct(...)`
2834
+ * should bind to. Those other files are never revisited, and their references
2835
+ * resolved successfully once and were deleted from `unresolved_refs`, so
2836
+ * nothing existed to revisit them with — the index kept an answer that was
2837
+ * correct against an older graph. Measured on lattice sensor's own long-lived
2838
+ * index: 4.3% of distinct edges differed from a clean rebuild, in BOTH
2839
+ * directions, overwhelmingly `calls`. See docs/benchmarks/index-drift-cg33.md.
2840
+ *
2841
+ * This deletes each affected edge and re-inserts it as the reference that
2842
+ * created it (the refName/refKind stamp), status='pending', for the sync's
2843
+ * resolution sweep to bind against the post-sync graph — the same input a
2844
+ * full rebuild resolves from, which is what makes the two converge.
2845
+ *
2846
+ * Deliberately conservative in three ways, because a wrong deletion is a
2847
+ * permanent edge loss while a missed rebind is only residual drift:
2848
+ * - an edge with no refName stamp (synthesized, or built by an engine older
2849
+ * than the stamp) is left ALONE rather than reconstructed from the target's
2850
+ * plain name, same rule as `resurrectRefFromDroppedEdge`;
2851
+ * - edges whose source is in a file this sync already re-extracted are
2852
+ * skipped — their references were re-resolved from scratch moments ago;
2853
+ * - very common names are skipped by the per-name ceiling in
2854
+ * `getResolutionEdgesByTargetName`.
2855
+ *
2856
+ * Returns the number of references resurrected.
2857
+ */
2858
+ resurrectStaleResolutionEdges(definitionDelta, changedFilePaths) {
2859
+ if (definitionDelta.length === 0)
2860
+ return 0;
2861
+ const alreadyFresh = new Set(changedFilePaths);
2862
+ const candidates = this.queries.getResolutionEdgesByTargetName(definitionDelta);
2863
+ const edgeIds = [];
2864
+ const refs = [];
2865
+ for (const e of candidates) {
2866
+ if (alreadyFresh.has(e.sourceFilePath))
2867
+ continue;
2868
+ const ref = resurrectRefFromDroppedEdge(e);
2869
+ if (!ref)
2870
+ continue; // no stamp — never delete what we cannot restore
2871
+ edgeIds.push(e.edgeId);
2872
+ refs.push(ref);
2873
+ }
2874
+ if (refs.length === 0)
2875
+ return 0;
2876
+ // Delete first. The sweep re-inserts whichever edge resolution now picks,
2877
+ // and `insertEdges` is INSERT OR IGNORE against idx_edges_identity — so a
2878
+ // rebind to the same target is a clean no-op, but leaving the old row in
2879
+ // place for a rebind ELSEWHERE would keep both, turning drift into
2880
+ // duplication.
2881
+ this.queries.replaceResolutionEdgesWithUnresolvedRefs(edgeIds, refs);
2882
+ return refs.length;
2883
+ }
2325
2884
  /**
2326
2885
  * Sync the index with the current file state.
2327
2886
  *
@@ -2340,7 +2899,15 @@ class ExtractionOrchestrator {
2340
2899
  * set is not exactly known (directory removals, event overflow): the full
2341
2900
  * scan-diff remains the ground truth those cases need (#1285).
2342
2901
  */
2343
- scopedPaths) {
2902
+ scopedPaths,
2903
+ /**
2904
+ * Writer-side WAL pressure valve (#1539). Called after every changed file
2905
+ * is stored, when no extraction transaction is open, so a checkpoint can
2906
+ * safely catch up before the next file grows the WAL further.
2907
+ */
2908
+ backpressure,
2909
+ /** Inspect changed inputs before deletion/re-extraction cascades their edges. */
2910
+ onFileChange) {
2344
2911
  await (0, grammars_1.initGrammars)(); // Initialize WASM runtime (grammars loaded lazily below)
2345
2912
  const startTime = Date.now();
2346
2913
  let filesChecked = 0;
@@ -2350,12 +2917,17 @@ class ExtractionOrchestrator {
2350
2917
  let extractionHealed = 0;
2351
2918
  let nodesUpdated = 0;
2352
2919
  const changedFilePaths = [];
2920
+ // `file\0name` definition pairs for the files this sync touches, sampled
2921
+ // BEFORE their nodes are replaced/deleted. Compared against the post-store
2922
+ // pairs below to derive `definitionDelta` (CG-33).
2923
+ const pairsBefore = new Set();
2353
2924
  onProgress?.({
2354
2925
  phase: 'scanning',
2355
2926
  current: 0,
2356
2927
  total: 0,
2357
2928
  });
2358
2929
  const filesToIndex = [];
2930
+ const failedFilePaths = [];
2359
2931
  // === Filesystem reconcile (git-independent) ===
2360
2932
  // The source of truth for "what changed" is the filesystem vs the indexed
2361
2933
  // state — never git. We enumerate the current source files and reconcile
@@ -2375,7 +2947,20 @@ class ExtractionOrchestrator {
2375
2947
  // reads `filesChecked === 0 && durationMs === 0` as the
2376
2948
  // lock-unavailable signature (#449).
2377
2949
  const unique = [...new Set(scopedPaths)];
2378
- currentFiles = unique.filter((p) => fs.existsSync(path.join(this.rootDir, p)));
2950
+ // A scoped path is "present" only if it exists AND is in scope — the
2951
+ // same two gates the full walk applies (source extension, scope
2952
+ // matcher). Without the scope gate a caller's stale view of scope
2953
+ // leaked straight into the index: the watcher re-parsed a file the
2954
+ // user had just excluded in `lattice-sensor.json` while `lattice sensor sync`
2955
+ // removed it (#1590). Out-of-scope paths fall out of `currentFiles`,
2956
+ // so a tracked one takes the removal branch below, exactly as a full
2957
+ // sync would treat it. (`include`-forced paths pass: ScopeIgnore
2958
+ // applies the include precedence itself.)
2959
+ const scope = this.scopedSyncMatcher();
2960
+ const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
2961
+ currentFiles = unique.filter((p) => (0, grammars_1.isSourceFile)(p, overrides) &&
2962
+ !scope.ignores(p) &&
2963
+ fs.existsSync(path.join(this.rootDir, p)));
2379
2964
  trackedFiles = [];
2380
2965
  for (const p of unique) {
2381
2966
  const rec = this.queries.getFileByPath(p);
@@ -2387,10 +2972,17 @@ class ExtractionOrchestrator {
2387
2972
  console.error(`[phase-timing] sync-scoped: ${Date.now() - tSyncScan}ms (${unique.length} paths, ${trackedFiles.length} tracked)`);
2388
2973
  }
2389
2974
  else {
2975
+ // Full reconcile: drop the memoized scope matcher so a nested
2976
+ // `.gitignore` / exclude-standard change that forced this full sync is
2977
+ // visible to the next scoped sync (#1728).
2978
+ this.scopedMatcher = null;
2390
2979
  currentFiles = await scanDirectoryAsync(this.rootDir);
2391
2980
  if (process.env.LATTICE_SENSOR_SYNTH_TIMINGS)
2392
2981
  console.error(`[phase-timing] sync-scan: ${Date.now() - tSyncScan}ms (${currentFiles.length} files)`);
2393
2982
  filesChecked = currentFiles.length;
2983
+ // Full reconcile only (scoped syncs must not touch rows outside their
2984
+ // scope): drop zero-node rows so the wiped files re-index as adds below.
2985
+ this.healZeroNodeRows();
2394
2986
  const tTracked = Date.now();
2395
2987
  trackedFiles = this.queries.getAllFiles();
2396
2988
  if (process.env.LATTICE_SENSOR_SYNTH_TIMINGS)
@@ -2407,26 +2999,34 @@ class ExtractionOrchestrator {
2407
2999
  // `reconcileChecks` drives the cooperative yield shared with the adds/mods loop
2408
3000
  // below (see SYNC_RECONCILE_YIELD_INTERVAL / issue #905).
2409
3001
  let reconcileChecks = 0;
3002
+ const removeTracked = (tracked) => {
3003
+ // Before the cascade deletes them, resurrect incoming cross-file
3004
+ // resolution edges as their original refs (#1240 removal case): the
3005
+ // callers live in files this sync will NOT revisit, so this is their
3006
+ // only chance to rebind to an alternative definition — or to park as
3007
+ // failed until the symbol reappears somewhere. (A deleted file whose
3008
+ // CALLERS are also being deleted is fine: their nodes cascade later
3009
+ // in this loop and take the resurrected rows with them.)
3010
+ // Every name this file defined is about to stop existing here, which
3011
+ // narrows the candidate set for that name repo-wide (CG-33).
3012
+ for (const pair of this.queries.getNodeNamePairsByFiles([tracked.path]))
3013
+ pairsBefore.add(pair);
3014
+ const incoming = this.queries.getCrossFileIncomingEdgesWithTarget(tracked.path);
3015
+ if (incoming.length > 0) {
3016
+ const resurrected = incoming
3017
+ .map((e) => resurrectRefFromDroppedEdge(e))
3018
+ .filter((r) => r !== null);
3019
+ if (resurrected.length > 0) {
3020
+ this.queries.insertUnresolvedRefsBatch(resurrected);
3021
+ }
3022
+ }
3023
+ onFileChange?.(tracked.path);
3024
+ this.queries.deleteFile(tracked.path);
3025
+ filesRemoved++;
3026
+ };
2410
3027
  for (const tracked of trackedFiles) {
2411
3028
  if (!currentSet.has(tracked.path) || !fs.existsSync(path.join(this.rootDir, tracked.path))) {
2412
- // Before the cascade deletes them, resurrect incoming cross-file
2413
- // resolution edges as their original refs (#1240 removal case): the
2414
- // callers live in files this sync will NOT revisit, so this is their
2415
- // only chance to rebind to an alternative definition — or to park as
2416
- // failed until the symbol reappears somewhere. (A deleted file whose
2417
- // CALLERS are also being deleted is fine: their nodes cascade later
2418
- // in this loop and take the resurrected rows with them.)
2419
- const incoming = this.queries.getCrossFileIncomingEdgesWithTarget(tracked.path);
2420
- if (incoming.length > 0) {
2421
- const resurrected = incoming
2422
- .map((e) => resurrectRefFromDroppedEdge(e))
2423
- .filter((r) => r !== null);
2424
- if (resurrected.length > 0) {
2425
- this.queries.insertUnresolvedRefsBatch(resurrected);
2426
- }
2427
- }
2428
- this.queries.deleteFile(tracked.path);
2429
- filesRemoved++;
3029
+ removeTracked(tracked);
2430
3030
  }
2431
3031
  if (++reconcileChecks % SYNC_RECONCILE_YIELD_INTERVAL === 0) {
2432
3032
  await new Promise((resolve) => setImmediate(resolve));
@@ -2467,39 +3067,54 @@ class ExtractionOrchestrator {
2467
3067
  }
2468
3068
  catch (error) {
2469
3069
  (0, errors_1.logDebug)('Skipping unstattable file during sync', { filePath, error: String(error) });
3070
+ failedFilePaths.push(filePath);
2470
3071
  continue;
2471
3072
  }
2472
3073
  }
2473
3074
  // New, or size/mtime changed — read + hash to confirm a real content change.
3075
+ // (An oversize file hashes as its size stamp, unread — #1910.)
2474
3076
  let content;
2475
3077
  try {
2476
- content = fs.readFileSync(fullPath, 'utf-8');
3078
+ content = readSourceOrStamp(fullPath);
2477
3079
  }
2478
3080
  catch (error) {
2479
3081
  (0, errors_1.logDebug)('Skipping unreadable file during sync', { filePath, error: String(error) });
3082
+ failedFilePaths.push(filePath);
3083
+ continue;
3084
+ }
3085
+ // Not source after all — an MPEG transport stream named `.ts` (#1910):
3086
+ // a new one is ignored, a tracked file that became one is removed.
3087
+ if (content === null) {
3088
+ if (tracked)
3089
+ removeTracked(tracked);
2480
3090
  continue;
2481
3091
  }
2482
3092
  const contentHash = hashContent(content);
2483
3093
  if (!tracked) {
3094
+ onFileChange?.(filePath, content);
2484
3095
  filesToIndex.push(filePath);
2485
3096
  changedFilePaths.push(filePath);
2486
3097
  filesAdded++;
2487
3098
  }
2488
3099
  else if (tracked.contentHash !== contentHash) {
3100
+ onFileChange?.(filePath, content);
2489
3101
  filesToIndex.push(filePath);
2490
3102
  changedFilePaths.push(filePath);
2491
3103
  filesModified++;
2492
3104
  }
2493
3105
  }
3106
+ // Sampled here — after the add/modify classification, before any file is
3107
+ // re-extracted — because `storeExtractionResult` deletes a file's nodes
3108
+ // before inserting the new ones, so this is the last point the pre-edit
3109
+ // definition set is readable (CG-33).
3110
+ if (filesToIndex.length > 0) {
3111
+ for (const pair of this.queries.getNodeNamePairsByFiles(filesToIndex))
3112
+ pairsBefore.add(pair);
3113
+ }
2494
3114
  // Load only grammars needed for changed files
2495
3115
  if (filesToIndex.length > 0) {
2496
3116
  const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
2497
- const neededLanguages = [...new Set(filesToIndex.map((f) => (0, grammars_1.detectLanguage)(f, undefined, overrides)))];
2498
- // .h files default to 'c' but may be C++ — ensure cpp grammar is loaded
2499
- if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
2500
- neededLanguages.push('cpp');
2501
- }
2502
- await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
3117
+ await (0, grammars_1.loadGrammarsForLanguages)(preloadLanguagesForFiles(filesToIndex, overrides));
2503
3118
  }
2504
3119
  // Index changed files
2505
3120
  const total = filesToIndex.length;
@@ -2512,7 +3127,12 @@ class ExtractionOrchestrator {
2512
3127
  currentFile: filePath,
2513
3128
  });
2514
3129
  const result = await this.indexFile(filePath);
3130
+ if (result.errors.some(e => e.severity === 'error'))
3131
+ failedFilePaths.push(filePath);
2515
3132
  nodesUpdated += result.nodes.length;
3133
+ const pause = backpressure?.();
3134
+ if (pause)
3135
+ await pause;
2516
3136
  }
2517
3137
  // The sync pass above re-extracted every stale-stamp file it saw. When no
2518
3138
  // stale rows remain, the whole index reflects current extraction semantics
@@ -2529,6 +3149,28 @@ class ExtractionOrchestrator {
2529
3149
  }
2530
3150
  catch { /* metadata is advisory — never fail a sync over it */ }
2531
3151
  }
3152
+ // Names whose definition set this sync changed: a `file\0name` pair present
3153
+ // before but not after (removed/renamed away) or after but not before
3154
+ // (added). A pair on both sides is untouched as far as resolution's
3155
+ // candidate set is concerned — only its node id moved, which
3156
+ // reattachCrossFileEdges already follows — so an edit that only changes
3157
+ // bodies yields an empty delta and no downstream rebind work (CG-33).
3158
+ //
3159
+ // Compared per FILE, not as one name set over the whole batch: a commit
3160
+ // that adds `collect` to a new file while an unrelated changed file already
3161
+ // defined `collect` must still flag the name, and a bare name set cancels
3162
+ // exactly that case out. That miss left the largest residual class in the
3163
+ // first measurement of this fix.
3164
+ const pairsAfter = this.queries.getNodeNamePairsByFiles(filesToIndex);
3165
+ const deltaNames = new Set();
3166
+ const nameOf = (pair) => pair.slice(pair.indexOf('\0') + 1);
3167
+ for (const pair of pairsBefore)
3168
+ if (!pairsAfter.has(pair))
3169
+ deltaNames.add(nameOf(pair));
3170
+ for (const pair of pairsAfter)
3171
+ if (!pairsBefore.has(pair))
3172
+ deltaNames.add(nameOf(pair));
3173
+ const definitionDelta = [...deltaNames];
2532
3174
  return {
2533
3175
  filesChecked,
2534
3176
  filesAdded,
@@ -2538,48 +3180,112 @@ class ExtractionOrchestrator {
2538
3180
  nodesUpdated,
2539
3181
  durationMs: Date.now() - startTime,
2540
3182
  changedFilePaths: changedFilePaths.length > 0 ? changedFilePaths : undefined,
3183
+ ...(failedFilePaths.length > 0 ? { failedFilePaths } : {}),
3184
+ definitionDelta: definitionDelta.length > 0 ? definitionDelta : undefined,
2541
3185
  };
2542
3186
  }
3187
+ indexedDirtyPaths(stamp) {
3188
+ try {
3189
+ const state = JSON.parse(this.queries.getMetadata('indexed_dirty_paths') ?? 'null');
3190
+ if (!state || state.commit !== (stamp ?? '') || !Array.isArray(state.paths))
3191
+ return null;
3192
+ if (!state.paths.every((p) => typeof p === 'string' && p.length > 0 &&
3193
+ !path.isAbsolute(p) && !p.split('/').includes('..')))
3194
+ return null;
3195
+ return state.paths;
3196
+ }
3197
+ catch {
3198
+ return null;
3199
+ }
3200
+ }
3201
+ /** Capture before file reads. In-flight/failed full writes must not claim freshness. */
3202
+ beginGitIndexState(full) {
3203
+ const head = getGitHeadSha(this.rootDir) ?? '';
3204
+ const stamp = this.queries.getMetadata(exports.INDEXED_AT_COMMIT_KEY) ?? '';
3205
+ const prior = this.indexedDirtyPaths(stamp);
3206
+ const status = getGitChangedFiles(this.rootDir);
3207
+ const dirty = status ? [...new Set([
3208
+ ...(full ? [] : prior ?? []), ...status.added, ...status.modified, ...status.deleted,
3209
+ ])] : null;
3210
+ if (full || prior === null || dirty === null) {
3211
+ this.queries.setMetadata(exports.INDEXED_AT_COMMIT_KEY, '');
3212
+ this.queries.setMetadata('indexed_dirty_paths', '');
3213
+ }
3214
+ else {
3215
+ // Scoped writes leave the commit alone and retain dirty paths before the
3216
+ // first write, so a crash cannot forget an indexed uncommitted edit.
3217
+ this.queries.setMetadata('indexed_dirty_paths', JSON.stringify({ commit: stamp, paths: dirty }));
3218
+ }
3219
+ return { head, stamp, dirty };
3220
+ }
3221
+ finishGitIndexState(snapshot, full, retries = []) {
3222
+ const after = getGitChangedFiles(this.rootDir);
3223
+ if (!snapshot.dirty || !after)
3224
+ return;
3225
+ const commit = full ? snapshot.head : snapshot.stamp;
3226
+ const paths = [...new Set([...snapshot.dirty, ...after.added, ...after.modified, ...after.deleted, ...retries])].sort();
3227
+ // The embedded commit makes a torn pair fail closed: readers reject a dirty
3228
+ // set that doesn't match the separately stored commit. Write the set first.
3229
+ this.queries.setMetadata('indexed_dirty_paths', JSON.stringify({ commit, paths }));
3230
+ if (full)
3231
+ this.queries.setMetadata(exports.INDEXED_AT_COMMIT_KEY, commit);
3232
+ }
2543
3233
  /**
2544
3234
  * Get files that have changed since last index.
2545
3235
  * Uses git status as a fast path when available, falling back to full scan.
2546
3236
  */
2547
3237
  getChangedFiles() {
2548
- const gitChanges = getGitChangedFiles(this.rootDir);
3238
+ // The commit this index was last brought up to date at. Absent on an index
3239
+ // built before stamping existed — getGitChangedFiles then declines the fast
3240
+ // path and the full scan below answers correctly, once, until a sync or a
3241
+ // full index writes the stamp. (#1829)
3242
+ let sinceCommit = null;
3243
+ try {
3244
+ sinceCommit = this.queries.getMetadata(exports.INDEXED_AT_COMMIT_KEY) ?? null;
3245
+ }
3246
+ catch { /* advisory */ }
3247
+ const dirtyPaths = this.indexedDirtyPaths(sinceCommit);
3248
+ const gitChanges = dirtyPaths !== null && canTrustGitFastPath(this.rootDir, sinceCommit)
3249
+ ? getGitChangedFiles(this.rootDir, sinceCommit)
3250
+ : null;
2549
3251
  if (gitChanges) {
2550
3252
  // === Git fast path ===
2551
3253
  const added = [];
2552
3254
  const modified = [];
2553
3255
  const removed = [];
2554
- // Deleted files — only report if tracked in DB
2555
- for (const filePath of gitChanges.deleted) {
3256
+ // Git supplies candidates, never the verdict. A committed deletion may
3257
+ // have been recreated locally; a previously indexed dirty path may have
3258
+ // vanished from git status after restore. Classify current disk vs DB once.
3259
+ const candidates = new Set([...gitChanges.deleted, ...gitChanges.modified, ...gitChanges.added, ...dirtyPaths]);
3260
+ const scope = this.scopedSyncMatcher();
3261
+ const overrides = (0, project_config_1.loadExtensionOverrides)(this.rootDir);
3262
+ for (const filePath of candidates) {
2556
3263
  const tracked = this.queries.getFileByPath(filePath);
2557
- if (tracked) {
2558
- removed.push(filePath);
2559
- }
2560
- }
2561
- // Modified + added files — read + hash, compare with DB. Untracked (`??`)
2562
- // files stay untracked in git even after indexing, so they must be
2563
- // hash-compared like modified files instead of always counting as added —
2564
- // otherwise status reports them as pending forever. (See issue #206.)
2565
- for (const filePath of [...gitChanges.modified, ...gitChanges.added]) {
2566
3264
  const fullPath = path.join(this.rootDir, filePath);
3265
+ if (!(0, grammars_1.isSourceFile)(filePath, overrides) || scope.ignores(filePath) || !fs.existsSync(fullPath)) {
3266
+ if (tracked)
3267
+ removed.push(filePath);
3268
+ continue;
3269
+ }
2567
3270
  let content;
2568
3271
  try {
2569
- content = fs.readFileSync(fullPath, 'utf-8');
3272
+ content = readSourceOrStamp(fullPath);
2570
3273
  }
2571
3274
  catch (error) {
2572
3275
  (0, errors_1.logDebug)('Skipping unreadable file while detecting changes', { filePath, error: String(error) });
2573
3276
  continue;
2574
3277
  }
2575
- const contentHash = hashContent(content);
2576
- const tracked = this.queries.getFileByPath(filePath);
2577
- if (!tracked) {
2578
- added.push(filePath);
3278
+ // A `.ts` that is an MPEG transport stream is not source (#1910), so an
3279
+ // untracked clip is never pending and a tracked file that became one is gone.
3280
+ if (content === null) {
3281
+ if (tracked)
3282
+ removed.push(filePath);
3283
+ continue;
2579
3284
  }
2580
- else if (tracked.contentHash !== contentHash) {
3285
+ if (!tracked)
3286
+ added.push(filePath);
3287
+ else if (tracked.contentHash !== hashContent(content))
2581
3288
  modified.push(filePath);
2582
- }
2583
3289
  }
2584
3290
  // Rows written by a different extractor are pending even though git sees
2585
3291
  // no change — the observer moved, not the file. Without this, the git
@@ -2613,14 +3319,20 @@ class ExtractionOrchestrator {
2613
3319
  const fullPath = path.join(this.rootDir, filePath);
2614
3320
  let content;
2615
3321
  try {
2616
- content = fs.readFileSync(fullPath, 'utf-8');
3322
+ content = readSourceOrStamp(fullPath);
2617
3323
  }
2618
3324
  catch (error) {
2619
3325
  (0, errors_1.logDebug)('Skipping unreadable file while detecting changes', { filePath, error: String(error) });
2620
3326
  continue;
2621
3327
  }
2622
- const contentHash = hashContent(content);
2623
3328
  const tracked = trackedMap.get(filePath);
3329
+ // An MPEG transport stream named `.ts` (#1910) is not source.
3330
+ if (content === null) {
3331
+ if (tracked)
3332
+ removed.push(filePath);
3333
+ continue;
3334
+ }
3335
+ const contentHash = hashContent(content);
2624
3336
  if (!tracked) {
2625
3337
  added.push(filePath);
2626
3338
  }