@backbencher/brain 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-Q5APTJQL.js +17 -0
  8. package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
  9. package/dist/brain.sql-EIM4WS4D.js +29 -0
  10. package/dist/chunk-22V37YTK.js +1758 -0
  11. package/dist/chunk-2BSKD7O3.js +40 -0
  12. package/dist/chunk-2DE27DVC.js +1314 -0
  13. package/dist/chunk-2RRBMNBQ.js +322 -0
  14. package/dist/chunk-2WXYNLBA.js +29 -0
  15. package/dist/chunk-3CTFIJJB.js +17 -0
  16. package/dist/chunk-3ZTOHHSW.js +2104 -0
  17. package/dist/chunk-4H22TLOT.js +543 -0
  18. package/dist/chunk-5BVAH26D.js +340 -0
  19. package/dist/chunk-62DHLYW6.js +3427 -0
  20. package/dist/chunk-7AN7U5IZ.js +371 -0
  21. package/dist/chunk-7DRPNVIN.js +782 -0
  22. package/dist/chunk-7MBYT777.js +174 -0
  23. package/dist/chunk-AHDWGM7Q.js +236 -0
  24. package/dist/chunk-B5GEAAYF.js +818 -0
  25. package/dist/chunk-BEIZZ73B.js +1479 -0
  26. package/dist/chunk-BQ3OVQ3P.js +125 -0
  27. package/dist/chunk-D2Q4Y47A.js +53 -0
  28. package/dist/chunk-DIYAGQJL.js +34 -0
  29. package/dist/chunk-DWKXZ6IF.js +336 -0
  30. package/dist/chunk-DYM4FQEE.js +90 -0
  31. package/dist/chunk-ENW4BTBA.js +930 -0
  32. package/dist/chunk-EWMWK7WN.js +75 -0
  33. package/dist/chunk-EY5ZZUUA.js +626 -0
  34. package/dist/chunk-GBI7V7KY.js +69 -0
  35. package/dist/chunk-GEFFO6O2.js +152 -0
  36. package/dist/chunk-HJEWO5EV.js +247 -0
  37. package/dist/chunk-HQSPJDHI.js +613 -0
  38. package/dist/chunk-KAET47ZL.js +125 -0
  39. package/dist/chunk-KCDECRJY.js +440 -0
  40. package/dist/chunk-L5TDYKYK.js +76 -0
  41. package/dist/chunk-MLKGABMK.js +9 -0
  42. package/dist/chunk-NO3U3FJQ.js +201 -0
  43. package/dist/chunk-OWISBAOF.js +61 -0
  44. package/dist/chunk-QKTRECYS.js +1119 -0
  45. package/dist/chunk-QLK6ED4Q.js +235 -0
  46. package/dist/chunk-R2GW4F6R.js +19 -0
  47. package/dist/chunk-RCMTFSEF.js +87 -0
  48. package/dist/chunk-RM3PY6TG.js +376 -0
  49. package/dist/chunk-ROMM6KMC.js +433 -0
  50. package/dist/chunk-SGGCSHAX.js +12050 -0
  51. package/dist/chunk-SK6YVHFZ.js +211 -0
  52. package/dist/chunk-SM7RFQRO.js +214 -0
  53. package/dist/chunk-UGR4QIGS.js +67 -0
  54. package/dist/chunk-UQNQZXHY.js +711 -0
  55. package/dist/chunk-UUNHHX4M.js +13 -0
  56. package/dist/chunk-VL52PL2T.js +71 -0
  57. package/dist/chunk-WIG36E67.js +196 -0
  58. package/dist/chunk-XHK3WWGB.js +7436 -0
  59. package/dist/chunk-XTQM2IP3.js +2863 -0
  60. package/dist/chunk-ZE2LG76D.js +1296 -0
  61. package/dist/chunk-ZETTGPAW.js +6950 -0
  62. package/dist/cli/index.js +37 -0
  63. package/dist/config-HZAAYDP3.js +10 -0
  64. package/dist/content-search-7XIP555Q.js +593 -0
  65. package/dist/database-76RP44JG.js +9 -0
  66. package/dist/doctor-ANGETYMN.js +395 -0
  67. package/dist/edit-engine-CG72LDZD.js +51 -0
  68. package/dist/engine-BKEOGN7N.js +11 -0
  69. package/dist/engine-VIW6NSKO.js +18 -0
  70. package/dist/fingerprint-OBLIMZLP.js +33 -0
  71. package/dist/framework-intelligence-3LORTPOX.js +860 -0
  72. package/dist/freshness-3K6VU5OT.js +174 -0
  73. package/dist/graph-engine-HBSQNPTB.js +21 -0
  74. package/dist/graph_request-Q6NKB6N2.js +350 -0
  75. package/dist/impact_request-LYP672WE.js +68 -0
  76. package/dist/index-space-IUUN5WYV.js +15 -0
  77. package/dist/index-status-7OVGYSGW.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-FK2XKZXY.js +88 -0
  86. package/dist/pool-worker.js +1153 -0
  87. package/dist/project-health-6XPENVAA.js +94 -0
  88. package/dist/project-index-5SDPHITE.js +57 -0
  89. package/dist/project-registry-ZJ3DVUGO.js +355 -0
  90. package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
  91. package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
  92. package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
  93. package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
  94. package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
  95. package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
  96. package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
  97. package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
  98. package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
  99. package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
  100. package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
  101. package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
  102. package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
  103. package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
  104. package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
  105. package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
  106. package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
  107. package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
  108. package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
  109. package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
  110. package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
  111. package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
  112. package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
  113. package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
  114. package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
  115. package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
  116. package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
  117. package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
  118. package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
  119. package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
  120. package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
  121. package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
  122. package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
  123. package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
  124. package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
  125. package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
  126. package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
  127. package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
  128. package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
  129. package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
  130. package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
  131. package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
  132. package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
  133. package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
  134. package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
  135. package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
  136. package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
  137. package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
  138. package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
  139. package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
  140. package/dist/regex-search-NJKHMZNO.js +239 -0
  141. package/dist/rules-ACHPB7QU.js +152 -0
  142. package/dist/scoped-path-MJYWJZQX.js +11 -0
  143. package/dist/setup-show-NT5ZHYNE.js +15 -0
  144. package/dist/setup-wizard-G5YYR5T6.js +24 -0
  145. package/dist/symbol-search-GZJIM2J2.js +16 -0
  146. package/dist/taint-KXUHHPIE.js +202 -0
  147. package/dist/taint-analyzer-N7YLZU3N.js +163 -0
  148. package/dist/tool-handlers-OJDC35DI.js +72 -0
  149. package/dist/tool-registry-BPEJNSZG.js +17 -0
  150. package/dist/tool-validation-Q57YMMBX.js +32 -0
  151. package/dist/type-inference-QMT2RMLV.js +13 -0
  152. package/dist/vector-binary-LMY4QGAN.js +19 -0
  153. package/dist/version-ABP2TUL2.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,169 @@
1
+ <!--
2
+ Author: Muhammad Ishaq Khan
3
+
4
+ WHAT THIS DOCUMENT IS — the generated reference for bb_doc: contract, input schema, returns, examples, errors, limitations, performance, safety and evidence semantics.
5
+ WHO IT IS FOR — callers of bb_doc through any MCP host, the CLI (`bb tool bb_doc`), or a library.
6
+ EDIT TOGETHER WHEN THIS CHANGES — src/mcp/tools/bb_doc/descriptor.ts, src/mcp/tools/bb_doc/handler.ts, script/tool-docs/curation/bb_doc.ts; regenerate with `npx tsx script/verify-tool-docs.ts --only bb_doc --write`.
7
+ -->
8
+ # `bb_doc`
9
+
10
+ Generate read-only, evidence-backed documentation with resolvable citations and a freshness block over the cited files. Every mode emits its declared section set, an empty result is never reported as passing, and the output budget is enforced on the payload the caller receives: `quality.budget.estimatedTokens` is measured on the decorated answer, and `estimatedTokens * 4` is never smaller than what was sent. When the required sections cannot fit, the result says so rather than dropping one. `quality.references` counts the citations the answer EMITS (plus the `collected*` totals of everything gathered), so the stale/missing numbers match the Sources list. Supports symbol, file, module, feature, API, project, and documentation-coverage modes in Markdown or JSON. When the cited files are stale, the handler auto-refreshes exactly those files within the shared budget and regenerates the document; refresh=false opts out and answers from existing data with the freshness block. A module target that matches no indexed file is refused by name rather than answered with project-wide facts, and `limit`/`maxTokens` outside their ranges are refused rather than clamped.
11
+
12
+ ## Contract
13
+
14
+ - Category: `analysis`
15
+ - Mutability: `read`
16
+ - Requires index: `true`
17
+ - Modes: `symbol`, `file`, `module`, `feature`, `api`, `project`, `coverage`
18
+ - Planned modes (roadmap, not executable): (none)
19
+ - Defaults: `{"format":"markdown","audience":"developer","detail":"standard","includeExamples":false,"includeDiagrams":false,"includeTests":false,"limit":20,"maxTokens":8000}`
20
+
21
+ ## Input Schema
22
+
23
+ ```json
24
+ {
25
+ "type": "object",
26
+ "properties": {
27
+ "mode": {
28
+ "type": "string",
29
+ "enum": [
30
+ "symbol",
31
+ "file",
32
+ "module",
33
+ "feature",
34
+ "api",
35
+ "project",
36
+ "coverage"
37
+ ],
38
+ "description": "documentation mode"
39
+ },
40
+ "target": {
41
+ "type": "string",
42
+ "description": "symbol, file, module, feature, or API target (`symbol`/`symbolName`/`query` are accepted as aliases); optional for project and coverage. `symbol` matches an indexed name, qualified name or its last dotted/`::` segment. `file` accepts an exact repository-relative path, an absolute path, a directory prefix, or a bare file name with or without its extension. `module` needs a repository-relative directory the index holds; a target that matches nothing is REFUSED with the forms tried. `project` accepts a target but uses it as document metadata only — it does not scope the selection."
43
+ },
44
+ "format": {
45
+ "type": "string",
46
+ "enum": [
47
+ "markdown",
48
+ "json"
49
+ ],
50
+ "description": "output format (default markdown)"
51
+ },
52
+ "audience": {
53
+ "type": "string",
54
+ "enum": [
55
+ "developer",
56
+ "maintainer",
57
+ "user",
58
+ "website"
59
+ ],
60
+ "description": "documentation audience: selects OPTIONAL sections only; a required section is never dropped for a smaller audience"
61
+ },
62
+ "detail": {
63
+ "type": "string",
64
+ "enum": [
65
+ "brief",
66
+ "standard",
67
+ "deep"
68
+ ],
69
+ "description": "detail level: `brief` caps each section at 5 facts and drops optional sections, `standard` caps at `limit`, `deep` caps at 3x`limit` and adds the optional all-edges section; for `feature` it also enables semantic candidate expansion, which is much slower"
70
+ },
71
+ "includeExamples": {
72
+ "type": "boolean",
73
+ "description": "include source-backed examples. No example is emitted when no cited snippet or verified contract supplies one; an honest not-observed unknown is recorded instead"
74
+ },
75
+ "includeDiagrams": {
76
+ "type": "boolean",
77
+ "description": "include cited diagrams when available. This implementation does not emit diagrams; `reference.diagrams` stays empty and an honest not-observed unknown is recorded"
78
+ },
79
+ "includeTests": {
80
+ "type": "boolean",
81
+ "description": "include indexed test evidence. Accepted for `symbol` and `feature` modes only (their documents render a test section); the other modes REFUSE it because it would do nothing. Absent means the mode default (on); explicit false suppresses the test facts"
82
+ },
83
+ "limit": {
84
+ "type": "number",
85
+ "minimum": 1,
86
+ "maximum": 100,
87
+ "description": "maximum evidence items per section (whole number 1-100, default 20). Out-of-range values are REFUSED, not clamped, so the page returned is the page requested"
88
+ },
89
+ "maxTokens": {
90
+ "type": "number",
91
+ "minimum": 1,
92
+ "maximum": 16000,
93
+ "description": "output token budget measured on the emitted payload (whole number 1-16000, default 8000; the default clears the measured minimum honest rendering on the reference index, ~6,700 tokens). `estimatedTokens` reports that measurement; when the required sections cannot fit, `budget.passed` is false and a shortfall unknown names the smallest honest rendering. Out-of-range values are REFUSED"
94
+ },
95
+ "refresh": {
96
+ "type": "boolean",
97
+ "description": "false opts out of the inline auto-refresh of the files this document cites (default: auto-refresh those files within the shared budget, then regenerate); the decision is always declared in `autoRefresh` or the answer text"
98
+ }
99
+ },
100
+ "required": [
101
+ "mode"
102
+ ]
103
+ }
104
+ ```
105
+
106
+ ## Returns
107
+
108
+ - Markdown or structured JSON documentation: title, summary, the mode's declared sections, citation objects with stable IDs and per-citation freshness, and declared unknowns
109
+ - `quality`: the four-term verdict, emitted-citation reference counts (`broken`/`stale`/`missing` plus the `collectedStale`/`collectedMissing` totals of everything gathered), completeness against the declared section set, the index snapshot, and the payload-measured budget
110
+ - `quality.bounds`: per-section numbers — `available`, `capped`, `droppedForBudget`, `emitted` — with `emitted + droppedForBudget === capped` enforced before the result is returned
111
+ - A declared selection bound when a scope has more indexed symbols than the evidence limit selected, naming both numbers
112
+ - `reference`: stable website keys (`projectId`, `snapshotId`, `generatedAt`, `indexVersion`, `schemaVersion`, `sourceLanguage`, `canonicalPath`, `slugs`, `tags`) and an empty `diagrams` array
113
+ - An `autoRefresh` line whenever stale cited files were repaired or a repair was declined, with the reason
114
+
115
+ ## Examples
116
+
117
+ `{"mode":"symbol","target":"handleRequest"}`
118
+
119
+ `{"mode":"feature","target":"request handling","format":"json","detail":"deep"}`
120
+
121
+ `{"mode":"coverage","target":"src","audience":"maintainer"}`
122
+
123
+ ## Errors
124
+
125
+ - Missing mode, or a mode outside symbol|file|module|feature|api|project|coverage
126
+ - Missing target for `symbol`, `file`, `module` or `feature`
127
+ - `limit` outside 1-100, or `maxTokens` outside 1-16000 — refused with the accepted range rather than clamped
128
+ - `includeTests` on a mode that renders no test evidence (everything except `symbol` and `feature`)
129
+ - A `module` target that matches no indexed file — refused by name, with the two directory forms it tried
130
+ - An invalid `format`, `audience` or `detail` value (rejected by the registry validator with the accepted enum)
131
+ - Unavailable indexed evidence
132
+
133
+ ## Limitations
134
+
135
+ - Unknown facts are explicit, and a required section that renders empty says why in its own words; a document with no cited claim never reports `passed`
136
+ - Stale evidence is qualified per citation and counted as `freshness.passed: false`; it cannot silently become current
137
+ - `includeTests` is a real option only where a test section is rendered: `symbol` and `feature`. The other modes refuse it instead of accepting a no-op
138
+ - `project` mode accepts a target but uses it as document metadata; it does not scope the selection
139
+ - `feature` mode's semantic candidate expansion is opt-in via `detail: "deep"`; every other call is lexically discovered and says so, because semantic expansion measured 19-52 s on a 310k-symbol index
140
+ - A scope with more indexed symbols than `2 x limit` reports the selection bound (`selected` of `available`) rather than implying it mapped the whole set
141
+ - When the required sections cannot fit `maxTokens`, the result is returned with `budget.passed: false` and the smallest honest rendering named; it is never met by dropping a required section
142
+
143
+ ## Performance
144
+
145
+ Measured warm on the reference index (6,452 files, 310,423 symbols, 820,040 relationships): default calls take symbol 0.24 s, file 0.19 s, module 0.42 s, api 1.03 s, project 0.76 s, coverage 0.07 s, feature 1.97 s (p50; five runs, p95 at most 3.65 s). A COLD first default call in a fresh process is 3.9 s on a cold page cache (930 ms with a warm one) and the read-only database open is 2 ms; the cold cost is index page-cache warm-up, not query time, and it is declared here rather than hidden by the warm figures. `feature` with `detail: "deep"` adds a semantic candidate pass (measured 18.0 s and 926 MB on the reference index) and is the only shape that can approach the client timeout; the default feature call is lexical and 2 s. Work is bounded by the request: the evidence limit bounds the facts, the per-section cap bounds each section, and `maxTokens` bounds the emitted payload. The default budget is 8,000 tokens because the measured minimum honest rendering of a project document on the reference index is about 6,700 tokens; a 4,000-token default could not be honoured by its own reference call.
146
+
147
+ ## Safety
148
+
149
+ Read-only with one bounded, declared exception: when the files a document cites are stale, the handler reindexes exactly those cited files (at most 25 files / 2 MiB per call) and regenerates. `refresh:false` or `BB_NO_AUTO_REFRESH=1` opts out and the answer declares the decline. It never writes source, never creates a file, never calls bb_edit, and never touches a file the document does not cite.
150
+
151
+ ## Evidence Semantics
152
+
153
+ Claims are classified as extracted, inferred, generated or unknown and carry resolvable citations; relationship facts name the symbol on the other end of the edge, with inbound edges naming the documented symbol. `quality.references` counts the citations the answer emits so the stale/missing numbers can be checked against the Sources list.
154
+
155
+ ## Relationship to other tools
156
+
157
+ For raw symbol metadata use `bb_search` mode `symbol`; this tool composes documented, cited explanations. For cross-language reference facts on one symbol use `bb_cross_language`; for graph traversal use `bb_graph`.
158
+
159
+ ## Deep guides
160
+
161
+ - [Document generation: modes, citations, audiences](../BB_DOC.md)
162
+ - [Doc reference: every field and option](../BB_DOC_REFERENCE.md)
163
+ - [Doc examples](../BB_DOC_EXAMPLES.md)
164
+
165
+ ## Staleness
166
+
167
+ Freshness applies to the tool's read output (no registered per-mode split). Staleness surfaces as: `freshness` envelope (`{ fresh, stale, missing, staleRatio, mode, remediation }`) on JSON + footer naming the remediation; per-citation `(stale)` in Sources; silent when all fresh. When something is stale/missing, the remediation names the registered reindex interfaces: project scope → `reads auto-refresh stale files within budget when enabled — disable with BB_NO_AUTO_REFRESH=1 or refresh=false; manual repair: run: bb index --changed (or the bb_index tool with the project path — incremental, changed files only)`; file scope → `reads auto-refresh this file within budget when enabled — disable with BB_NO_AUTO_REFRESH=1 or refresh=false; manual repair: run: bb index "<file>" (or the bb_index tool with that file path)`. Nothing is stale → silence (no remediation text). Detection uses a mtime+size fast path with a content-hash fallback: an edit that leaves both size and mtime unchanged is reported fresh without hashing (same trade-off as git's index stat-cache; accepted, documented in `src/brain/freshness.ts`).
168
+
169
+ This page is generated from the executable registry and curated documentation. Run `npm run docs:generate` to update it, or `npx tsx script/verify-tool-docs.ts --only <tool> --write` for a single page.
@@ -0,0 +1,185 @@
1
+ <!--
2
+ Author: Muhammad Ishaq Khan
3
+
4
+ WHAT THIS DOCUMENT IS — the generated reference for bb_edit: contract, input schema, returns, examples, errors, limitations, performance, safety and evidence semantics.
5
+ WHO IT IS FOR — callers of bb_edit through any MCP host, the CLI (`bb tool bb_edit`), or a library.
6
+ EDIT TOGETHER WHEN THIS CHANGES — src/mcp/tools/bb_edit/descriptor.ts, src/mcp/tools/bb_edit/handler.ts, script/tool-docs/curation/bb_edit.ts; regenerate with `npx tsx script/verify-tool-docs.ts --only bb_edit --write`.
7
+ -->
8
+ # `bb_edit`
9
+
10
+ Unified, validated source mutation. Discriminated by mode: write, update, insert, delete, or refactor. Supports dry-run previews, separator/symlink-safe paths, AST byte bounds, caller protection, rollback on validation failure, formatting, and reindexing. If the target file changed since it was indexed, this tool refreshes that one file and re-resolves the symbol before editing (reported in warnings) — an edit is never applied to stale byte bounds, and a symbol that was renamed or removed is refused by name. The bytes each target had when the edit was PLANNED are re-checked immediately before the write, so a file that changed in between (an editor saving during a multi-file refactor, say) is refused with nothing written rather than overwritten from a plan computed against its previous contents. A project with no index is not a dead end: `write` plans from the request and the disk, resolves no symbol, and reindexes the file it wrote (so `reindexed` is how a brand-new project gets its first indexed rows), while `update`, `insert`, `delete` and `refactor` resolve their target through the index and are refused — with the mode that still works named beside the reindex command — until one exists. `write` replacing an existing file is guarded: a replacement that removes at least half of a file's lines (and at least 5 lines) is refused as a probable mistake unless `overwrite: true` is passed, and `overwrite: false` makes the write create-only — the refusal names the file, the line counts and both options. `batch` applies several edits, or one edit across several files, as ONE atomic call: every entry is planned before anything is written (so a refusal anywhere writes nothing), the bytes of EVERY target are re-checked together immediately before the first write, and formatting, validation and reindexing each run ONCE for the whole call. Two entries may not resolve to the same file — each is planned from its file's pre-edit bytes, so the second would discard the first, and that is refused by entry index. Every answer carries a per-file `envelope` (`planned`, `committed`, `restored`, and the pre/post byte counts), and a refused call carries the same envelope on the error, so "is my working tree back?" is answered per file rather than by one boolean. Unlike `write`, `batch` needs an index: the mode cannot promise of a list what a single entry's plan proves.
11
+
12
+ ## Contract
13
+
14
+ - Category: `operations`
15
+ - Mutability: `write`
16
+ - Requires index: `true` (except `write`, which needs no index at all: it plans from the request and the disk, resolves no symbol, and reindexes the file it wrote — so it works on a project the indexer has never run over, and that is how such a project gets its first indexed rows)
17
+ - Modes: `write`, `update`, `insert`, `delete`, `refactor`, `batch`
18
+ - Planned modes (roadmap, not executable): (none)
19
+ - Defaults: `{"dryRun":false,"format":true,"force":false}`
20
+
21
+ ## Input Schema
22
+
23
+ ```json
24
+ {
25
+ "type": "object",
26
+ "properties": {
27
+ "mode": {
28
+ "type": "string",
29
+ "enum": [
30
+ "write",
31
+ "update",
32
+ "insert",
33
+ "delete",
34
+ "refactor",
35
+ "batch"
36
+ ],
37
+ "description": "Edit kind: write (create or overwrite a file), update (replace a symbol body), insert (add code relative to a symbol), delete (remove a symbol), refactor (identifier-aware rename), or batch (apply several edits, or one edit across several files, as one atomic call)"
38
+ },
39
+ "dryRun": {
40
+ "type": "boolean",
41
+ "description": "Preview the planned changes without writing any file (default false)"
42
+ },
43
+ "filePath": {
44
+ "type": "string",
45
+ "description": "write: project-relative target file to create or overwrite. Other modes: disambiguates symbolName when it appears in multiple files"
46
+ },
47
+ "symbolName": {
48
+ "type": "string",
49
+ "description": "Name of the symbol to update, delete, or refactor (the index must contain it; a target file changed since indexing is refreshed automatically before the edit)"
50
+ },
51
+ "lineStart": {
52
+ "type": "number",
53
+ "description": "Declaration line of the target symbol, used with symbolName/filePath to disambiguate repeated names"
54
+ },
55
+ "content": {
56
+ "type": "string",
57
+ "description": "Full file content for write mode; also accepted in place of newContent for update and insert"
58
+ },
59
+ "newContent": {
60
+ "type": "string",
61
+ "description": "Replacement source: update swaps the symbol body for this text; insert adds this text relative to targetSymbolName"
62
+ },
63
+ "targetSymbolName": {
64
+ "type": "string",
65
+ "description": "insert: symbol whose body or boundary receives the new code (required for insert)"
66
+ },
67
+ "position": {
68
+ "type": "string",
69
+ "enum": [
70
+ "before",
71
+ "after",
72
+ "inside-top",
73
+ "inside-bottom"
74
+ ],
75
+ "description": "insert anchor relative to targetSymbolName (default after)"
76
+ },
77
+ "newName": {
78
+ "type": "string",
79
+ "description": "refactor: the valid identifier the symbol is renamed to"
80
+ },
81
+ "force": {
82
+ "type": "boolean",
83
+ "description": "delete: allow removal when indexed callers or live references survive; leftovers are reported as warnings (default false)"
84
+ },
85
+ "format": {
86
+ "type": "boolean",
87
+ "description": "Run the project formatter on written files after the edit (default true)"
88
+ },
89
+ "overwrite": {
90
+ "type": "boolean",
91
+ "description": "write only. Omitted: replace the file, but refuse a replacement that removes at least half of a file's lines and at least 5 lines (a probable mistake, such as a body fragment sent to write instead of update). true: replace it whatever the size. false: create-only — refuse when the file already exists."
92
+ },
93
+ "edits": {
94
+ "type": "array",
95
+ "description": "batch only, required there: the edits to apply, each a complete request in one of the five single-file modes (write|update|insert|delete|refactor), e.g. [{mode:'update',symbolName:'helper',newContent:'...'},{mode:'delete',symbolName:'oldFlag'}]. Entries may not set dryRun or format (those belong to the batch, which previews, formats and reindexes once), may not nest a batch, and may not two of them resolve to the same file — each entry is planned from its file's pre-edit bytes, so a second entry on one file would discard the first (refused by entry index).",
96
+ "items": {
97
+ "type": "object",
98
+ "description": "one edit: `mode` (required: write|update|insert|delete|refactor) plus exactly the options THAT mode accepts at the top level — write takes filePath+content/newContent (+overwrite), update takes symbolName+newContent, insert takes targetSymbolName+newContent (+position), delete takes symbolName (+force), refactor takes symbolName+newName; every entry may also take filePath and lineStart to disambiguate. An option the entry's own mode would not accept at the top level is refused here too, by name, so an entry is validated by the same rule as the single-mode call it mirrors."
99
+ }
100
+ },
101
+ "files": {
102
+ "type": "array",
103
+ "description": "batch only: the fan-out form — each entry applies once per listed project-relative path, and `files` supplies the filePath of every entry (so an entry with `files` must not name its own). Omit it to let each entry name its own target. The list may not repeat a path.",
104
+ "items": {
105
+ "type": "string"
106
+ }
107
+ }
108
+ },
109
+ "required": [
110
+ "mode"
111
+ ]
112
+ }
113
+ ```
114
+
115
+ ## Returns
116
+
117
+ - Structured mutation result or dry-run preview
118
+ - Validation, rollback, and reindex status, plus the measured per-file formatter outcome (`formatting`)
119
+ - Affected paths and caller protection outcomes
120
+ - `envelope` — the per-file outcome of the call (`planned`, `committed`, `restored`, the pre/post byte counts, and any restore that FAILED with its reason), on the answer and on a refusal alike, so `batch`'s per-file question is answered per file
121
+ - `batch` (batch only) — one row per `edits` entry with the mode it ran and the files it reached
122
+ - `importPruning` (delete only) — `attempted`, `language`, the `removedBindings` it removed, and for a language other than TypeScript/JavaScript the `reason` it left the file's imports unchanged
123
+ - a refusal carries the per-file `envelope` too — the EMPTY one (`entries: []`, nothing committed, nothing restored) with `refusedBeforeWriting: true` when the call refused while planning
124
+
125
+ ## Examples
126
+
127
+ `{"mode":"update","symbolName":"helper","newContent":"export function helper() { return 2 }"}`
128
+
129
+ `{"mode":"insert","targetSymbolName":"main","position":"inside-top","newContent":"const started = true"}`
130
+
131
+ `{"mode":"write","filePath":"src/new.ts","content":"export const value = 1"}`
132
+
133
+ ## Errors
134
+
135
+ - Path escape or symlink escape, and a target inside a protected path (`.git/`, `.hg/`, `.svn/`, `node_modules/`, or the index data directory — refused by name before anything is written)
136
+ - Ambiguous symbol name (the message names filePath/lineStart)
137
+ - `refactor` onto a name the file already declares, or onto the symbol's current name — both refused at plan time, nothing written
138
+ - Symbol renamed or removed since the target file was indexed (refreshed first, then refused by name)
139
+ - `update`, `insert`, `delete` or `refactor` on a project with NO index — the refusal names the reindex command and says that `write` still works, because the symbol modes resolve their target through the index and `write` resolves none
140
+ - Caller protection, parse, or reindex failure — a FORMATTER failure is not an error: the edit stands and the outcome is reported per file in `formatting` with `formatted: false`
141
+ - `batch`: an empty or missing `edits`, an entry with no mode or with an option its own mode does not accept (the refusal names the entry index), a per-entry `dryRun`/`format` (they belong to the call), an entry that names its own `filePath` while `files` supplies the targets, a repeated path in `files`, two entries that resolve to ONE file, or more than 200 planned edits — each refused with the run's own reason and nothing written
142
+
143
+ ## Limitations
144
+
145
+ - Named symbol edits need an INDEX, not a fresh one: when the target file changed after it was indexed, the tool refreshes that ONE file's rows and re-resolves the symbol before planning, so bounds always come from the bytes on disk and never from a stale span. The refresh is reported in `warnings`; if the symbol was renamed or removed in the meantime the edit is refused by name instead of guessing a span
146
+ - Directly after a write, the file's own evidence is current but cross-file relationships are only re-derived within the bounded post-pass, so a caller in an unrelated file may lag until the next dir index (same bound as bb_index)
147
+ - Refactoring is identifier-aware static rewriting, not a full language-server proof
148
+ - Per-mode arguments are enforced in BOTH directions and the accepted set is named, so a rejection can be acted on: `bb_edit update mode does not accept: position (position is accepted for insert) — accepted for update: mode, dryRun, filePath, symbolName, lineStart, content, newContent, format`. An option this mode cannot honor is REFUSED, never silently ignored, and an option that belongs to exactly one other mode says which one. The mode contract: `write` → `mode`, `dryRun`, `filePath`, `content`, `newContent`, `format`, `overwrite`; `update` → `mode`, `dryRun`, `filePath`, `symbolName`, `lineStart`, `content`, `newContent`, `format`; `insert` → `mode`, `dryRun`, `filePath`, `targetSymbolName`, `position`, `lineStart`, `content`, `newContent`, `format`; `delete` → `mode`, `dryRun`, `filePath`, `symbolName`, `lineStart`, `force`, `format`; `refactor` → `mode`, `dryRun`, `filePath`, `symbolName`, `lineStart`, `newName`, `format`; `batch` → `mode`, `dryRun`, `edits`, `files`, `format`.
149
+ - Body-fragment updates (`newContent` that is a statement, not a declaration) require a BRACE-bodied language. TypeScript, JavaScript, Go, Rust, Java, C/C++, Swift, Kotlin, Scala and PHP all take this path. In an indentation- or keyword-delimited language — Python, Ruby, YAML — there is no `{ ... }` range to splice and the tool REFUSES rather than guessing; the refusal names the remedy and passing the full declaration then works. Elixir's `do ... end` is likewise refused. Measured across Python (2-space, 4-space and tab indentation), Ruby at two nesting depths, Go, Rust, Java and Elixir.
150
+ - `reindexed` is measured from the reindex result, not from the intent: an edit whose reindex reports per-file failures keeps its validated bytes on disk, answers `reindexed: false` and names the files in a `reindex:` warning. The index is then stale for those files; the NEXT symbol edit on them either refreshes that one file inline (when inline refresh is enabled) or refuses with the stale-file remedy (`bb_index` for that file), which is the tool declining to write to bytes it cannot vouch for, not a lost edit.
151
+ - `delete` refuses when the symbol has indexed callers OR when an import or re-export specifier in another file names it, which covers `import { x as alias }` — the analyzer records no call edge there because it never resolved the alias. The specifier is matched by RESOLVING the module path, not by name, so `../models/user` and `./user` are both caught and a same-named module in a different directory is not. It does NOT refuse for a side-effect-only `import './x'`, a namespace import, or a file with a local that merely shares the name, because none of those can dangle. A forced delete always warns and names every surviving specifier and its file.
152
+ - KNOWN GAP in `delete`: a tsconfig path alias (`@/models/user`, `~/models/user`) is not caught. The indexer records no import row for a non-relative specifier, so there is nothing to resolve; such a project's only remaining signal is the indexed `calls` edge. A bare package specifier (`react`, `lodash/fp`) is likewise unresolved, correctly, since it cannot be shown to refer to a project file.
153
+ - Formatting is best-effort and MEASURED, never assumed: an extension with no mapping (`.rb`, `.swift`, `.kt`, `.yaml`, …), a formatter that is not installed, a non-zero exit, and the 1.5s timeout all report `formatted: false` with the per-file reason in `formatting`. The tool never installs a formatter and does not read a project's own formatter configuration — it invokes the standard binary for the extension (prettier/rustfmt/gofmt/clang-format/black-or-ruff) that it can resolve from `node_modules/.bin` or `PATH`.
154
+ - `write` replacing an existing file is GUARDED rather than free: with `overwrite` omitted, a replacement that removes at least half of the file's lines and at least 5 lines is refused as a probable mistake (a body fragment sent to `write` instead of `update`, or the wrong `filePath`), and the refusal states both line counts, the share that would be lost, and both ways forward. `overwrite: true` replaces it whatever the size; `overwrite: false` is create-only. A 41-line file replaced by one line is therefore refused rather than reported `ok` with nothing distinguishing it from an intentional rewrite
155
+ - The pre-image check covers staleness on the WRITE side too, and it is not about the index: every target's sha256 is taken as the plan reads it and compared with the bytes the commit is about to replace, immediately before the first write of the batch. A file that changed in that window — an editor saving during a multi-file refactor — is refused with NOTHING written and both hashes named, rather than overwritten from a plan computed against its previous contents. A path the plan intended to create must still not exist; a file that appeared there is refused the same way.
156
+ - Import cleanup after `delete` is TypeScript/JavaScript-only and AST-driven: the tree names the import statements and identifier occurrences decide usage, so dead bindings are removed and still-used (including aliased and namespace) bindings are kept. In every other language the file is left byte-for-byte unchanged and the answer DECLARES it (`importPruning.attempted: false` with the language and the reason) plus a warning — a leftover import never breaks a build, while an import removed on a pattern can
157
+ - Writes are refused inside `.git/`, `.hg/`, `.svn/`, `node_modules/`, and the index data directory, by resolved path and by name. That is a safety list beside the security boundary (containment in the project), so a typo'd `filePath` cannot rewrite version-control state or the database the tool's answers come from
158
+ - A formatter may rewrite a file's line endings with its own: `gofmt -w` turns a CRLF file into LF. The engine restores the file's own ending after formatting and declares the repair on that file's `formatting` entry (`lineEndingRestored: true`), so an edit never silently migrates a file to another line-ending convention
159
+ - A commit is SECONDS, not milliseconds: it formats, validates and reindexes synchronously before answering. Measured warm p50/p95 on lane fixtures: update commit 1801.5/2062.9 ms on a 5-file fixture and 4500.7/7052.3 ms on a 100-file fixture; a dry run of the same edit is 11.5-71 ms p50. The MCP client's 30 s timeout is the only ceiling, and a timed-out write call is the case where `envelope` (or a re-run, which re-plans against the current bytes) is how a caller recovers committed-versus-restored
160
+
161
+ ## Performance
162
+
163
+ Only affected files are written, formatted, validated and reindexed; work is proportional to the TOUCHED file, not the project size. A missing formatter costs no subprocess at all: the binary is resolved before anything is spawned. `batch` pays each step ONCE for the whole call (one formatting pass, one validation pass, one reindex), and dry runs plan only: measured p50/p95 on lane fixtures, an update dry run is 11.5/11.8 ms on a 5-file fixture and 71.0/76.8 ms on a 100-file fixture, while a commit of the same edit is 1801.5/2062.9 ms and 4500.7/7052.3 ms respectively because it also formats, validates and reindexes before answering
164
+
165
+ ## Safety
166
+
167
+ Write-capable. Uses project-root path checks, atomic writes, snapshots, validation, and rollback; use dryRun before risky edits. dryRun writes no source file, but a stale target file is still refreshed in the INDEX first (derived data only), because the bounds it previews must be the bounds the real edit would use. `write` on an existing file is guarded against the catastrophic case — a replacement that removes at least half of a file's lines and at least 5 lines needs `overwrite: true`, and `overwrite: false` makes the write create-only — and every committed batch re-checks that the bytes it is replacing are the bytes the plan read, refusing with nothing written when they moved. The guard's refusal is raised at PLAN time, so a dry run reports exactly what the real call would. `batch` plans every entry before the first write, so a refusal anywhere is a refusal of the whole call; the pre-image check then reads every target of the call at once — a target that moved refuses all of it, including entries whose own plan was fine; and a failure AFTER the writes restores each committed file and reports what happened in `envelope` rather than in one boolean. Two entries that resolve to one file are refused rather than composed, because both are planned from that file's pre-edit bytes.
168
+
169
+ ## Evidence Semantics
170
+
171
+ Mutation bounds come from indexed AST bytes, re-resolved from the file on disk whenever the indexed rows are stale; validation and reindex state report whether post-edit evidence is current. The plan's own pre-images are checked against disk immediately before the write, so a plan is applied only to the bytes it was computed from. `write` is the one mode that consumes no indexed row: it takes its bytes from the request and the disk, which is why it is also the one mode usable on a project with no index.
172
+
173
+ ## Relationship to other tools
174
+
175
+ To attach human-visible notes to symbols use `bb_annotate`; this tool mutates source files.
176
+
177
+ ## Deep guides
178
+
179
+ - [Unified editing: safety model, rollback and every mode](../BB_EDIT.md)
180
+
181
+ ## Staleness
182
+
183
+ No freshness flags are returned — this tool does not summarize OTHER files' staleness, and it does not read stale data: a stale TARGET file is refreshed (that file only) and the symbol re-resolved from the bytes on disk before the edit is planned, and the refresh is named in `warnings`. If that change renamed or removed the symbol, the edit is refused by name rather than applied to a stale span. For unrelated files, remediation stays manual: project scope → run `bb index --changed` (or the `bb_index` tool with the project path — incremental, changed files only).
184
+
185
+ This page is generated from the executable registry and curated documentation. Run `npm run docs:generate` to update it, or `npx tsx script/verify-tool-docs.ts --only <tool> --write` for a single page.
@@ -0,0 +1,169 @@
1
+ <!--
2
+ Author: Muhammad Ishaq Khan
3
+
4
+ WHAT THIS DOCUMENT IS — the generated reference for bb_graph: contract, input schema, returns, examples, errors, limitations, performance, safety and evidence semantics.
5
+ WHO IT IS FOR — callers of bb_graph through any MCP host, the CLI (`bb tool bb_graph`), or a library.
6
+ EDIT TOGETHER WHEN THIS CHANGES — src/mcp/tools/bb_graph/descriptor.ts, src/mcp/tools/bb_graph/handler.ts, script/tool-docs/curation/bb_graph.ts; regenerate with `npx tsx script/verify-tool-docs.ts --only bb_graph --write`.
7
+ -->
8
+ # `bb_graph`
9
+
10
+ Per-symbol dependency graph over the canonical graph engine: callers, callees, per-edge evidence tier (structural/import_based/usage_contextual/reference_qualified/inferred), the stored evidence class and resolution basis behind each tier, confidence, and cross-language edge count. Same engine and tier rules the editor graph panel uses; truncated traversals are declared with a reason, never silent. Traversal semantics: a relationship stored against any member of a class (a method call, an inherited behavior) is attributed to the owner class, so caller/callee sets include the owner-family level — every edge still traces to a stored relationship row and states whether it matched the symbol itself or one of its members. Each edge's `rowId` is the stored relationship key with its leading '<projectHash>:rel:' removed, and each endpoint's `id` is the stored symbol key with its leading '<projectHash>:' removed — both prefixes are constant within one answer, and `root.id` carries the FULL stored form, so '<projectHash>' is the segment before its first ':' and prepending it (plus ':rel:' for a rowId) reconstructs the store key exactly. An `ext:` id has no such prefix and is returned whole. The graph is READ from a read-only index handle: asking a question cannot mutate the index. Measured on this repository's reference index (11,440 files, 310,423 symbols, 820,040 stored relationships), a default call answers in 22-355 ms p95 depending on the symbol's degree, where the same call took 131 s before the engine's cascade and arm queries were rewritten to seek their indexes.
11
+
12
+ ## Contract
13
+
14
+ - Category: `graph`
15
+ - Mutability: `read`
16
+ - Requires index: `true`
17
+ - Modes: `graph`
18
+ - Planned modes (roadmap, not executable): (none)
19
+ - Defaults: `{"maxDepth":5,"direction":"both"}`
20
+
21
+ ## Input Schema
22
+
23
+ ```json
24
+ {
25
+ "type": "object",
26
+ "properties": {
27
+ "symbol": {
28
+ "type": "string",
29
+ "description": "symbol name to root the graph at (`target`/`symbolName`/`query` are accepted as aliases). Required; a blank name is refused as missing. An unknown name is refused after the resolver's declared containment fallback runs: that fallback is a substring scan, and on the 11,440-file reference index it costs about 5 s before answering not-found. It is a documented step of the resolver, not a silent budget breach; an exact-name miss does not run it."
30
+ },
31
+ "maxDepth": {
32
+ "type": "number",
33
+ "description": "traversal depth bound (default 5, max 10). Must be a whole number: a fractional depth is refused rather than floored."
34
+ },
35
+ "direction": {
36
+ "type": "string",
37
+ "enum": [
38
+ "callers",
39
+ "callees",
40
+ "both"
41
+ ],
42
+ "description": "which side to traverse (default both). 'callers' answers \"who depends on this\" and skips the callee walk entirely — on a real index that is the difference between 1 edge and 37 in the same payload."
43
+ },
44
+ "relationshipTypes": {
45
+ "type": "array",
46
+ "items": {
47
+ "type": "string"
48
+ },
49
+ "description": "stored relationship kinds to include. Default: EVERY kind the index stores (calls, extends, implements, uses, instantiates, overrides, contains, references, imports, exports, decorates, renders, mixes_in, part_of) — deliberately not narrowed, because a narrower default silently drops a fifth of the graph and cannot descend a class hierarchy: 'contains' is the parent-to-member edge, so excluding it means a traversal rooted at a class cannot reach that class's own methods. The cost is size — the type-level 'uses'/'references'/'contains' rows are roughly half of what a typical index stores, so a hub symbol's default answer is 2.5-4.8x larger than a filtered one (measured: lodash 487 KiB against 101 KiB, ripgrep 795 against 321). Pass relationshipTypes: ['calls'] for the pure call graph, or ['implements'] to see only interface satisfaction. A kind you do not list is never reported; a filter you supply is a REQUEST, not a bound — it is reported in `summary.filterNarrowed` and `summary.filterReason` (never in `truncationReason`, which is reserved for bounds), and the kinds actually searched come back in `summary.scope.relationshipTypes`."
50
+ },
51
+ "includeCrossLanguage": {
52
+ "type": "boolean",
53
+ "description": "include edges the indexer marked cross-language (default true). Set false for a single-language view; the rows removed BY REQUEST are counted in `summary.crossLanguageExcluded` per direction, so an empty set beside a non-zero count is a request, not an empty store."
54
+ },
55
+ "timeBudgetMs": {
56
+ "type": "number",
57
+ "description": "wall-clock ceiling for the whole walk in milliseconds. DEFAULT 0 = NO clock bound: every other bound on this answer is a function of the DATA (edgeLimit, the 50-symbol per-direction fan-out, maxEdges, maxDepth), so the default answer is complete over them and byte-identical on a re-run. Pass a value only to cap latency, and know what you are buying: the ceiling is checked BETWEEN statements and never inside one — a SQLite statement cannot be interrupted — so a run that hits it returns a SAMPLE of the set. The sample is declared, never silent: `summary.timeBudgetExhausted` is true and `walk-time-budget-exhausted` is among `summary.truncationReasons`. There is no value that makes a clock-bounded answer reproducible; if you need the whole set, leave this unset"
58
+ },
59
+ "maxEdges": {
60
+ "type": "number",
61
+ "description": "hard cap on the number of edges in the WHOLE answer, across both directions and all depths (default 1000, max 5000; whole numbers only). `edgeLimit` bounds one query and this bounds the answer; their product is what a caller receives. Reaching it is declared as `total-edge-limit` with the value in `summary.maxEdges`. Raise it for a hub symbol, lower it for a quick shape check — a value below `edgeLimit` IS honored (the answer stops at it and says so), so `maxEdges` is the knob for \"just show me the shape\""
62
+ },
63
+ "edgeLimit": {
64
+ "type": "number",
65
+ "description": "Per-symbol call-site limit for one edge query (default 200, max 5000; whole numbers only). Reaching it is declared in `summary.truncated` with `summary.truncationReason` and the root's true count in `summary.callersIfUncapped`/`summary.calleesIfUncapped` — the limit is never a silent cap. Raise it to receive more sites in one answer."
66
+ }
67
+ },
68
+ "required": [
69
+ "symbol"
70
+ ]
71
+ }
72
+ ```
73
+
74
+ ## Returns
75
+
76
+ - Per-symbol dependency graph: callers and callees
77
+ - Per-edge endpoints as described nodes — `{ id, name, symbolType, file, line, isExternal }` on BOTH ends, so an edge is self-contained and no id has to be parsed or looked up in a second call
78
+ - Per-edge `rowId`: the stored `brain_relationship` row the edge came from, so every claim is auditable against the index (measured: every emitted rowId resolves to exactly one stored row, and the edge's endpoints agree with that row)
79
+ - Endpoints the index could not resolve as `ext:<name>` nodes, with `isExternal: true` — a recorded name, never a symbol guessed from that name
80
+ - Per-edge `match` (`exact` | `owner-cascade`) and `matchedMember` (`{ name, file }` when the cascade matched): whether the stored row names the symbol that was asked for, or one of its members folded onto it
81
+ - Per-edge `evidenceClass`: the stored classification when the index made one, or `null` when the `tier` was DERIVED from the edge's own stored kind and confidence — so a reader can tell a stored claim from a derivation without a second query
82
+ - Per-edge `targetBasis`: why the target may claim this identity, from the stored row — `symbol` (a stored declaration), `external` or `unresolved` (a name the index could not attach). Never inferred by this tool
83
+ - Per-edge evidence tier (structural/import_based/usage_contextual/reference_qualified/inferred) with confidence
84
+ - Per-edge call-site address: `{ kind, file, line, note }` — `call-site` is an exact stored line; `declaration` is a LABELED substitute (the calling symbol's own declaration, shown only when no call-site line is stored); `unknown` has no position at all (line null) and states why. A position is never fabricated and never reported as line 0
85
+ - `summary.edgesByType`: the composition of the RETURNED edges by stored relationship kind, counted from the emitted edges. The default filter covers every stored kind, so an answer is usually not all call edges — measured: lodash `baseConvert` 99 of 230, cobra `getCompletions` 36 of 65, rake `standard_rake_options` 72 of 76. This is how you learn the ratio without walking the edges
86
+ - `summary.maxEdges` / `totalEdgeLimitReached`: the bound on the WHOLE answer, across both directions and every depth, and whether it bit. `edgeLimit` bounds one query; this bounds the answer, and their product is what a caller receives. A requested `maxEdges` is HONORED even below `edgeLimit` — `{maxEdges: 5}` returns 5 edges with `totalEdgeLimitReached: true` and `truncationReason: "total-edge-limit"` — so it is the way to ask for a quick shape check
87
+ - `summary.truncated` / `truncationReason` / `truncationReasons`: a DECLARED BOUND bit — the walk stopped before the store ran out. `truncationReasons` is the same set as a list (`truncationReason` is its `+`-joined rendering, kept for existing consumers)
88
+ - `summary.filterNarrowed` / `filterReason`: the `relationshipTypes` argument narrowed the QUESTION — a request, not a bound. `filterNarrowed: true` with `truncated: false` means the edge sets are the whole of what was asked for (and possibly not the whole of what the store holds); `filterReason` names the narrowing (e.g. `relationshipTypes:calls|uses`) and is `null` when the default kind set ran. Asking for `relationshipTypes:["implements"]` on a symbol with none answers 0 edges with `truncated: false, filterNarrowed: true` — an empty subset, not a clipped answer
89
+ - `summary.edgeKindPriority`: the order in which kinds survive the per-symbol site limit — dependency kinds first, then lexical, then module-level, then membership. Only observable under `truncated`, published always
90
+ - `summary.scope.relationshipTypes`: the relationship kinds this run actually searched, RESOLVED from the engine's own registered list rather than restated, so the filter that produced the answer is never a restatement of a default that may have moved
91
+ - `summary.exactCallers` / `cascadedCallers` / `exactCallees` / `cascadedCallees`: DEPTH 0 only — how much of the answer is about the symbol that was asked for and how much the owner cascade folded in from its members. `deeperCallers` / `deeperCallees` hold the edges about symbols the traversal discovered, so exact + cascaded + deeper is the number of edges returned
92
+ - `summary.callerBudget` / `calleeBudget` with `callerBudgetExhausted` / `calleeBudgetExhausted`: the fan-out bound in force and whether it bit. `truncationReason` can name `caller-budget-exhausted`; this says what the budget was
93
+ - `summary.unattributed`: stored rows that NAME this symbol (or one of its owner-cascade members) but carry no source symbol, so the caller and callee queries — which join the source side — cannot show them in either direction. Per direction, depth 0 only, and `null` (not zero) when the count could not be taken. Measured on this repository's index: 11.0% of stored rows (69,171 of 631,139) carry no source symbol and 2,099 symbols answered `callers: []` while the store held incoming rows for them. The rows are counted, never fabricated into edges
94
+ - `summary.noiseFiltered` and `summary.crossLanguageExcluded`: how many stored rows this answer dropped BY POLICY (vendored, generated, fixture, snapshot, dist or build paths, and generated names) and BY REQUEST (`includeCrossLanguage: false`), per direction. Both are filters, not bounds: `truncated` stays false while they bite, which is why they are numbers rather than prose — a zero edge set beside a non-zero count is a policy or a request, not an empty result
95
+ - `summary.edgesInBothDirections`: stored rows present in both the caller and the callee list — `totalCallers + totalCallees` double-counts exactly this many
96
+ - `root.candidates` and `root.match`: how many stored symbols the requested name resolved among, and which resolution step won — `exact`, `exact-any-kind` (the name is a symbol the graph cannot traverse: a constant, a type alias), or `containment` (the answer is about a symbol whose name CONTAINS the one asked for, so the name was not itself a symbol). When `root.candidates` is greater than 1 the graph is about one of those symbols, so confirm `root` first
97
+ - `summary.maxDepth` and `summary.maxDepthReached`: the bound this run actually used and the depth it actually reached. Together they are the only way to tell "the walk finished" from "the walk stopped"
98
+ - A per-edge `depth`, which is HOW FAR THE WALK WENT before finding that row — not the shortest distance between two symbols. See Limitations
99
+ - `summary.scope`: the `direction`, `relationshipTypes` and `includeCrossLanguage` actually applied, so a narrowed traversal returning zero edges is distinguishable from a widened one that found nothing
100
+ - Declared truncation with a reason when the traversal is bounded
101
+
102
+ ## Examples
103
+
104
+ `{"symbol":"handleRequest"}`
105
+
106
+ `{"symbol":"handleRequest","maxDepth":8}`
107
+
108
+ `{"symbol":"handleRequest","direction":"callers"}`
109
+
110
+ `{"symbol":"Handler","relationshipTypes":["calls"]}`
111
+
112
+ ## Errors
113
+
114
+ - Missing symbol (a blank/whitespace-only `symbol` is refused as missing, not echoed back)
115
+ - `maxDepth` outside the declared 1-10 range, or not a whole number
116
+ - `edgeLimit` outside 1-5000, or not a whole number
117
+ - `maxEdges` outside 1-5000, or not a whole number
118
+ - `direction` outside `callers`|`callees`|`both`
119
+ - `relationshipTypes` given as an empty list, or with a blank entry (a narrowed traversal that can match nothing is refused rather than silently reinterpreted)
120
+ - `timeBudgetMs` negative (0 is accepted and means no clock bound)
121
+ - `includeCrossLanguage` not a boolean
122
+ - An option the tool does not accept (the acceptance set and the aliases are named in the error)
123
+ - Symbol not found in the index. The refusal runs the resolver's three declared steps first — exact name among traversable kinds, exact name among all kinds, then a `LIKE '%name%'` containment fallback — and the containment step is a substring scan: on the 11,440-file reference index an unknown name costs about 5 s before the not-found error. That cost is the declared fallback, not a hidden budget breach, and it is the same step that lets a near-miss (`root.match: "containment"`) answer instead of refusing
124
+
125
+ ## Limitations
126
+
127
+ - Edges reflect indexed static evidence with declared tiers; inferred edges are labeled, not proven
128
+ - An edge's `depth` is the depth of the query that found its stored row, not the shortest path between the two symbols. The traversal is depth-first and the per-row dedupe is global, so the first traversal to reach a row emits it: measured, 24% of edges on cobra, 57% on lodash and 60% on rake sit deeper than the shortest path in their own answer. No edge ever claims a SHORTER path than its answer can demonstrate, so `depth` is a valid upper bound on distance — read it as 'how far the walk got', not 'how far apart these are'. Use `direction` with a smaller `maxDepth` when the distance itself matters
129
+ - The default filter covers every stored relationship kind, so an answer mixes dependency edges with lexical references (`uses`, `references`), structural membership (`contains`) and module edges (`imports`, `exports`). `contains` means owner → member: a class reporting its own methods is structure, not dependency. Filter with `relationshipTypes` for a pure call graph, or read `edgesByType`
130
+ - Every kind competes for the per-symbol site limit, and the surviving set is decided by `edgeKindPriority` — dependency kinds first. A truncated answer therefore keeps more `calls` and fewer `contains` than an untruncated one would, and the published order is the rule that decides it
131
+ - A target the index could not resolve appears as an `ext:<name>` node rather than as a symbol. That is a real gap in the index, surfaced rather than papered over: the edge `caller --calls--> ext:name` is what the store holds, and no internal symbol is substituted for it
132
+ - A blank or whitespace-only `symbol` is refused immediately (it used to be sent through the containment scan and echoed back in the not-found message). An unknown name is refused AFTER the containment fallback runs, which scans symbol names: about 5 s on the 11,440-file reference index. Both refusals are declared here because a caller budgeting latency on this tool should not discover the scan from a timeout
133
+ - The graph reflects the index as of the last index run. This tool does not auto-refresh and does not report per-file staleness; when files changed after indexing, run: bb index --changed (or the `bb_index` tool with the project path)
134
+ - Owner-cascade attribution: a relationship stored against a class member (e.g. a call to a method) is reported toward the owner class, so caller/callee sets are a superset of exact-symbol-only matches. The superset is REDUCIBLE: every edge carries `match` (`exact` or `owner-cascade`), `matchedMember` naming the member the stored row actually referenced, and the summary carries the exact/cascade split. Measured on cobra, `Command` (182 methods) answers with 184 caller edges of which 24 name `Command` itself. Every edge also carries the `rowId` it traces to
135
+ - A target the index could not resolve is reported as an `ext:<name>` node — a NAME the index saw and could not attach to a symbol — and is never resolved by matching that name against other symbols in the project. The name is not unique there (measured on vscode, `match` names 37,876 stored symbols; on svelte, `end` names 437,570), so matching it would manufacture edges the store does not contain, crowd out the resolvable callees, and seed the traversal with a guess. `ext:` nodes are leaves: there is nothing stored about them to walk into
136
+ - Two identical calls return identical answers: the DEFAULT asks for no wall-clock bound, and every remaining bound — the per-query site limit, the fan-out budget, the total-answer bound, the depth — is a function of the data. Passing `timeBudgetMs` ends that guarantee on purpose: the ceiling is checked BETWEEN statements and never inside one (a SQLite statement is not interruptible), so a run that hits it returns a declared SAMPLE of the set — `summary.timeBudgetExhausted: true` with `walk-time-budget-exhausted` among `summary.truncationReasons`
137
+ - A symbol name is not a unique identifier. When one name matches several stored symbols the resolver takes the highest-gravity match (ties broken by lowest id, so the pick is reproducible rather than SQLite's whim) and says how many it chose between in `root.candidates` — measured: `build` in lodash resolves among 7, `code_block.go` in cobra among 50, `Command` among 1. When `root.candidates` is greater than 1 the graph is about ONE of those symbols, so confirm `root` is the one you meant before reading the edges. The other candidates are not listed
138
+ - A name containing a SQL LIKE wildcard (`%`, `_`) is matched LITERALLY, not as a pattern: it either resolves to a symbol that genuinely contains those characters or is reported not found. It never widens into a match on an unrelated symbol
139
+ - Call-site positions depend on the analyzer: an exact line is claimed only where the index stored a real line; a legacy 0 or a NULL line yields the declared `declaration` or `unknown` address instead (derived by src/intelligence/graph/line-address.ts — never an inline fallback)
140
+ - `summary.totalCallers`/`totalCallees` are the RETURNED counts, not the totals. A per-symbol site limit (`edgeLimit`, default 200, max 5000) bounds one edge query; when it is reached the answer declares `truncated` with `truncationReason` naming the bound (`caller-site-limit` / `callee-site-limit`), `cappedQueries` says how many queries hit it, and `callersIfUncapped`/`calleesIfUncapped` carry the root's true site count (null when no cap was reached — never zero). The cap fires only when rows were ACTUALLY dropped: the query asks for one row beyond the limit, so a symbol holding exactly `edgeLimit` sites is reported complete instead of falsely truncated
141
+ - An empty caller or callee set is not proof that nothing references the symbol. Three populations sit BESIDE the edge sets rather than inside them, each with its own number: rows the JOIN cannot attribute because they carry no source symbol (`summary.unattributed`), rows removed by the noise policy (`summary.noiseFiltered`), and rows removed by the `includeCrossLanguage: false` scope option (`summary.crossLanguageExcluded`). Read them before concluding that a set is empty — in the measured index one symbol had 40 stored `calls` rows and answered three callers, 38 of them in the first population
142
+ - `truncated` means a BOUND bit, and nothing else. A `relationshipTypes` filter does not set it: the filter reports through `summary.filterNarrowed` / `filterReason` instead. The two questions are separate and both are answerable — "is this the whole store?" (`truncated`) and "is this the whole of what I asked for?" (`filterNarrowed` + the counts)
143
+ - `provenance.evidenceClass` is `inferred` for the ANSWER, not for every edge: root resolution (a gravity tie-break, or the containment fallback), the owner cascade, the noise policy and any cap are constructions over stored rows, so the whole object cannot claim `structural` even when each returned edge carries one stored row. Per-row evidence is on each edge's `tier` (a stored `calls` row is `structural`) and its `evidenceClass` / `targetBasis`
144
+
145
+ ## Performance
146
+
147
+ maxDepth (default 5, max 10) bounds traversal; per-edge evidence resolution is bounded by the returned subgraph. The answer is bounded at four levels and every level is declared: the per-query site limit (`edgeLimit`, default 200, max 5000), the fan-out budget (50 symbols per direction, `callerBudget`/`calleeBudget`), the size of the WHOLE answer (`maxEdges`, default 1000, max 5000) and the depth. The third exists because the product of the first two is not a number a reader can use: measured, ripgrep's `unwrap` returns 206 edges at depth 1, 454 at depth 3 and 702 at depth 5 — 228 KiB, 504 KiB and 770 KiB rendered. Reaching `maxEdges` is declared as `total-edge-limit` with the value in `summary.maxEdges`, and a `maxEdges` below `edgeLimit` is honored rather than raised to it (`{maxEdges: 5}` answers 5 edges, `totalEdgeLimitReached: true`). The wall-clock ceiling (`timeBudgetMs`) is OFF BY DEFAULT (0): it is the ONE bound here that is a function of the clock rather than of the data, so it is the caller's decision, never the tool's — and it is checked between statements, never inside one, so a run that hits it returns a declared SAMPLE (`timeBudgetExhausted`, `walk-time-budget-exhausted`) rather than a bounded latency. Measured on this repository's reference index (11,440 files, 310,423 symbols, 820,040 stored relationships): a default call on a leaf symbol answers in 22 ms p50 / 30 ms p95, a mid symbol with 307 edges in 161 ms / 355 ms, and a hub with 672 callers in 90 ms / 125 ms. Those numbers are 4,000-6,000x faster than the same calls before the engine's cascade and arm queries were rewritten to seek their indexes (a leaf default call was 131 s p50, and mid/hub default calls did not answer inside ten minutes), and the traversal now opens the index read-only, so a question never mutates the index (open cost 19.6 ms, down from 18,714 ms for the read-write open). The engine reference (docs/GRAPH_ENGINE.md) carries the statement-level before/after and the plan shapes that hold it.
148
+
149
+ ## Safety
150
+
151
+ Read-only. It does not modify source files or index data: the traversal opens the index read-only, with no migrations, no auto-refresh and no write tool. When the index is stale, refresh it deliberately with: bb index --changed (or the bb_index tool with the project path).
152
+
153
+ ## Evidence Semantics
154
+
155
+ Nodes and edges come from the canonical graph engine over brain_symbol and brain_relationship rows; each edge carries its evidence tier, the stored classification that may have decided it (`evidenceClass`), the stored resolution basis of its target (`targetBasis`), its confidence, the `rowId` of the row it came from, and `match`/`matchedMember` saying whether the row names the traced symbol or one of its members. Every emitted rowId resolves to exactly one stored row and the edge's endpoints agree with that row. The declared totals are counted with the same query and the same noise filters as the returned edges, so a reported total is the number a caller would receive if no limit existed. Owner-cascade edges are auditable in the same way: `match` and `matchedMember` identify the member the stored row actually named, and the exact/cascade split in the summary is verified against an independent SQL count of direct-target rows on real repos (cobra: tool reports `exactCallers: 24`; oracle counts 24 direct caller rows for the same symbol). The traversal's row sets were compared before and after the query-plan rewrite over a leaf, three classes with 3-6 members, a 143-callee mid symbol and a 672-caller hub: identical rowIds for both arms and both unattributed counts (validation/tools/bb_graph/results/semantics-equivalence.json).
156
+
157
+ ## Relationship to other tools
158
+
159
+ For richer analysis (dependencies, complexity, tests, impact aggregation) use `bb_analyze`; for plain callers/callees with evidence tiers this is the tool.
160
+
161
+ ## Deep guides
162
+
163
+ - [The graph engine: what each edge means and how it is resolved](../GRAPH_ENGINE.md)
164
+
165
+ ## Staleness
166
+
167
+ This tool does not report per-file index staleness and does not auto-refresh. Output reflects the index as of the last index run; when files changed after indexing, refresh first: run: bb index --changed (or the bb_index tool with the project path — incremental, changed files only).
168
+
169
+ This page is generated from the executable registry and curated documentation. Run `npm run docs:generate` to update it, or `npx tsx script/verify-tool-docs.ts --only <tool> --write` for a single page.