@backbencher/brain 0.0.0-stage → 0.1.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 (319) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/EMBEDDINGS.md +355 -0
  3. package/LICENSE +21 -0
  4. package/README.md +415 -2
  5. package/bin/bb +13 -0
  6. package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
  7. package/dist/brain-index-health-KY7K552E.js +17 -0
  8. package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-2BSKD7O3.js +40 -0
  11. package/dist/chunk-2DE27DVC.js +1314 -0
  12. package/dist/chunk-2RRBMNBQ.js +322 -0
  13. package/dist/chunk-2RTN2DFU.js +711 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3LFTSMIV.js +174 -0
  17. package/dist/chunk-47HKX4AJ.js +930 -0
  18. package/dist/chunk-4H22TLOT.js +543 -0
  19. package/dist/chunk-5SD45FLC.js +818 -0
  20. package/dist/chunk-66A5ZFYA.js +1758 -0
  21. package/dist/chunk-6AIRWAO2.js +247 -0
  22. package/dist/chunk-6WNGD3NN.js +196 -0
  23. package/dist/chunk-7AN7U5IZ.js +371 -0
  24. package/dist/chunk-BHB7BYYI.js +626 -0
  25. package/dist/chunk-BQ3OVQ3P.js +125 -0
  26. package/dist/chunk-CW4XXSNN.js +1479 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DYM4FQEE.js +90 -0
  30. package/dist/chunk-EWMWK7WN.js +75 -0
  31. package/dist/chunk-FVVQTSIM.js +3427 -0
  32. package/dist/chunk-FWSL4VG6.js +1290 -0
  33. package/dist/chunk-GBI7V7KY.js +69 -0
  34. package/dist/chunk-GEFFO6O2.js +152 -0
  35. package/dist/chunk-HOWAQXJO.js +236 -0
  36. package/dist/chunk-HQSPJDHI.js +613 -0
  37. package/dist/chunk-IFZNR5UJ.js +2863 -0
  38. package/dist/chunk-J4PMBXQQ.js +336 -0
  39. package/dist/chunk-KAET47ZL.js +125 -0
  40. package/dist/chunk-KCDECRJY.js +440 -0
  41. package/dist/chunk-KLK3PBTO.js +1119 -0
  42. package/dist/chunk-L5TDYKYK.js +76 -0
  43. package/dist/chunk-MLKGABMK.js +9 -0
  44. package/dist/chunk-OWISBAOF.js +61 -0
  45. package/dist/chunk-PJTNGD3Z.js +6924 -0
  46. package/dist/chunk-QLK6ED4Q.js +235 -0
  47. package/dist/chunk-R2GW4F6R.js +19 -0
  48. package/dist/chunk-RCMTFSEF.js +87 -0
  49. package/dist/chunk-RH7KC5HU.js +12050 -0
  50. package/dist/chunk-RM3PY6TG.js +376 -0
  51. package/dist/chunk-ROMM6KMC.js +433 -0
  52. package/dist/chunk-SK6YVHFZ.js +211 -0
  53. package/dist/chunk-SM7RFQRO.js +214 -0
  54. package/dist/chunk-UFE77L67.js +782 -0
  55. package/dist/chunk-UGR4QIGS.js +67 -0
  56. package/dist/chunk-UUNHHX4M.js +13 -0
  57. package/dist/chunk-V44PGPA6.js +2104 -0
  58. package/dist/chunk-VL52PL2T.js +71 -0
  59. package/dist/chunk-VURYFIHW.js +201 -0
  60. package/dist/chunk-XJKJYIPP.js +7436 -0
  61. package/dist/chunk-YI6FF2ZR.js +340 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-642F7E5V.js +593 -0
  65. package/dist/database-TCTZDLE5.js +9 -0
  66. package/dist/doctor-GXCKSJOX.js +395 -0
  67. package/dist/edit-engine-KM7MPFEC.js +51 -0
  68. package/dist/engine-42TZFNIZ.js +11 -0
  69. package/dist/engine-LO6L4JLQ.js +18 -0
  70. package/dist/fingerprint-XDXFRWO6.js +33 -0
  71. package/dist/framework-intelligence-DWFCKV44.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-PL5YA7BA.js +21 -0
  74. package/dist/graph_request-MM4REP6H.js +350 -0
  75. package/dist/impact_request-WDE47IZ5.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-6BD7UBHR.js +34 -0
  78. package/dist/index.js +159 -0
  79. package/dist/instance-FO6QZRLH.js +19 -0
  80. package/dist/language-detection-DFCAU5DZ.js +11 -0
  81. package/dist/lsp/server.js +4301 -0
  82. package/dist/mcp/server.js +132 -0
  83. package/dist/onboard-DIQZV2NW.js +161 -0
  84. package/dist/override-companion-pass-XR7MHTP7.js +117 -0
  85. package/dist/parallel-indexer-4PW3AFXP.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-ZS5TS2Z6.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-DGZBIZS7.js +355 -0
  90. package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
  91. package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
  92. package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
  93. package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
  94. package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
  95. package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
  96. package/dist/real-css-analyzer-HPU277Q6.js +15 -0
  97. package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
  98. package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
  99. package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
  100. package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
  101. package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
  102. package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
  103. package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
  104. package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
  105. package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
  106. package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
  107. package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
  108. package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
  109. package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
  110. package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
  111. package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
  112. package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
  113. package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
  114. package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
  115. package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
  116. package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
  117. package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
  118. package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
  119. package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
  120. package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
  121. package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
  122. package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
  123. package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
  124. package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
  125. package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
  126. package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
  127. package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
  128. package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
  129. package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
  130. package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
  131. package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
  132. package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
  133. package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
  134. package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
  135. package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
  136. package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
  137. package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
  138. package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
  139. package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
  140. package/dist/regex-search-OPTLKUN5.js +239 -0
  141. package/dist/rules-I453JTRC.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-Y5GB5KXA.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-KB4MU4SI.js +16 -0
  146. package/dist/taint-YOTN5X55.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-G4NAGJIH.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-RF675A3C.js +32 -0
  151. package/dist/type-inference-4Y7WTMFE.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-LXGNFO37.js +9 -0
  154. package/dist/wasm-NMWO3WXY.js +16 -0
  155. package/docs/EMBEDDINGS.md +132 -0
  156. package/docs/README.md +178 -0
  157. package/docs/data/m10-baseline.json +602 -0
  158. package/docs/data/m10-benchmark-baseline.json +57 -0
  159. package/docs/data/m10-benchmark-last-run.json +127 -0
  160. package/docs/data/m10-final-audit.json +215 -0
  161. package/docs/data/m12-final-audit.json +138 -0
  162. package/docs/data/m12-surface-matrix.json +50 -0
  163. package/docs/data/public-tools.json +1902 -0
  164. package/docs/indexing/DEAD-ASSETS.md +88 -0
  165. package/docs/indexing/INDEXER-REVIEW.md +634 -0
  166. package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
  167. package/docs/indexing/README.md +427 -0
  168. package/docs/indexing/discovery-matrix.md +41 -0
  169. package/docs/indexing/embeddings.md +325 -0
  170. package/docs/indexing/environment-variables.md +77 -0
  171. package/docs/indexing/performance.md +323 -0
  172. package/docs/indexing/resume.md +345 -0
  173. package/docs/indexing/troubleshooting.md +391 -0
  174. package/docs/man/bb.1 +190 -0
  175. package/docs/tools/bb_analyze.md +147 -0
  176. package/docs/tools/bb_annotate.md +110 -0
  177. package/docs/tools/bb_annotations.md +106 -0
  178. package/docs/tools/bb_ask.md +101 -0
  179. package/docs/tools/bb_cross_language.md +103 -0
  180. package/docs/tools/bb_doc.md +169 -0
  181. package/docs/tools/bb_edit.md +185 -0
  182. package/docs/tools/bb_graph.md +169 -0
  183. package/docs/tools/bb_health.md +156 -0
  184. package/docs/tools/bb_impact.md +117 -0
  185. package/docs/tools/bb_index.md +108 -0
  186. package/docs/tools/bb_migrate.md +135 -0
  187. package/docs/tools/bb_patterns.md +140 -0
  188. package/docs/tools/bb_search.md +150 -0
  189. package/docs/tools/bb_security.md +100 -0
  190. package/docs/tools/bb_visualize.md +184 -0
  191. package/grammars/README.md +57 -0
  192. package/grammars/THIRD-PARTY-NOTICES.md +50 -0
  193. package/grammars/tree-sitter-c/LICENSE +21 -0
  194. package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
  195. package/grammars/tree-sitter-cpp/LICENSE +21 -0
  196. package/grammars/tree-sitter-csharp/LICENSE +21 -0
  197. package/grammars/tree-sitter-css/LICENSE +21 -0
  198. package/grammars/tree-sitter-css/README.md +21 -0
  199. package/grammars/tree-sitter-dart/LICENSE +20 -0
  200. package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
  201. package/grammars/tree-sitter-dart-upstream/README.md +18 -0
  202. package/grammars/tree-sitter-elixir/LICENSE +176 -0
  203. package/grammars/tree-sitter-elixir/README.md +11 -0
  204. package/grammars/tree-sitter-fsharp/LICENSE +21 -0
  205. package/grammars/tree-sitter-fsharp/README.md +45 -0
  206. package/grammars/tree-sitter-graphql/LICENSE +21 -0
  207. package/grammars/tree-sitter-graphql/README.md +106 -0
  208. package/grammars/tree-sitter-groovy/LICENSE +21 -0
  209. package/grammars/tree-sitter-groovy/README.md +4 -0
  210. package/grammars/tree-sitter-haskell/LICENSE +21 -0
  211. package/grammars/tree-sitter-haskell/README.md +394 -0
  212. package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
  213. package/grammars/tree-sitter-hcl/LICENSE +201 -0
  214. package/grammars/tree-sitter-hcl/README.md +39 -0
  215. package/grammars/tree-sitter-html/LICENSE +21 -0
  216. package/grammars/tree-sitter-html/README.md +21 -0
  217. package/grammars/tree-sitter-julia/LICENSE +21 -0
  218. package/grammars/tree-sitter-julia/README.md +24 -0
  219. package/grammars/tree-sitter-kotlin/LICENSE +9 -0
  220. package/grammars/tree-sitter-lua/LICENSE.md +21 -0
  221. package/grammars/tree-sitter-lua/README.md +29 -0
  222. package/grammars/tree-sitter-make/LICENSE +21 -0
  223. package/grammars/tree-sitter-matlab/LICENSE +19 -0
  224. package/grammars/tree-sitter-matlab/README.md +99 -0
  225. package/grammars/tree-sitter-nim/LICENSE +373 -0
  226. package/grammars/tree-sitter-nim/README.md +46 -0
  227. package/grammars/tree-sitter-objc/LICENSE +21 -0
  228. package/grammars/tree-sitter-objc/README.md +20 -0
  229. package/grammars/tree-sitter-perl/LICENSE +7 -0
  230. package/grammars/tree-sitter-perl/README.md +100 -0
  231. package/grammars/tree-sitter-powershell/LICENSE +21 -0
  232. package/grammars/tree-sitter-powershell/README.md +7 -0
  233. package/grammars/tree-sitter-proto/LICENSE +21 -0
  234. package/grammars/tree-sitter-proto/README.md +105 -0
  235. package/grammars/tree-sitter-r/LICENSE +9 -0
  236. package/grammars/tree-sitter-r/README.md +66 -0
  237. package/grammars/tree-sitter-ruby/LICENSE +22 -0
  238. package/grammars/tree-sitter-scss/LICENSE +21 -0
  239. package/grammars/tree-sitter-scss/README.md +17 -0
  240. package/grammars/tree-sitter-solidity/LICENSE +7 -0
  241. package/grammars/tree-sitter-sql/LICENSE +21 -0
  242. package/grammars/tree-sitter-typescript/LICENSE +21 -0
  243. package/grammars/tree-sitter-vba/LICENSE +21 -0
  244. package/grammars/tree-sitter-vba/README.md +738 -0
  245. package/grammars/tree-sitter-vbnet/LICENSE +21 -0
  246. package/grammars/tree-sitter-vbnet/README.md +322 -0
  247. package/grammars/tree-sitter-xml/LICENSE +19 -0
  248. package/grammars/tree-sitter-yaml/LICENSE +20 -0
  249. package/grammars/tree-sitter-yaml/README.md +21 -0
  250. package/package.json +184 -4
  251. package/wasm/dart.wasm +0 -0
  252. package/wasm/tree-sitter-bash.wasm +0 -0
  253. package/wasm/tree-sitter-c.wasm +0 -0
  254. package/wasm/tree-sitter-c_sharp.wasm +0 -0
  255. package/wasm/tree-sitter-clean.wasm +0 -0
  256. package/wasm/tree-sitter-clojure.wasm +0 -0
  257. package/wasm/tree-sitter-core_schema.wasm +0 -0
  258. package/wasm/tree-sitter-cpp.wasm +0 -0
  259. package/wasm/tree-sitter-css.wasm +0 -0
  260. package/wasm/tree-sitter-dart.wasm +0 -0
  261. package/wasm/tree-sitter-elisp.wasm +0 -0
  262. package/wasm/tree-sitter-elixir.wasm +0 -0
  263. package/wasm/tree-sitter-elm.wasm +0 -0
  264. package/wasm/tree-sitter-embedded_template.wasm +0 -0
  265. package/wasm/tree-sitter-erlang.wasm +0 -0
  266. package/wasm/tree-sitter-fsharp.wasm +0 -0
  267. package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
  268. package/wasm/tree-sitter-go.wasm +0 -0
  269. package/wasm/tree-sitter-graphql.wasm +0 -0
  270. package/wasm/tree-sitter-groovy.wasm +0 -0
  271. package/wasm/tree-sitter-haskell.wasm +0 -0
  272. package/wasm/tree-sitter-hcl.wasm +0 -0
  273. package/wasm/tree-sitter-html.wasm +0 -0
  274. package/wasm/tree-sitter-java.wasm +0 -0
  275. package/wasm/tree-sitter-javascript.wasm +0 -0
  276. package/wasm/tree-sitter-json.wasm +0 -0
  277. package/wasm/tree-sitter-json_schema.wasm +0 -0
  278. package/wasm/tree-sitter-julia.wasm +0 -0
  279. package/wasm/tree-sitter-kotlin.wasm +0 -0
  280. package/wasm/tree-sitter-lua.wasm +0 -0
  281. package/wasm/tree-sitter-make.wasm +0 -0
  282. package/wasm/tree-sitter-markdown.wasm +0 -0
  283. package/wasm/tree-sitter-markdown_inline.wasm +0 -0
  284. package/wasm/tree-sitter-matlab.wasm +0 -0
  285. package/wasm/tree-sitter-nim.wasm +0 -0
  286. package/wasm/tree-sitter-objc.wasm +0 -0
  287. package/wasm/tree-sitter-ocaml.wasm +0 -0
  288. package/wasm/tree-sitter-perl.wasm +0 -0
  289. package/wasm/tree-sitter-php.wasm +0 -0
  290. package/wasm/tree-sitter-php_only.wasm +0 -0
  291. package/wasm/tree-sitter-powershell.wasm +0 -0
  292. package/wasm/tree-sitter-protobuf.wasm +0 -0
  293. package/wasm/tree-sitter-python.wasm +0 -0
  294. package/wasm/tree-sitter-ql.wasm +0 -0
  295. package/wasm/tree-sitter-r.wasm +0 -0
  296. package/wasm/tree-sitter-rescript.wasm +0 -0
  297. package/wasm/tree-sitter-ruby.wasm +0 -0
  298. package/wasm/tree-sitter-rust.wasm +0 -0
  299. package/wasm/tree-sitter-scala.wasm +0 -0
  300. package/wasm/tree-sitter-scss.wasm +0 -0
  301. package/wasm/tree-sitter-solidity.wasm +0 -0
  302. package/wasm/tree-sitter-sql.wasm +0 -0
  303. package/wasm/tree-sitter-svelte.wasm +0 -0
  304. package/wasm/tree-sitter-swift.wasm +0 -0
  305. package/wasm/tree-sitter-systemrdl.wasm +0 -0
  306. package/wasm/tree-sitter-terraform.wasm +0 -0
  307. package/wasm/tree-sitter-tlaplus.wasm +0 -0
  308. package/wasm/tree-sitter-toml.wasm +0 -0
  309. package/wasm/tree-sitter-tsx.wasm +0 -0
  310. package/wasm/tree-sitter-typescript.wasm +0 -0
  311. package/wasm/tree-sitter-vba.wasm +0 -0
  312. package/wasm/tree-sitter-vbnet.wasm +0 -0
  313. package/wasm/tree-sitter-vue.wasm +0 -0
  314. package/wasm/tree-sitter-xml.wasm +0 -0
  315. package/wasm/tree-sitter-yaml.wasm +0 -0
  316. package/wasm/tree-sitter-zig.wasm +0 -0
  317. package/wasm/tree-sitter.wasm +0 -0
  318. package/wasm/tsx.wasm +0 -0
  319. package/wasm/typescript.wasm +0 -0
@@ -0,0 +1,634 @@
1
+ <!--
2
+ Author: Muhammad Ishaq Khan
3
+
4
+ WHAT THIS DOCUMENT IS — the measured review of the indexer's file discovery, storage and derived phases, with each finding's evidence and status.
5
+ WHO IT IS FOR — maintainers of the indexing path and the gates that guard it.
6
+ EDIT TOGETHER WHEN THIS CHANGES — src/ast/parallel-indexer.ts, src/ast/index-space.ts, src/ast/index-lock.ts, and the gate scripts a finding names. A finding marked FIXED names the gate that now holds it.
7
+ -->
8
+
9
+ # Indexer review — time, space, scope, and the surface a user sees
10
+
11
+ Scope: `bb index` end to end on this repository. Written 2026-10-09 against
12
+ `analyzers/ontology-laptop-b`. Every claim below is either (a) measured on this machine in
13
+ this session, (b) read from the named file:line, or (c) produced by a focused review pass and
14
+ **re-verified by reading the code myself** before being written down. Where a claim was NOT
15
+ verified, it says so.
16
+
17
+ Method: the four review passes (discovery/scope, worker pool, store/DB, derived phases) each
18
+ read the real code paths and cited `file:line`; two of them additionally *executed* the real
19
+ functions (the discovery reviewer imported the actual `discoverIndexableFiles` and ran it
20
+ against synthetic fixtures; the pool reviewer reproduced Node's IPC behaviour with real child
21
+ processes). I then re-read the load-bearing findings myself. Measurements come from the
22
+ project's own `brain_index_status.performance_stats`, `dbstat`, and the run logs committed in
23
+ `validation/m60/results/` and `.tmp-probe/logs/`.
24
+
25
+ ---
26
+
27
+ ## 1. The measured baseline
28
+
29
+ Same command (`bb index . --force --no-setup`), same machine, before and after the recent work:
30
+
31
+ | | files | index time | whole command | worker kills | errors |
32
+ |---|---|---|---|---|---|
33
+ | before | 11,045 | 9,069 s (151 min) | — | 99, in bursts of 7 | 231 + 118 timeouts |
34
+ | after | 5,086 | 443.3 s (7 m 7 s) | 911 s (15.1 min) | 0 | 0 |
35
+
36
+ Phases recorded by the run itself (`brain_index_status.performance_stats`, last run):
37
+
38
+ ```
39
+ discoveryMs 620 parseMs 372,264 (6.2 min)
40
+ workerStartupMs 241 dbFlushMs 404,176 (6.7 min) <-- now the largest
41
+ finalAccountingMs 490 postProcessingMs 67,417 (1.1 min)
42
+ totalWallMs 441,852 (7.4 min)
43
+ ```
44
+
45
+ **The bottleneck moved from parsing to storing.** `dbFlush` now exceeds `parse`, and the two
46
+ overlap (their sum 776 s > wall 442 s). Store throughput implied: 687,099 symbol+relationship
47
+ rows written in 404 s ≈ **1,700 rows/s**. That is the number any further "blazing" claim has to
48
+ beat, and it is the reason §3 (space) and §6 (store path) matter more than parser work now.
49
+
50
+ ---
51
+
52
+ ## 2. Time complexity, phase by phase
53
+
54
+ | Phase | Cost model | Evidence |
55
+ |---|---|---|
56
+ | discovery/walk | O(F) stat + one `readdir` per directory; O(F²/B) for dispatch (see §5.13) | 620 ms for 5,086 files |
57
+ | parse (7 workers) | O(Σ parse cost per file); dominated by the largest files, not by F | 372 s |
58
+ | store (parent, synchronous) | O(S + R) rows, each with 18 index objects on `brain_relationship` (§3) | 404 s at ~1.7k rows/s |
59
+ | resolve | **linear plan**: three per-name aggregate temp tables + one indexed lookup per relationship. Fan-out 0.51 → 0.56 across 47→196 files (bound 1.5×) — but see §7.2: the reported "largest intermediate" excludes the plan's biggest table | 27,471 ms for 496,886 rels |
60
+ | enrich / gravity / taint / health | one pass each over stored relationships | 13,875 ms / ~0.5 s / 278 ms / 163 ms at 5,086 files |
61
+ | embeddings | O(embeddable symbols) network calls, background, capped | 24,502 queued vs a 25,000 cap; 2,283 stored; 7,014 still pending after a 300 s bounded wait |
62
+
63
+ **The gate's tiers do not cover production scale.** `script/perf/verify-scaling-linear.ts`
64
+ measures at 47/97/196 files and passes with 1.11× fan-out growth and 1.48× ms/rel growth
65
+ (bounds 1.5× / 2×). The production run resolves at **0.0553 ms/rel** — 2.6× the 0.0212 ms/rel
66
+ measured at 47 files, i.e. growth beyond what the 2× bound is tested over, across 108× the file
67
+ count. This is not proof of a superlinear algorithm (the per-rel cost also carries terms that
68
+ shrink relative to R), but it *is* a measurement the gate does not see, and a tier at ~1,000+
69
+ files belongs in it.
70
+
71
+ ---
72
+
73
+ ## 3. Space complexity
74
+
75
+ The database is **3,177.4 MB** for 5,086 files / 190,213 symbols / 496,886 relationships /
76
+ 27,280 embeddings. `dbstat` attribution:
77
+
78
+ | Object | Bytes | Note |
79
+ |---|---|---|
80
+ | `brain_semantic_embedding` | **891.2 MB (28%)** | 27,280 rows = **34,254 B/row on disk** |
81
+ | `brain_relationship` + its **17** index objects | **1,077 MB** | table 328.5 MB, indexes **748.5 MiB** |
82
+ | `brain_symbol` + indexes | ~200 MB | `source_text` 23.9 MB over 36,946 rows (avg 132 B, cap 4,000) |
83
+ | free pages | **857.7 MB (27%)** | awaiting the next `--force` (VACUUM needs the write lock) |
84
+
85
+ **3.1 The vector is stored twice.** Every one of the 27,280 embedding rows has BOTH columns:
86
+
87
+ ```
88
+ embedding_vector (text) 662.1 MB = 25,450 B/row <- 2048 floats as JSON text
89
+ # NOTE (2026-11-01): this duplication is FIXED — dense vectors are stored once, as float32; the
90
+ # JSON column is empty for dense rows. Measured after the change on apollo-server: DB 84.5 -> 53.3 MiB
91
+ # with SUM(LENGTH(embedding_vector)) = 0. See validation/m14/verification/INDEXER-P1-STORAGE.md.
92
+ embedding_blob (blob) 209.5 MB = 8,053 B/row <- the same 2048 floats as float32
93
+ needed for float32 2048-d: 8,192 B
94
+ rows with BOTH populated: 27,280 of 27,280
95
+ ```
96
+
97
+ So ~662 MB of the database — 21% of the whole file — is a second copy of vectors that already
98
+ exist as blobs. `src/brain/database.ts` comments that the blob is written for every dense vector
99
+ "so fresh indexes never need this [backfill]", which makes the text column pure duplication on
100
+ the write path as well: it is serialised, stored, indexed and read by nothing that needs it.
101
+
102
+ **3.2 Indexes cost more than the data they index.** `brain_relationship` carries **17** B-tree
103
+ objects (16 named indexes + the PK autoindex) for 496,886 rows: **748.5 MiB of indexes over a
104
+ 328 MiB table (2.3×)**. The five largest alone (`autoindex_brain_relationship_1` 93.9,
105
+ `brain_rel_proj_type_target_idx` 81.0, `brain_rel_proj_target_idx` 77.5,
106
+ `brain_rel_proj_source_idx` 74.8, `brain_rel_target_type_idx` 58.0 MB) exceed the table. Every
107
+ insert maintains all 17 — this is the mechanical reason `dbFlushMs` is now the largest phase.
108
+ **Only three of the 17 are read during the load** (PK autoindex for dedup, `brain_rel_source_idx`,
109
+ `brain_rel_target_idx`), so **549 MiB of index is deferrable** to the end of a run; for
110
+ `brain_symbol`, 10 of 12 are deferrable (127 MB). The constraint is that deferral must be
111
+ implemented *inside* the run: `ensureHealthIndexes` recreates 8 core indexes on **every open** of
112
+ an existing database (`schema-ddl.ts:714-731` via `database.ts:85`) and `ensureQueryShapeIndexes`
113
+ recreates the gravity index (`:604-613`).
114
+
115
+ **3.3 Parent memory scales with the whole run, not the pool.** The parent retains every file's
116
+ full result — including per-symbol snippet text — in `results` for the entire run
117
+ (`parallel-indexer.ts:1490`, pushed on every outcome, cleared only after the pool resolves at
118
+ `:5844`; `symbols` carry snippets of up to 10,000 chars, `pool-worker.ts:1047`). Measured by the
119
+ pool reviewer with real child processes: while the parent was blocked in a synchronous store, a
120
+ child's RSS climbed 75 → 120 → 215 → 306 MB sending 300 × 1 MB messages, and the parent then
121
+ allocated 399 MB receiving them. There is no IPC high-water mark in either direction
122
+ (`safeSend` discards `process.send`'s boolean, `pool-worker.ts:94-103`). For a 1M-symbol project
123
+ this is the failure mode that ends the run, and it is a *space* bound driven by total symbols.
124
+
125
+ ---
126
+
127
+ ## 4. Discovery and scope — what is indexed versus what exists
128
+
129
+ Measured against `git ls-files` (12,699 tracked) and the real index (5,086 files):
130
+
131
+ | tree | tracked | indexed | coverage |
132
+ |---|---|---|---|
133
+ | `src` | 209 | 205 | **98.1%** |
134
+ | `script` | 4,297 | 3,906 | 90.9% |
135
+ | `docs` | 291 | 288 | 99.0% |
136
+ | `test` | 42 | 42 | 100% |
137
+ | `website` | 124 | 92 | 74.2% |
138
+ | `ide` | 36 | 32 | 88.9% |
139
+ | `validation` | 6,580 | 214 | 3.3% (`.bbignore`, deliberate) |
140
+ | `grammars` | 286 | 0 | 0% (`.bbignore`, deliberate) |
141
+ | `worktree-snapshots` | 286 | 0 | 0% (`.bbignore`, deliberate) |
142
+ | `.tmp-probe` | 144 | 0 | 0% (`.bbignore`, deliberate) |
143
+
144
+ Findings, all read from the code (the discovery pass additionally executed the real walk):
145
+
146
+ 1. **CRITICAL — directory-name skips beat every ignore file and cannot be re-included.**
147
+ `shouldSkip("/" + relativePath)` runs at `parallel-indexer.ts:1033`, *before*
148
+ `localIg.ignores(...)` at `:1035`, and `shouldSkip` is a bare
149
+ `skipPatterns.some(p => p.test(path))` (`:636-638`) with no negation. The patterns include
150
+ whole segment names: `env` (`:270`), `bin` (`:298`), `out` (`:274`), `deps` (`:293`),
151
+ `gen`/`generated` (`:312-313`), `platforms` (`:307`), `build` (`:273`), `target` (`:275`),
152
+ `perf` (`:337`). Verified by running the real walk: `src/env/config.ts`,
153
+ `src/bin/tool.ts`, `src/out/box.tsx`, `src/gen/api.ts` are all absent — and no `.bbignore`
154
+ line can bring them back. Rust `src/bin/*.rs`, `packages/ui/src/out/Box.tsx` and
155
+ `src/deps/container.ts` are permanently invisible.
156
+ 2. **HIGH — `.mts` and `.cts` are never indexed.** `EXTENSION_LANGUAGE`
157
+ (`language-detection.ts:20-103`) maps `ts, tsx, js, cjs, mjs, jsx` and has no `mts`/`cts`
158
+ (verified myself). **490 tracked files** in this repository are `.mts`, including much of the
159
+ gate suite (`script/m14/clean-*.mts`, `script/m12/xml-*.mts`). Same gate also drops `.hh`,
160
+ `.tpp`, `.ipp`, `.inl`, `.inc`, `.csproj`, `.fsproj`, `.xaml`, `.plist`, `.wsdl`, `.pom`,
161
+ `.conf`, `.ini`, `.cfg`, and hand-written `.d.ts` (`:320`) — the last of which *routes* but is
162
+ then skipped. `ParallelIndexerConfig.extensions` (`:145-251`) is dead code: nothing reads it,
163
+ and it disagrees with the map that actually admits files.
164
+ 3. **HIGH — the 1–5 MB tier reads only the first 256 KB, silently.** `pool-worker.ts:920-935`
165
+ allocates `min(size, 256 KB)`, hashes the *truncated* text, and returns
166
+ `extractFallback(...)`. The "Using structural extraction for large file" message goes to
167
+ `function log(..._args: unknown[]) {}` (`pool-worker.ts:36`) — **a no-op logger** (verified).
168
+ Worse, the read-side freshness check hashes the whole file (`freshness.ts:17,323`), so any
169
+ metadata change makes such a file report permanently `hash-differs` with a reindex
170
+ remediation that cannot clear it.
171
+ 4. **HIGH — symlinks are never followed.** `isSymbolicLink` appears zero times in `src`; the walk
172
+ only consults `isDirectory()`/`isFile()` (`:1006`, `:1038-1040`). Verified: a symlinked file
173
+ and a symlinked directory are both absent (and a cyclic link cannot hang — it is never
174
+ entered).
175
+ 5. **HIGH — nested ignore files are flattened, so anchored patterns are inert.** Child
176
+ `.gitignore`/`.bbignore` files are added into one root `ignore()` instance (`:976-997`) but
177
+ tested with root-relative paths (`:1035`). Verified: `sub/.gitignore` containing `/build2/`
178
+ and `/anchored.ts` left both files indexed.
179
+ 6. **HIGH — submodules and worktrees are indexed as first-party.** The nested-repo check tests
180
+ `e.isDirectory() && e.name === ".git"` (`:972-974`); a submodule or worktree writes `.git` as
181
+ a FILE, so the subtree is walked (verified) and its symbols enter the graph.
182
+ 7. **HIGH — a file that grows past 5 MB keeps its old rows and is reported "unchanged".**
183
+ `foundFiles.add()` (`:918`) happens before the >5 MB return (`:921-925`), and stale cleanup
184
+ only removes paths absent from `foundFiles` (`:1088-1089`); `skippedFiles++` is then reported
185
+ as `unchanged` (`:5719`; CLI prints it at `cli/index.ts:416`).
186
+ 8. **HIGH — no stable ordering, and it reaches a page.** Nothing sorts the walk (`readdir` →
187
+ `await stat` → push, `:944-955`), and `symbol-search.ts:323,327` applies `LIMIT` with **no
188
+ ORDER BY**, so which rows survive a candidate cap is insertion order. Symbol IDs are
189
+ content-addressed, so stored rows are reproducible; the *page* is not.
190
+ 9. **HIGH — the in-process (no-fork) mode silently truncates.** `FULL_AST_CUTOFF = 13000`,
191
+ `MAX_FILES_IN_PROCESS = 14000` (`:1169-1174`): files past 14,000 are never parsed, files past
192
+ 13,000 are regex-extracted, and the summary's arithmetic cannot account for them
193
+ (`:5324`, residual at `:5720`); the CLI never prints `byLanguage` or fallback counts.
194
+ 10. **HIGH — no duplicate-content detection.** Identity is the path
195
+ (`INSERT OR REPLACE INTO brain_file` keyed on `projectId:relativePath`, `:2752-2757`); the
196
+ hash column is a *non-unique* index (`brain.sql.ts:29,48,52`) and `fileHash` is never used to
197
+ dedupe. A copied or vendored file is stored twice — duplicate symbols, duplicate embeddings,
198
+ and `bb_edit`'s "symbol is ambiguous" path becomes reachable.
199
+ 11. **HIGH — read-time exclusions hide files the index deliberately kept.** `NOISE_DIR_SEGMENTS`
200
+ (`analysis-exclusions.ts:7-21`) hides `fixtures`, `vendor`, `snapshots`, `baselines`,
201
+ `generated`, `gen` from every read surface while the walk indexes them. The reviewer
202
+ demonstrated it live in this repo: `bb_search mode=file` returns
203
+ `script/fixtures/contract-fixture.ts` as indexed and fresh, while a symbol search for its
204
+ export `createContractFixture` answers "no results … scan was cut short (noise-filter)" —
205
+ despite three tracked scripts importing it.
206
+ 12. **MEDIUM — data formats are admitted by name, bounded only by size.** json/yaml/toml/xml/
207
+ svg/md/sql/graphql all route; lock files are named one by one (`:322-325`) so `Cargo.lock`
208
+ and `Gemfile.lock` are excluded by accident while `poetry.lock`/`Pipfile` are indexed as
209
+ TOML. Measured in this repo before the `.bbignore` work: json was 78.8% of all symbols.
210
+ 13. **MEDIUM — three more walks with divergent skip lists.** `fingerprint.ts:79-80` uses the union
211
+ of `skipPatterns` + `cliSkipPatterns` (which uniquely has `snapshot.json$`), so
212
+ `computeCheckoutFingerprint` can never equal `computeDbFingerprint` for a tree containing
213
+ such a file — "moved project" adoption never fires. The third list was `collectDirChanges`'
214
+ `DIR_WALK_SKIP_DIRS`, which included `vendor` that discovery deliberately does *not* skip;
215
+ that divergence is removed (see finding 14) and the deletion rule no longer depends on walk
216
+ absence at all.
217
+ 14. **FIXED (was HIGH) — `bb_index mode=dir` deleted what its own walk did not see.** The walk's
218
+ absence used to be treated as deletion: every stored path under the prefix that the walk did
219
+ not return became `{type:"deleted"}` and was purged. `vendor` was in the walk's skip set while
220
+ discovery deliberately indexes it, so editing one file in a tree containing `vendor/` silently
221
+ purged that code's symbols and reported a normal-looking `deleted: N` — measured on an isolated
222
+ fixture as files 3→1. Two rules now hold: a row is marked deleted ONLY when the file is
223
+ provably absent from disk (stat ENOENT), and the walk's skip set matches discovery's (`vendor`
224
+ walked). Proof: `validation/tools/bb_index/audit/door-vendor.json` (before: deleted 2, files
225
+ 3→1) versus `audit/door-vendor-after-fix.json` (after: deleted 0, files 3→3), and the mutant in
226
+ `script/m40/verify-a3-write-parity.ts` (`BB_M40_MUTATE_A3_WALK_DELETE=1`) goes red.
227
+ 15. **MEDIUM — explicit-file indexing bypasses ignore files.** `:5304-5305` filters only
228
+ `shouldSkipCli` + `hasValidExtension`; `:6993-6994` only `hasValidExtension` + `shouldSkip`.
229
+ A gitignored file added by path enters the index and is purged again by the next full run.
230
+ 16. **MEDIUM — stored text is capped twice, silently**: 10,000-char snippets
231
+ (`pool-worker.ts:1047`) and `source_text` at 4,000 chars / 200 lines (`:733-751`, applied
232
+ `:3698`). Content search cannot find a term that appears late in a large symbol.
233
+ 17. **MEDIUM — no binary-content detection.** Any name-routed file is parsed; the only content
234
+ probe is the 256-byte shebang check for extensionless files (`:692-710`). A misnamed binary
235
+ becomes a symbol-less `brain_file` row counted as `symbolLoss`.
236
+ 18. **LOW — name rules catch hand-written files**: `.*-min\.` and `.*\.min\.` (`:315-316`) exclude
237
+ `admin-min.ts`; `.*\.map$` (`:319`) excludes any `foo.map`; depth > 50 returns silently
238
+ (`:967`); `localIg.ignores` is written twice (`:1035-1036`) and `/obj/` twice
239
+ (`:1022-1023`); the inline compiled-mode list (`:1013-1028`) is redundant with
240
+ `skipPatterns` + `rootIg` and is inert at root level.
241
+
242
+ ---
243
+
244
+ ## 5. The worker pool
245
+
246
+ Architecture: the parent forks workers; workers parse and `process.send` results; **the parent is
247
+ the only process that touches SQLite**, synchronously. Verified findings:
248
+
249
+ 1. **HIGH — no `error` listener on pool workers, so a send to a dying worker can kill the parent.**
250
+ The only `.on("error")` in 7,083 lines is on the fork *probe's* throwaway worker (`:5486`) —
251
+ verified myself. `child.send()` to a closed channel emits an unhandled `'error'`
252
+ asynchronously, so the `try/catch` at `:2034-2064` does not catch it. The reviewer reproduced
253
+ both orderings on Node v22.22.1 with real children: after the `exit` event, send returns false
254
+ *and* the process exits 1 with `ERR_IPC_CHANNEL_CLOSED`; in the pool's actual ordering — the
255
+ parent blocked 400 ms across the child's death, with `exitCode===null, killed===false,
256
+ connected===true` (exactly the state the guard reads as alive) — send returns **true** and the
257
+ write fails with EPIPE, exiting 7. This is the same failure class the comment at `:1579-1606`
258
+ claims to have fixed. One line fixes it: `worker.on("error", …)` at fork time.
259
+ 2. **HIGH — a worker that dies before `ready` bypasses the restart cap and forks forever.**
260
+ The `exit` handler calls `clearStartupTimer(worker)` first (`:1881-1883`), so the startup
261
+ watchdog can never fire for a fast death; `startupFailures` is incremented only at `:1739`,
262
+ `:1754`, `:1775`, `:1934` — never on `exit` (verified). With `MAX_STARTUP_FAILURES` never
263
+ reached, the replacement branch (`:1899-1916`) loops fork → die → fork with nothing parsed and
264
+ nothing reported. The *silent*-worker variant is capped; that is the only case the existing
265
+ fixture covers.
266
+ 3. **MEDIUM — no IPC flow control; the parent retains the whole run in memory** (see §3.3).
267
+ 4. **MEDIUM — an engine-failure worker is pooled twice.** `pool-worker.ts:1528-1530` sends
268
+ `engine_fail` and then `:1545-1554` sends `ready` unconditionally; the parent pushes on both
269
+ (`:1812-1830`) and dispatch shifts from `idleWorkers` without a membership check (`:2003-2015`),
270
+ so one process can run two batches — halving effective parallelism on exactly the regex-fallback
271
+ path, against unchanged 45 s budgets.
272
+ 5. **MEDIUM — the startup watchdog has no stall deferral.** `decideWatchdog` is applied only to
273
+ per-file timeouts (`:1619-1638`); the startup timer kills on wall clock with the sole test
274
+ `workers.has(worker)` (`:1769-1777`), so a parent block longer than the startup timeout can
275
+ SIGKILL a worker that already sent `ready`, and each such kill advances `startupFailures`
276
+ toward failing the remaining queue outright.
277
+ 6. **MEDIUM — `processed` counts attempts, not files.** Ten `processed++` sites (verified: `:1187`,
278
+ `:1232`, `:1655`, `:1669`, `:1684`, `:1696`, `:1730`, `:1856`, `:1862`, …), including timeout,
279
+ poison-drop and retry paths; the percentage is `processedFiles/totalFiles*98`
280
+ (`:5536`) with no clamp, and `filesPerSecond` uses the same counter (`:5855`). A run with
281
+ retries can therefore report >100%.
282
+ 7. **LOW — a `file_done` for an already-killed batch is still accepted** (`:1838-1857` never
283
+ consults `pendingBatches`, unlike `file_start` `:1832` and `batch_done` `:1865`), so a file can
284
+ be parsed and stored twice after a timeout kill. No data loss (`INSERT OR REPLACE`); CPU and
285
+ counters suffer.
286
+ 8. **LOW/MEDIUM — a dispatch into a dead-but-unreaped worker is dropped by the kernel**, and the
287
+ ensuing timeout blames `batch.files[0]` as a poison pill (`:1678-1704`). The end-of-run DB pass
288
+ usually rescues it (`:5740-5748`) — but that pass is skipped on a cancelled run (`:5737-5740`).
289
+ *Positive:* no reachable path leaves a file with zero result entries, and the batch retry loops
290
+ are bounded (`MAX_FILE_RETRIES = 3`, `:1557-1558`, checked on every requeue).
291
+ 9. **MEDIUM — `new Promise(async (resolve) => …)` has no catch** (`:1489`): a synchronous throw
292
+ from `fork()` (EMFILE/ENOMEM) leaves the pool promise pending forever, with no diagnostic and
293
+ live children holding IPC handles.
294
+ 10. **LOW — pool size ignores cgroup/affinity limits.** `Math.max(2, Math.min(cpus().length - 1, 12))`
295
+ (`:125`) gives **2 workers on a 1-core machine** (`min(0,12)=0` → `max(2,0)`), and a 2-CPU
296
+ container on a 64-core host reports 63 → caps at 12. `os.availableParallelism()` is
297
+ affinity-aware; `cpus()` is not. There is no CLI or env override for concurrency.
298
+ 11. **LOW — an empty file list still forks a worker** (`:1387` `|| 1`, then `:2068-2070`), costing
299
+ one Node/tsx boot (0.55–3.4 s) on a no-op run.
300
+ 12. **LOW — a >5 MB file is recorded as `success: true` with zero symbols** and an empty-string
301
+ hash (`pool-worker.ts:897-916`); it is only revisited if mtime/size change.
302
+ 13. **INFORMATIONAL — dispatch is O(F²/B) worst case**: `fileQueue.splice(0, batchSize)` copies the
303
+ remaining tail per dispatch (`:2015`), and retries `unshift` (`:1674`, `:1701`, `:2058`).
304
+ Irrelevant below ~100k files; the dominant term is memory (§3.3).
305
+ 14. **NO DEFECT — dispatch is work-conserving.** Every event that can idle a worker calls
306
+ `scheduleNext()` synchronously, and there is deliberately no timer-based dispatch
307
+ (`:1911-1913`) because a blocked loop would never fire it.
308
+
309
+ ---
310
+
311
+ ## 6. The store path and the database
312
+
313
+ `storeResults` (`parallel-indexer.ts:2727`) prepares **13 statements once per call** and executes
314
+ one `.run()` per row — no multi-row `VALUES` anywhere, though better-sqlite3 allows 32,766
315
+ variables. One transaction per call (`:3003`, invoked `:4326`, default `BEGIN DEFERRED`; the first
316
+ statement per file is a DELETE, so the write lock is taken at the top). Per file, in order: six
317
+ DELETEs (`:3059-3066`) → `insertFile` (`:3163`) → the symbol loop (`:3654`) → the relationship
318
+ loop (`:3984`/`:4281`).
319
+
320
+ **6.1 Batch sizes have drifted apart.** `workerBatchSize` is 10 files per IPC batch (`:136`); the
321
+ pool's DB flush threshold is **hardcoded 20** (`:1850`); `FLUSH_CHUNK_FILES` is 20 (`:1453`);
322
+ and `dbBatchSize: 100` (`:139`), documented as the bulk-insert bound, **never binds in the
323
+ worker-pool path** because `storeResults` receives at most 20 files per call. So the pool commits
324
+ more often, and writes more WAL frames, than the configuration implies.
325
+
326
+ **6.2 A SELECT is executed PER RELATIONSHIP, and it sorts every candidate.** Call site
327
+ `:4119-4122`; statements `resolveTargetStmt` (`:2856-2862`) and `resolveContainsTargetStmt`
328
+ (`:2971-2977`), both shaped:
329
+
330
+ ```sql
331
+ SELECT s.id, s.file_id, f.language FROM brain_symbol s
332
+ LEFT JOIN brain_file f ON f.id = s.file_id
333
+ WHERE s.project_id = ? AND s.name = ?
334
+ ORDER BY COALESCE(s.is_declaration,0) ASC, (s.file_id = ?) DESC, (f.language = ?) DESC, s.id
335
+ LIMIT 1
336
+ ```
337
+
338
+ `EXPLAIN QUERY PLAN` on the real index reports `SEARCH s USING INDEX
339
+ brain_sym_proj_name_type_idx` … **`USE TEMP B-TREE FOR ORDER BY`** — every edge materialises and
340
+ sorts *all* same-named candidates. Measured per query: **0.146 ms at 0 candidates → 0.229 at 5 →
341
+ 0.663 at 55 → 6.506 at 553 → 11.538 at 2,010 → 21.342 ms at 3,159**. Distribution across the
342
+ 496,886 edges (proxy: `target_external`): **26,838 edges see ≥1,000 candidates**, 67,814 see
343
+ 100–999, 115,083 see 10–99, 162,440 see 1–9, 124,711 see none. The same information is already in
344
+ memory (`nameToSymbol`, `:3246`, `:3367-3391`).
345
+
346
+ **Honest bound on this finding.** The additive estimate from those buckets (~14.7 min) *exceeds*
347
+ the measured 404 s store phase, so the `target_external` proxy over-counts (about 20% of targets
348
+ are qualified, and the leaf differs). The 404 s is the real ceiling for the sum of this SELECT,
349
+ 17-way index maintenance and the per-file DELETEs; the split among them is **not measured** and
350
+ would need instrumentation.
351
+
352
+ **6.3 Failure semantics are the dangerous part.**
353
+
354
+ 1. **CRITICAL — a full disk is invisible.** There is no `SQLITE_FULL`/`ENOSPC` path anywhere in
355
+ `src/`. A failing commit is caught, rolled back and **skipped** (`:4330-4343`); the run reaches
356
+ its status write and summary, whose counts are re-read from the database
357
+ (`:5847-5849`; `cli/index.ts:299-302`). The user sees `✓ indexed N files…` with N silently
358
+ smaller.
359
+ 2. **HIGH — a broken index can be persisted as `complete`.** `status = errorFiles > 0 ? "partial"
360
+ : "complete"` (`:5906`), but `errorFiles` only counts *parse* outcomes (`:5694`, `:5812`); a
361
+ store rollback is written to stderr and nothing else (`:4335`, `:1971-1976`).
362
+ 3. The end-of-run checkpoint fires **before** the embedding drain finishes (`cli/index.ts:303-307`
363
+ vs the drain awaited at `:317-333`), so "the writes have stopped here" is false and the WAL
364
+ regrows after the `index space:` line.
365
+ 4. The force-wipe is **not transactional**: nine autocommit DELETEs with `foreign_keys = OFF`
366
+ (`:6168-6194`). A crash mid-wipe leaves a partially wiped index that nothing marks suspect.
367
+ 5. Corruption surfaces as one opaque driver line (`db.node.ts:22-23` → `cli/index.ts:1288-1292`),
368
+ and additive migrations are blanket `catch {}` (`schema-ddl.ts:569-576, 615-622, …`), so a
369
+ corrupt database can be silently left un-migrated.
370
+
371
+ **6.4 Pragmas are minimal and the temp store is on disk.** Only `journal_mode=WAL`,
372
+ `busy_timeout=5000`, `synchronous=NORMAL`, `foreign_keys` are set (`db.node.ts:29-35`).
373
+ `cache_size`, `mmap_size`, `temp_store` and `journal_size_limit` are never set, so the driver's
374
+ compiled defaults apply: **16 MB page cache, mmap off, `TEMP_STORE=1` (file-backed), 4 MB
375
+ auto-checkpoint**. For a 3.3 GB database whose store path forces a temp sort per edge, that is the
376
+ un-tuned part of the load — and it is why the `USE TEMP B-TREE` in 6.2 lands on disk.
377
+
378
+ **6.5 Concurrency and locking are mostly right, with one hole.** A second indexer is refused
379
+ immediately by an `O_EXCL` lockfile naming the holder (`index-lock.ts:96-122`, taken before the
380
+ `--force` wipe at `cli/index.ts:154-166`). The in-place wipe is verified to keep a live MCP
381
+ server's cached handle valid — and VACUUM is verified to work while another process holds the
382
+ database open (it needs the *write* lock, not exclusivity). The hole: **the MCP's own writers hold
383
+ no index lock** — the background embedding drainer (`:2496-2690`) and annotations — so they contend
384
+ only through `busy_timeout=5000`, and a lost embedding chunk is counted `failed` and downgraded to
385
+ tf-idf (`:2648-2656`).
386
+
387
+ **6.6 FIXED — reclamation ran on no path, not just the CLI one.** `index-space.ts` issued its
388
+ PRAGMAs through `rawDb.pragma`, but every caller holds the `CoreDatabase` wrapper, which exposes
389
+ `native`/prepare/exec and no `pragma` — so `checkpointWal` threw inside its own try/catch and
390
+ returned `false` on every runtime, while `spaceReport` read a zero freelist. The MCP `bb_index`
391
+ path additionally never called it at all. Both are fixed: PRAGMAs are issued through the driver's
392
+ own method (or a prepared statement), and a successful reindex now runs a WAL checkpoint and
393
+ reports `walCheckpointed`. Evidence: `validation/tools/bb_index/audit/door-t0-postfix.json`
394
+ (`walCheckpointed: true`) and `script/perf/verify-space-reclaim.ts`.
395
+
396
+ ---
397
+
398
+ ## 7. Derived phases, and where the candidate cost actually went
399
+
400
+ The bulk plan really is the linear one, and it is measured. `stats = { plan: "linear", … }`
401
+ (`:4939`); the plan builds `_i13_src` (`:4827`), `_i13_tier` (`:4854`), `_i13_best_file` (`:4865`),
402
+ `_i13_best_lang` (`:4877`), `_i13_best_any` (`:4888`), `_i13_pick` (`:4910`) and applies with one
403
+ `UPDATE … FROM` (`:4945-4951`). `ROW_NUMBER`/"window" survives only in probes. Measured on the real
404
+ index (496,886 relationships, 190,213 symbols): `_i13_src` 1,600 ms · `_i13_tier` 155 ·
405
+ `_i13_best_file` 304 · `_i13_best_lang` 381 · `_i13_best_any` 173 · `_i13_pick` 2,033 ·
406
+ **apply 33,433 ms**. The apply dominates because it rewrites rows in a table carrying 17 indexes.
407
+ What makes the shape safe is the unique index on `_i13_src(rel_id)` (`:4852`) — without it the plan
408
+ degenerates to a scan per relationship (measured 15.6 s vs 0.060 s).
409
+
410
+ **7.1 The candidate cost did not disappear — it moved into the store.** `resolveTargetStmt` and
411
+ `resolveContainsTargetStmt` (`:2856-2862`, `:2971-2977`) run **once per relationship at insert
412
+ time** (`:4119-4122`), and that call site is **not** inside the `if (!options.skipResolution)`
413
+ block (which spans `:4368-4555`). Verified: the pool path stores every batch with
414
+ `skipResolution: true` (`:1465`) and still pays the per-edge SELECT for all 496,886 edges. So the
415
+ complexity is O(R × C log C), and it lands in `dbFlushMs` (404 s of the 442 s run), not in
416
+ `postProcessingMs`. Measured C on the live index: **average 3.66, max 3,159; 237 names have ≥100
417
+ candidates and 14 have ≥1,000**. This is the single largest algorithmic term in the indexer today,
418
+ and it is exactly the term the linear plan was written to remove from the other pass.
419
+
420
+ **7.2 The gate cannot see the plan's biggest intermediate.** The reported value is
421
+ `MAX(c)` over `_i13_tier`, `_i13_best_file`, `_i13_best_lang`, `_i13_best_any` and `_i13_pick`
422
+ (`:4930-4938`) — it **excludes `_i13_src`**, which is R rows (496,886 here against a reported
423
+ 246,909). The comment above it says "the largest intermediate it materialised, counted from the
424
+ tables it built", and `verify-scaling-linear.ts` divides this number by R to compute the clock-free
425
+ fan-out check. A table of R rows is dropped from a metric whose purpose is to bound the largest
426
+ intermediate, so the gate's headline number under-states it by roughly 2× on this corpus.
427
+
428
+ **7.3 Resolution is 6–8 passes over R, not one.** Between them: the unresolved `COUNT`
429
+ (`:4602-4604`), two strategy `UPDATE`s (`:4641`, `:4655`), the dotted-target update (`:4443`), the
430
+ import resolver (`:4690`), `_i13_src` (`:4827`), `_i13_pick` (`:4910`), the `is_cross_file` recompute
431
+ (`:4956-4966`) and cross-language normalisation with two correlated `EXISTS` per resolved row
432
+ (`:4979-5006`, measured 11.1–13.7 s for 496,886 rows).
433
+
434
+ **7.4 The other derived phases are sound.** Enrichment is O(R) keyset pages with one PK update per
435
+ row (`brain-relationship-enrichment.ts:180-728`); gravity is O(S log R) with two indexed probes per
436
+ symbol plus five whole-table updates (`:43-117`, measured 3.4–4.1 s for 190k symbols); health is
437
+ four aggregates (`project-health.ts:48-169`); taint is one CTE plus per-flow updates. **No derived
438
+ phase is O(R×N).** Two blemishes: taint's `LIMIT 10000` (`:5071`) truncates without a truncation
439
+ flag, and the function swallows query errors and returns 0 (`:5147-5150`), so a failed taint pass is
440
+ recorded as an "ok" phase outcome.
441
+
442
+ **7.5 The plan's largest intermediate is on disk.** No `PRAGMA temp_store` is ever set
443
+ (§6.4), so `_i13_src` — R rows — is a **file-backed** temp table, and the per-edge `ORDER BY` in
444
+ §7.1 sorts through disk-backed temp storage too. On a 3.3 GB database with a 16 MB page cache, that
445
+ is the un-tuned part of every sort in this review.
446
+
447
+ **7.6 ANALYZE runs once, late, and stale stats are never repaired.** `ANALYZE` runs at the end of
448
+ `runPostProcessing` (`:6472-6480`) — not in the bounded incremental pass (`:6666-6842`), not when
449
+ the run is cancelled (`:5916-5917`), and not with `--skip-post-processing`. `ensurePlannerStats`
450
+ (`schema-ddl.ts:753-764`) only fires when `sqlite_stat1` is **absent**, on the pre-existing-DB
451
+ branch, so present-but-stale statistics are never refreshed on the read path and nothing reports
452
+ that a project maintained by `--changed`/`--resume`/`bb_edit` is planning on statistics that
453
+ describe an older database. The three documented blowups from missing stats (`_i13_src` 260×,
454
+ taint 600×, graph owner-cascade 200–400×) are data-skew dependent; the reviewer reproduced the plan
455
+ changes, not the blowups.
456
+
457
+ **7.7 `--skip-post-processing` can report a healthy index that never had a derived pass.** The run
458
+ persists `status: "complete"` (`:5900-5910`, `:6045-6056`) although nothing derived ran, and
459
+ `bb_health`'s "Index Completeness" check reads exactly that row (`brain-index-health.ts:287-360`) —
460
+ so a deferred index can read HEALTHY. The CLI's own "consistency" line counts only rows with no
461
+ external identity on either end (`:5194-5214`); the **34,646 rows on this repo that carry
462
+ `target_external` and a NULL target** are not counted, so it can print "every internal relationship
463
+ is attached to a symbol" over tens of thousands of unattached internal edges. The `--post` help
464
+ lists five phases; the code records eight (`:6353-6480`).
465
+
466
+ **7.8 Embeddings: capped by product design, invisible until the summary.** The queue cap is 25,000
467
+ (`BB_EMBEDDING_QUEUE_LIMIT`, `:2110`) and a single batch over `BB_EMBEDDING_BATCH_LIMIT` (50,000) is
468
+ dropped whole (`:2453-2457`). Overflow drops are logged at **debug** level only (`:2462`) and
469
+ surface solely in the end-of-run line "N dropped (queue cap)". The drainer is serial, chunked at 100
470
+ vectors, one transaction per chunk, with a tf-idf fallback for the rest (`:2662-2704`); it never
471
+ blocks the index but keeps the process alive (measured ~10 min of silent tail, now announced and
472
+ bounded). Two models in one index is **by design** (`:2143-2147`); search filters by the *query's*
473
+ model (`semantic-search.ts:191`) and falls back to keyword search with reason
474
+ `no-matching-model-embeddings`, so the other model's vectors are invisible rather than merged. The
475
+ summary warns about it (`cli/index.ts:368-380`).
476
+
477
+ ---
478
+
479
+ ## 8. Progress, ETA, and the messages a user reads
480
+
481
+ All from `src/cli/index-progress.ts` (313 lines) plus the indexer's own framing line.
482
+
483
+ 1. **The ETA is a cumulative average and ignores everything after parsing.**
484
+ `estimateRemainingMs` (`:116-124`) is `(elapsed / processedFiles) × remaining`, with a single
485
+ guard `elapsed < 1000 ms`. Consequences measured in this session's own logs: the ETA slid
486
+ 33m → 25m → 19m → **8m** → 25m → 14m → 20m as the file mix changed, and at `100%` it prints
487
+ `eta=0s` while the derived phases and the embedding tail still lie ahead — in the measured
488
+ run, files finished at 5 m 33 s and the command ended at 15.1 min, i.e. the ETA called it
489
+ done two-thirds of the way early. It also inherits the attempt-vs-file counter (§5.6), so a
490
+ run with retries can show >100%. A windowed rate and a phase-aware remainder (parse vs
491
+ derived vs embedding queue) would both be honest; today's number is not.
492
+ 2. **The size bands exist and are sensible — except at the small end.**
493
+ `expectationFor` (`:100-105`): 0 → "nothing to index — the index is already up to date",
494
+ <150 → "this is a small project — a few minutes at most", <4,000 → "this will take a few
495
+ minutes", ≥4,000 → "large project — this takes a while. Grab a coffee; bb keeps working in the
496
+ background". Measured verbatim on a 7-file project: *"Indexing 7 files — this is a small
497
+ project — a few minutes at most"* (it finished in ~2 s). The module docstring promises a
498
+ "a moment" band that does not exist in the code.
499
+ 3. **Encouragement fires at thresholds that are absurd on small projects**, and one branch is
500
+ dead. `encouragementFor` (`:52-64`) announces "a quarter of the way in" at ≥25% — measured at
501
+ **1 of 7 files**. Its last two lines (`if (isBigProject && pct < 10) return null` / `return
502
+ null`) are equivalent, so the branch the comment describes is unreachable.
503
+ 4. **The non-TTY milestone rate limit is dead code.** `renderNonTtyLine` (`:270-296`) computes
504
+ `dueByTime` from `nonTtyIntervalMs` but then returns whenever `bucket === lastNonTtyBucket`,
505
+ so wall-clock alone can never emit a line — the documented "AND by wall-clock, so neither a
506
+ very long run nor a stalled run floods the log" is not what the code does. This is also why a
507
+ 250-second parent stall produced *no* output at all in the logs (§2 of the perf work).
508
+ 5. **"Longer than we expect" fires on healthy runs.** `finish()` (`:298-313`) prints "That took
509
+ … — longer than we expect. If it felt like it hung, that is worth reporting" at ≥5 min
510
+ regardless of project size. Measured on the *healthy* 5,086-file run: "That took 7m 7s —
511
+ longer than we expect." The threshold should scale with the discovered file count (or the
512
+ wording should be neutral), otherwise the product tells users something is wrong precisely
513
+ when it is not.
514
+ 6. **What is good and should be kept**: the "nothing to index — already up to date (N unchanged,
515
+ 0 skipped, 0.2s)" path with its per-bucket breakdown; the `[index] <pct>% … elapsed=… eta=…`
516
+ non-TTY line shape; the one-line-per-5% behaviour; the fixed-width TTY bar; the honest
517
+ "Indexing N files — …" framing emitted once; and the summary's explicit
518
+ `0 newly parsed | 7 unchanged | 0 errors | 0 timeouts` accounting.
519
+
520
+ ---
521
+
522
+ ## 9. The flag surface
523
+
524
+ `bb index --help` exposes eight options: `--force`, `--no-setup`, `--skip-post-processing`,
525
+ `--post`, `--file <path>`, `--changed`, `--resume`, `--import <db>` (`cli/index.ts:4-11`).
526
+
527
+ 1. **`--exclude` does not exist, though the indexer documents it.** `parallel-indexer.ts:1073`
528
+ reads *"Load CLI --exclude patterns (comma-separated globs)"* and `excludePatterns` is
529
+ threaded through — but no CLI option sets it, so `.bbignore` is the only user-facing exclusion
530
+ mechanism (and per §4.1 it cannot undo a directory-name skip).
531
+ 2. **Concurrency is not overridable.** `ParallelIndexerConfig.concurrency` (`:125`) has no env or
532
+ CLI knob, so a user cannot cap workers on a loaded machine, ask for deterministic
533
+ single-worker runs, or raise it on a big host past the hard 12 cap.
534
+ 3. **`--force` keeps the file** (documented at `cli/index.ts:167-170`, deliberately, so a live MCP
535
+ server's handle stays valid). Space is reclaimed instead by the wipe-path checkpoint + VACUUM
536
+ (`src/ast/index-space.ts`), which now decides on rows deleted rather than on `freelist_count`.
537
+ 4. **No `--json` / `--quiet`**: the index command speaks human text only, so scripting it means
538
+ parsing prose. (`bb show`/`bb stats` cover some of this outside the index path.)
539
+ 5. The remaining flags match their documented behaviour, including `--resume` reading the live
540
+ checkpoint and `--changed` costing seconds on an unchanged tree (verified: 0.2 s, "nothing to
541
+ index").
542
+
543
+ ---
544
+
545
+ ## 10. What I would fix, ranked
546
+
547
+ **Correctness first — small diffs, large consequences:**
548
+
549
+ 1. `worker.on("error", …)` on pool workers: one line, prevents a whole-run death (§5.1).
550
+ 2. Count a pre-`ready` exit toward `startupFailures`: prevents an infinite fork loop (§5.2).
551
+ 3. **A failed store batch must fail the run.** Today a full disk rolls back batch after batch,
552
+ skips them, and still prints `✓ indexed N files…` (§6.3.1). Count the skips, persist
553
+ `partial`/`failed`, exit non-zero.
554
+ 4. A rolled-back batch must not persist `status: "complete"` (§6.3.2) — `errorFiles` only counts
555
+ parse outcomes today.
556
+ 5. **Resolve targets from memory instead of one sorted SELECT per edge.** `O(R × C log C)` sits on
557
+ the critical path in `dbFlushMs` (§7.1): the data is already in `nameToSymbol`, and the
558
+ `_i13_best_*` aggregates exist for exactly this ranking. This is the biggest algorithmic win
559
+ available, and it is the term the linear plan was written to remove from the other pass.
560
+ 6. `largestIntermediate` must include `_i13_src` (§7.2): the gate's clock-free check divides a
561
+ number that omits a table of R rows by R.
562
+ 7. Stop storing every vector twice — drop the JSON column's write, or the column (§3.1): ~660 MB
563
+ and a large share of embedding write time.
564
+ 8. `shouldSkip` needs negation, and `bin`/`env`/`out`/`gen`/`deps` must leave the *segment* list
565
+ (§4.1): four real source trees in this repository are invisible today and unfixable by any
566
+ ignore file.
567
+ 9. Add `.mts`/`.cts` to the language map and delete the dead `extensions` list (§4.2): 490 tracked
568
+ files in this repo.
569
+ 10. `--skip-post-processing` must not persist `complete`, and the "consistency" line must count the
570
+ edges that carry an external identity but no target — 34,646 of them here (§7.7).
571
+ 11. Taint must surface its 10,000-row truncation and must not swallow a query failure as an "ok"
572
+ phase outcome (§7.4).
573
+
574
+ **Then the honest surface:**
575
+
576
+ 12. Phase-aware ETA: parse remainder + derived phases + the embedding queue, a windowed rate, and
577
+ "…" until there is signal (§8.1).
578
+ 13. Message bands: a "seconds/a moment" band below ~50 files, and a "longer than we expect"
579
+ threshold that scales with the discovered file count (§8.2, §8.5).
580
+ 14. Delete the dead branches in `encouragementFor` and `renderNonTtyLine`, or make them do what
581
+ their comments claim (§8.3, §8.4).
582
+
583
+ **Then space and throughput:**
584
+
585
+ 15. Defer the 14 relationship indexes (549 MiB) and 10 symbol indexes (127 MB) during a full load
586
+ and build them once at the end, inside the run (§3.2, §6.2) — less write amplification per
587
+ insert, which is what `dbFlushMs` is made of.
588
+ 16. Tune the bulk load: `temp_store = MEMORY` (the biggest intermediate and every per-edge sort are
589
+ file-backed today), a real page cache, and `journal_size_limit` (§6.4, §7.5).
590
+ 17. Bound parent memory and add an IPC high-water mark (§3.3); the in-process fallback retains every
591
+ file result for the whole run, and `fileRelCounters` is never cleared (§7 memory).
592
+ 18. Add a ≥1,000-file tier to `verify-scaling-linear.ts` and fix the intermediate accounting (§2,
593
+ §7.2) so the gate sees production scale.
594
+ 19. Reclaim on ordinary runs, not only after `--force`, and move the end-of-run checkpoint after
595
+ the embedding drain (§6.6, §6.3.3) — 858 MiB (27%) of this index is currently never returned.
596
+
597
+ **Worth deciding deliberately (policy, not bugs):** whether `.d.ts`, fixtures and data formats stay
598
+ indexed; whether read-time exclusions and index-time discovery share one list (§4.11-4.12);
599
+ duplicate-content detection (§4.10); reporting the in-process 13k/14k clamp (§4.9); and refreshing
600
+ stale planner stats on incremental runs (§7.6).
601
+
602
+ ---
603
+
604
+ ## 11. Not covered by this review
605
+
606
+ - **Not measured:** the split of the 404 s store phase between the per-edge SELECT, 17-way index
607
+ maintenance and the per-file DELETEs. Two reviewers bounded it (their additive estimate exceeds
608
+ the phase, so the proxy over-counts), but no run isolated the terms — that needs instrumentation,
609
+ which is a write.
610
+ - **Not observed in production:** §5.1's EPIPE ordering and §5.2's crash-before-`ready` loop. Both
611
+ are demonstrated at the Node/code level with real child processes, not caught in a run.
612
+ - **Not measured:** parent peak RSS per 1,000 retained symbols (the mechanism is certain from the
613
+ code; the constant is not), and the same for the in-process fallback's retained results.
614
+ - **Not reproduced:** the documented no-stats blowups (260× / 600× / 200–400×). The reviewer
615
+ reproduced the plan *changes* on a schema-identical A/B, not the blowups, whose preconditions are
616
+ data-skew dependent.
617
+ - **Not exercised:** the embedding drainer and semantic search on a genuinely mixed-model index
618
+ (read from code plus in-repo measurements); Bun's bundled SQLite (only the Node better-sqlite3
619
+ 3.49.2 path was probed, so `TEMP_STORE` may differ there).
620
+ - **Out of scope:** LSP/MCP read paths beyond what the discovery pass touched, every language
621
+ analyzer's own parse cost, and why 124,711 edges (25.1%) resolve to no symbol at all — that is
622
+ analyzer work, not indexer work.
623
+
624
+ ## Verification status of this document
625
+
626
+ | Section | How it was established |
627
+ |---|---|
628
+ | §1, §2, §3 | Measured on this machine: `brain_index_status.performance_stats`, `dbstat`, `PRAGMA`/`compile_options`, and the committed run logs |
629
+ | §4 | A review pass that **executed the real `discoverIndexableFiles`** against synthetic fixtures, plus my own audit of the index against `git ls-files`; I re-read the CRITICAL finding, the no-op logger and the 256 KB truncation myself |
630
+ | §5 | A review pass that reproduced the IPC orderings with real child processes; I re-verified both HIGH findings in the source (the only `error` listener is the fork probe's; `clearStartupTimer` is first in the exit handler) |
631
+ | §6 | A review pass that ran `EXPLAIN QUERY PLAN` and per-candidate timings on a schema-identical copy and `dbstat`-measured the real index; I re-read its two headline claims (the per-edge SELECT inside the store loop; `largestIntermediate` excluding `_i13_src`) |
632
+ | §7 | A review pass with real-database counts/distributions and synthetic timings; I re-read the `largestIntermediate` exclusion and the unguarded per-edge resolve |
633
+ | §8, §9 | Read and measured by me, including verbatim CLI runs on 7-file, 205-file and 5,086-file projects |
634
+ | §3.1, §6.4, §7.8 | Cross-checked against the reviewers' independent `dbstat`/`PRAGMA` measurements, which agreed byte-for-byte apart from one corrected object count |