@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.
- package/CHANGELOG.md +298 -0
- package/EMBEDDINGS.md +355 -0
- package/LICENSE +21 -0
- package/README.md +415 -2
- package/bin/bb +13 -0
- package/dist/analysis-exclusions-7ZJDXOVZ.js +35 -0
- package/dist/brain-index-health-Q5APTJQL.js +17 -0
- package/dist/brain-relationship-enrichment-MYR2AM4F.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -0
- package/dist/chunk-22V37YTK.js +1758 -0
- package/dist/chunk-2BSKD7O3.js +40 -0
- package/dist/chunk-2DE27DVC.js +1314 -0
- package/dist/chunk-2RRBMNBQ.js +322 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3ZTOHHSW.js +2104 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5BVAH26D.js +340 -0
- package/dist/chunk-62DHLYW6.js +3427 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-7DRPNVIN.js +782 -0
- package/dist/chunk-7MBYT777.js +174 -0
- package/dist/chunk-AHDWGM7Q.js +236 -0
- package/dist/chunk-B5GEAAYF.js +818 -0
- package/dist/chunk-BEIZZ73B.js +1479 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DWKXZ6IF.js +336 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-ENW4BTBA.js +930 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-EY5ZZUUA.js +626 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HJEWO5EV.js +247 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NO3U3FJQ.js +201 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-QKTRECYS.js +1119 -0
- package/dist/chunk-QLK6ED4Q.js +235 -0
- package/dist/chunk-R2GW4F6R.js +19 -0
- package/dist/chunk-RCMTFSEF.js +87 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SGGCSHAX.js +12050 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UQNQZXHY.js +711 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-WIG36E67.js +196 -0
- package/dist/chunk-XHK3WWGB.js +7436 -0
- package/dist/chunk-XTQM2IP3.js +2863 -0
- package/dist/chunk-ZE2LG76D.js +1296 -0
- package/dist/chunk-ZETTGPAW.js +6950 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-7XIP555Q.js +593 -0
- package/dist/database-76RP44JG.js +9 -0
- package/dist/doctor-ANGETYMN.js +395 -0
- package/dist/edit-engine-CG72LDZD.js +51 -0
- package/dist/engine-BKEOGN7N.js +11 -0
- package/dist/engine-VIW6NSKO.js +18 -0
- package/dist/fingerprint-OBLIMZLP.js +33 -0
- package/dist/framework-intelligence-3LORTPOX.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-HBSQNPTB.js +21 -0
- package/dist/graph_request-Q6NKB6N2.js +350 -0
- package/dist/impact_request-LYP672WE.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-7OVGYSGW.js +34 -0
- package/dist/index.js +159 -0
- package/dist/instance-FO6QZRLH.js +19 -0
- package/dist/language-detection-DFCAU5DZ.js +11 -0
- package/dist/lsp/server.js +4301 -0
- package/dist/mcp/server.js +132 -0
- package/dist/onboard-DIQZV2NW.js +161 -0
- package/dist/override-companion-pass-XR7MHTP7.js +117 -0
- package/dist/parallel-indexer-FK2XKZXY.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-6XPENVAA.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-ZJ3DVUGO.js +355 -0
- package/dist/real-bash-analyzer-EJAU5LW5.js +2201 -0
- package/dist/real-c-analyzer-HBD3FNNH.js +5942 -0
- package/dist/real-clean-analyzer-4EEZLPV5.js +3470 -0
- package/dist/real-clojure-analyzer-Q55EBOO2.js +2818 -0
- package/dist/real-cpp-analyzer-IVVOD25R.js +4101 -0
- package/dist/real-csharp-analyzer-ZBNLNJZJ.js +3298 -0
- package/dist/real-css-analyzer-6XRQLUN7.js +15 -0
- package/dist/real-dart-analyzer-OLR72EFW.js +3944 -0
- package/dist/real-elixir-analyzer-MQWGAH2I.js +2192 -0
- package/dist/real-erlang-analyzer-7BVPQEHO.js +1731 -0
- package/dist/real-fsharp-analyzer-C2UPFNOA.js +2390 -0
- package/dist/real-go-analyzer-UKGI43YS.js +4126 -0
- package/dist/real-graphql-analyzer-VE2EEFII.js +1442 -0
- package/dist/real-groovy-analyzer-W26VACFW.js +2285 -0
- package/dist/real-haskell-analyzer-5CVVAJRX.js +2533 -0
- package/dist/real-hcl-analyzer-5ZT4U43N.js +725 -0
- package/dist/real-html-analyzer-JWNX3C6I.js +1759 -0
- package/dist/real-java-analyzer-GU42VBGQ.js +4677 -0
- package/dist/real-javascript-analyzer-QA4QOJ6W.js +14 -0
- package/dist/real-json-analyzer-MPA6PRXL.js +1640 -0
- package/dist/real-julia-analyzer-22CWYQMN.js +2085 -0
- package/dist/real-kotlin-analyzer-QDZJLXXU.js +3956 -0
- package/dist/real-lua-analyzer-G7NHLYKS.js +1957 -0
- package/dist/real-makefile-analyzer-OCY3GT5N.js +920 -0
- package/dist/real-markdown-analyzer-TEJK2JXJ.js +1215 -0
- package/dist/real-matlab-analyzer-Q5SWLN7Z.js +480 -0
- package/dist/real-nim-analyzer-YLXSK563.js +3603 -0
- package/dist/real-objective-c-analyzer-D23ACC2R.js +1518 -0
- package/dist/real-perl-analyzer-WWTRHYDT.js +1865 -0
- package/dist/real-php-analyzer-JNKJHTCL.js +9368 -0
- package/dist/real-powershell-analyzer-XRSNQERV.js +2915 -0
- package/dist/real-protobuf-analyzer-3OKUCZ7T.js +867 -0
- package/dist/real-python-analyzer-VXAFWI72.js +4880 -0
- package/dist/real-r-analyzer-ZGAIDVI4.js +2596 -0
- package/dist/real-ruby-analyzer-X2KTLVBH.js +2429 -0
- package/dist/real-rust-analyzer-OFFHPL2A.js +3359 -0
- package/dist/real-scala-analyzer-TSTW27J6.js +3293 -0
- package/dist/real-scss-analyzer-AVWYPSOQ.js +1515 -0
- package/dist/real-solidity-analyzer-UCI2KWUB.js +2608 -0
- package/dist/real-sql-analyzer-F77ZMQXR.js +3840 -0
- package/dist/real-svelte-analyzer-SF7NRUO5.js +2532 -0
- package/dist/real-swift-analyzer-MN5QT3QN.js +2311 -0
- package/dist/real-toml-analyzer-66L7BDCQ.js +929 -0
- package/dist/real-typescript-analyzer-6KEMVDWQ.js +14 -0
- package/dist/real-vb-analyzer-VICVPOP7.js +3145 -0
- package/dist/real-vba-analyzer-HXLTTHKM.js +466 -0
- package/dist/real-vue-analyzer-QXR56CFV.js +2680 -0
- package/dist/real-xml-analyzer-YL465ZJW.js +2405 -0
- package/dist/real-yaml-analyzer-SUBCBNSI.js +3528 -0
- package/dist/real-zig-analyzer-HIQLZMGY.js +2430 -0
- package/dist/regex-search-NJKHMZNO.js +239 -0
- package/dist/rules-ACHPB7QU.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-NT5ZHYNE.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-GZJIM2J2.js +16 -0
- package/dist/taint-KXUHHPIE.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-OJDC35DI.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-Q57YMMBX.js +32 -0
- package/dist/type-inference-QMT2RMLV.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-ABP2TUL2.js +9 -0
- package/dist/wasm-NMWO3WXY.js +16 -0
- package/docs/EMBEDDINGS.md +132 -0
- package/docs/README.md +178 -0
- package/docs/data/m10-baseline.json +602 -0
- package/docs/data/m10-benchmark-baseline.json +57 -0
- package/docs/data/m10-benchmark-last-run.json +127 -0
- package/docs/data/m10-final-audit.json +215 -0
- package/docs/data/m12-final-audit.json +138 -0
- package/docs/data/m12-surface-matrix.json +50 -0
- package/docs/data/public-tools.json +1902 -0
- package/docs/indexing/DEAD-ASSETS.md +88 -0
- package/docs/indexing/INDEXER-REVIEW.md +634 -0
- package/docs/indexing/PERFORMANCE-FINDINGS.md +106 -0
- package/docs/indexing/README.md +427 -0
- package/docs/indexing/discovery-matrix.md +41 -0
- package/docs/indexing/embeddings.md +325 -0
- package/docs/indexing/environment-variables.md +77 -0
- package/docs/indexing/performance.md +323 -0
- package/docs/indexing/resume.md +345 -0
- package/docs/indexing/troubleshooting.md +391 -0
- package/docs/man/bb.1 +190 -0
- package/docs/tools/bb_analyze.md +147 -0
- package/docs/tools/bb_annotate.md +110 -0
- package/docs/tools/bb_annotations.md +106 -0
- package/docs/tools/bb_ask.md +101 -0
- package/docs/tools/bb_cross_language.md +103 -0
- package/docs/tools/bb_doc.md +169 -0
- package/docs/tools/bb_edit.md +185 -0
- package/docs/tools/bb_graph.md +169 -0
- package/docs/tools/bb_health.md +156 -0
- package/docs/tools/bb_impact.md +117 -0
- package/docs/tools/bb_index.md +108 -0
- package/docs/tools/bb_migrate.md +135 -0
- package/docs/tools/bb_patterns.md +140 -0
- package/docs/tools/bb_search.md +150 -0
- package/docs/tools/bb_security.md +100 -0
- package/docs/tools/bb_visualize.md +184 -0
- package/grammars/README.md +57 -0
- package/grammars/THIRD-PARTY-NOTICES.md +50 -0
- package/grammars/tree-sitter-c/LICENSE +21 -0
- package/grammars/tree-sitter-clojure/LICENSE.md +21 -0
- package/grammars/tree-sitter-cpp/LICENSE +21 -0
- package/grammars/tree-sitter-csharp/LICENSE +21 -0
- package/grammars/tree-sitter-css/LICENSE +21 -0
- package/grammars/tree-sitter-css/README.md +21 -0
- package/grammars/tree-sitter-dart/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/LICENSE +20 -0
- package/grammars/tree-sitter-dart-upstream/README.md +18 -0
- package/grammars/tree-sitter-elixir/LICENSE +176 -0
- package/grammars/tree-sitter-elixir/README.md +11 -0
- package/grammars/tree-sitter-fsharp/LICENSE +21 -0
- package/grammars/tree-sitter-fsharp/README.md +45 -0
- package/grammars/tree-sitter-graphql/LICENSE +21 -0
- package/grammars/tree-sitter-graphql/README.md +106 -0
- package/grammars/tree-sitter-groovy/LICENSE +21 -0
- package/grammars/tree-sitter-groovy/README.md +4 -0
- package/grammars/tree-sitter-haskell/LICENSE +21 -0
- package/grammars/tree-sitter-haskell/README.md +394 -0
- package/grammars/tree-sitter-hcl/CHANGELOG.md +102 -0
- package/grammars/tree-sitter-hcl/LICENSE +201 -0
- package/grammars/tree-sitter-hcl/README.md +39 -0
- package/grammars/tree-sitter-html/LICENSE +21 -0
- package/grammars/tree-sitter-html/README.md +21 -0
- package/grammars/tree-sitter-julia/LICENSE +21 -0
- package/grammars/tree-sitter-julia/README.md +24 -0
- package/grammars/tree-sitter-kotlin/LICENSE +9 -0
- package/grammars/tree-sitter-lua/LICENSE.md +21 -0
- package/grammars/tree-sitter-lua/README.md +29 -0
- package/grammars/tree-sitter-make/LICENSE +21 -0
- package/grammars/tree-sitter-matlab/LICENSE +19 -0
- package/grammars/tree-sitter-matlab/README.md +99 -0
- package/grammars/tree-sitter-nim/LICENSE +373 -0
- package/grammars/tree-sitter-nim/README.md +46 -0
- package/grammars/tree-sitter-objc/LICENSE +21 -0
- package/grammars/tree-sitter-objc/README.md +20 -0
- package/grammars/tree-sitter-perl/LICENSE +7 -0
- package/grammars/tree-sitter-perl/README.md +100 -0
- package/grammars/tree-sitter-powershell/LICENSE +21 -0
- package/grammars/tree-sitter-powershell/README.md +7 -0
- package/grammars/tree-sitter-proto/LICENSE +21 -0
- package/grammars/tree-sitter-proto/README.md +105 -0
- package/grammars/tree-sitter-r/LICENSE +9 -0
- package/grammars/tree-sitter-r/README.md +66 -0
- package/grammars/tree-sitter-ruby/LICENSE +22 -0
- package/grammars/tree-sitter-scss/LICENSE +21 -0
- package/grammars/tree-sitter-scss/README.md +17 -0
- package/grammars/tree-sitter-solidity/LICENSE +7 -0
- package/grammars/tree-sitter-sql/LICENSE +21 -0
- package/grammars/tree-sitter-typescript/LICENSE +21 -0
- package/grammars/tree-sitter-vba/LICENSE +21 -0
- package/grammars/tree-sitter-vba/README.md +738 -0
- package/grammars/tree-sitter-vbnet/LICENSE +21 -0
- package/grammars/tree-sitter-vbnet/README.md +322 -0
- package/grammars/tree-sitter-xml/LICENSE +19 -0
- package/grammars/tree-sitter-yaml/LICENSE +20 -0
- package/grammars/tree-sitter-yaml/README.md +21 -0
- package/package.json +184 -4
- package/wasm/dart.wasm +0 -0
- package/wasm/tree-sitter-bash.wasm +0 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-clean.wasm +0 -0
- package/wasm/tree-sitter-clojure.wasm +0 -0
- package/wasm/tree-sitter-core_schema.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-css.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-elisp.wasm +0 -0
- package/wasm/tree-sitter-elixir.wasm +0 -0
- package/wasm/tree-sitter-elm.wasm +0 -0
- package/wasm/tree-sitter-embedded_template.wasm +0 -0
- package/wasm/tree-sitter-erlang.wasm +0 -0
- package/wasm/tree-sitter-fsharp.wasm +0 -0
- package/wasm/tree-sitter-fsharp_signature.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-graphql.wasm +0 -0
- package/wasm/tree-sitter-groovy.wasm +0 -0
- package/wasm/tree-sitter-haskell.wasm +0 -0
- package/wasm/tree-sitter-hcl.wasm +0 -0
- package/wasm/tree-sitter-html.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-json.wasm +0 -0
- package/wasm/tree-sitter-json_schema.wasm +0 -0
- package/wasm/tree-sitter-julia.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-lua.wasm +0 -0
- package/wasm/tree-sitter-make.wasm +0 -0
- package/wasm/tree-sitter-markdown.wasm +0 -0
- package/wasm/tree-sitter-markdown_inline.wasm +0 -0
- package/wasm/tree-sitter-matlab.wasm +0 -0
- package/wasm/tree-sitter-nim.wasm +0 -0
- package/wasm/tree-sitter-objc.wasm +0 -0
- package/wasm/tree-sitter-ocaml.wasm +0 -0
- package/wasm/tree-sitter-perl.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-php_only.wasm +0 -0
- package/wasm/tree-sitter-powershell.wasm +0 -0
- package/wasm/tree-sitter-protobuf.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ql.wasm +0 -0
- package/wasm/tree-sitter-r.wasm +0 -0
- package/wasm/tree-sitter-rescript.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-scss.wasm +0 -0
- package/wasm/tree-sitter-solidity.wasm +0 -0
- package/wasm/tree-sitter-sql.wasm +0 -0
- package/wasm/tree-sitter-svelte.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-systemrdl.wasm +0 -0
- package/wasm/tree-sitter-terraform.wasm +0 -0
- package/wasm/tree-sitter-tlaplus.wasm +0 -0
- package/wasm/tree-sitter-toml.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vba.wasm +0 -0
- package/wasm/tree-sitter-vbnet.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
- package/wasm/tree-sitter-xml.wasm +0 -0
- package/wasm/tree-sitter-yaml.wasm +0 -0
- package/wasm/tree-sitter-zig.wasm +0 -0
- package/wasm/tree-sitter.wasm +0 -0
- package/wasm/tsx.wasm +0 -0
- package/wasm/typescript.wasm +0 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import {
|
|
2
|
+
selectSampleFiles
|
|
3
|
+
} from "./chunk-QLK6ED4Q.js";
|
|
4
|
+
import {
|
|
5
|
+
BrainDatabase
|
|
6
|
+
} from "./chunk-QKTRECYS.js";
|
|
7
|
+
|
|
8
|
+
// src/brain/brain-index-health.ts
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as crypto from "crypto";
|
|
11
|
+
async function runIndexHealthCheck(projectId, options = {}) {
|
|
12
|
+
const rawDb = await BrainDatabase.getRawInstance(projectId, { readonly: true });
|
|
13
|
+
const diskScan = options.diskScan ?? "sample";
|
|
14
|
+
const checks = [];
|
|
15
|
+
checks.push(await checkByteBounds(rawDb, projectId));
|
|
16
|
+
checks.push(await checkLineBounds(rawDb, projectId));
|
|
17
|
+
checks.push(await checkOrphanTargets(rawDb, projectId));
|
|
18
|
+
checks.push(await checkDanglingSources(rawDb, projectId));
|
|
19
|
+
checks.push(...await checkOrphanOwners(rawDb, projectId));
|
|
20
|
+
const preResolved = options.preResolved && options.preResolved.total === totalFiles(rawDb, projectId) ? options.preResolved : null;
|
|
21
|
+
const drift = await checkHashDrift(rawDb, projectId, diskScan, preResolved);
|
|
22
|
+
checks.push(drift.check);
|
|
23
|
+
checks.push(await checkMissingFiles(rawDb, projectId, diskScan, preResolved));
|
|
24
|
+
const deferredEmbeddingCoverage = options.deferEmbeddingCoverage === true;
|
|
25
|
+
if (!deferredEmbeddingCoverage) checks.push(await checkEmbeddingCoverage(rawDb, projectId));
|
|
26
|
+
checks.push(await checkSyntaxErrorFiles(rawDb, projectId));
|
|
27
|
+
checks.push(await checkWalSize(rawDb, projectId));
|
|
28
|
+
checks.push(await checkEmptyFiles(rawDb, projectId));
|
|
29
|
+
checks.push(await checkRelationshipRatio(rawDb, projectId));
|
|
30
|
+
const completeness = checkIndexCompleteness(rawDb, projectId);
|
|
31
|
+
checks.push(completeness.check);
|
|
32
|
+
const staleness = drift.classification;
|
|
33
|
+
const scored = checks.filter((c) => c.kind !== "informational");
|
|
34
|
+
const informational = checks.filter((c) => c.kind === "informational");
|
|
35
|
+
const ok = scored.filter((c) => c.status === "ok").length;
|
|
36
|
+
const warn = scored.filter((c) => c.status === "warn").length;
|
|
37
|
+
const error = scored.filter((c) => c.status === "error").length;
|
|
38
|
+
const overallHealth = Math.round(ok / Math.max(scored.length, 1) * 100);
|
|
39
|
+
const populated = staleness.total > 0 || completeness.totalSymbols > 0;
|
|
40
|
+
const verdict = !populated ? "empty" : error > 0 ? "broken" : warn > 0 ? "degraded" : "healthy";
|
|
41
|
+
return {
|
|
42
|
+
projectId,
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
overallHealth,
|
|
45
|
+
deferredEmbeddingCoverage,
|
|
46
|
+
verdict,
|
|
47
|
+
indexStatus: completeness.indexStatus,
|
|
48
|
+
staleness,
|
|
49
|
+
checks,
|
|
50
|
+
summary: { ok, warn, error, total: checks.length, scored: scored.length, informational: informational.length }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function checkIndexCompleteness(rawDb, projectId) {
|
|
54
|
+
const stored = rawDb.prepare(`
|
|
55
|
+
SELECT status, total_files, indexed_files
|
|
56
|
+
FROM brain_index_status WHERE project_id = ?
|
|
57
|
+
`).get(projectId);
|
|
58
|
+
const totalSymbols = rawDb.prepare(`SELECT COUNT(*) as count FROM brain_symbol WHERE project_id = ?`).get(projectId)?.count ?? 0;
|
|
59
|
+
const status = typeof stored?.status === "string" ? stored.status : null;
|
|
60
|
+
const totalFilesRecorded = typeof stored?.total_files === "number" ? stored.total_files : null;
|
|
61
|
+
const indexedFilesRecorded = typeof stored?.indexed_files === "number" ? stored.indexed_files : null;
|
|
62
|
+
const shortfall = totalFilesRecorded !== null && indexedFilesRecorded !== null && indexedFilesRecorded < totalFilesRecorded ? totalFilesRecorded - indexedFilesRecorded : null;
|
|
63
|
+
const indexStatus = {
|
|
64
|
+
status,
|
|
65
|
+
totalFiles: totalFilesRecorded,
|
|
66
|
+
indexedFiles: indexedFilesRecorded,
|
|
67
|
+
shortfall,
|
|
68
|
+
recorded: !!stored
|
|
69
|
+
};
|
|
70
|
+
const parts = [];
|
|
71
|
+
let status_ = "ok";
|
|
72
|
+
const errNote = indexedErrorsNote(rawDb, projectId);
|
|
73
|
+
if (!stored) {
|
|
74
|
+
status_ = "warn";
|
|
75
|
+
parts.push(
|
|
76
|
+
"No brain_index_status row exists for this project, so how complete the index is was never recorded. UNRECORDED is not the same as complete \u2014 it cannot be checked, and a reader who treats it as a pass is reading an absence as a measurement."
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
if (status === "partial") {
|
|
80
|
+
status_ = "error";
|
|
81
|
+
parts.push(
|
|
82
|
+
`The indexer recorded this run's status as "partial"${errNote}. An index marked partial is INCOMPLETE, and every count this report makes describes only the part that was written.`
|
|
83
|
+
);
|
|
84
|
+
} else if (status && status !== "complete") {
|
|
85
|
+
status_ = "warn";
|
|
86
|
+
parts.push(`The indexer recorded this run's status as "${status}", not "complete".`);
|
|
87
|
+
}
|
|
88
|
+
if (shortfall !== null && shortfall > 0) {
|
|
89
|
+
status_ = "error";
|
|
90
|
+
parts.push(
|
|
91
|
+
`${shortfall} of ${totalFilesRecorded} discovered files were never written (${indexedFilesRecorded} of ${totalFilesRecorded} indexed), so the index does not cover the project` + // The status field and the counts can disagree, and when they do the
|
|
92
|
+
// user needs to know which one to believe. health-provider.ts already
|
|
93
|
+
// detects this ("INCOMPLETE even though the status field says
|
|
94
|
+
// complete"); saying it here too keeps the two reports consistent.
|
|
95
|
+
(status === "complete" ? ` \u2014 note the status field says "complete" while the counts say otherwise; the counts are the measurement.` : ".")
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (parts.length === 0) {
|
|
100
|
+
parts.push(
|
|
101
|
+
`The indexer recorded this run as "${status ?? "complete"}" with ${indexedFilesRecorded ?? "?"} of ${totalFilesRecorded ?? "?"} discovered files written.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
check: {
|
|
106
|
+
name: "Index Completeness",
|
|
107
|
+
status: status_,
|
|
108
|
+
value: shortfall ?? 0,
|
|
109
|
+
total: totalFilesRecorded ?? 0,
|
|
110
|
+
threshold: { warn: 0, error: 1 },
|
|
111
|
+
description: parts.join(" "),
|
|
112
|
+
remediation: status_ === "ok" ? void 0 : `Re-index from the project root: bb index --force (full rebuild), then bb index --post if a derived phase is what failed. If the shortfall persists on a clean rebuild, this is an indexer bug \u2014 report it with the numbers above.`
|
|
113
|
+
},
|
|
114
|
+
indexStatus,
|
|
115
|
+
totalSymbols
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function indexedErrorsNote(rawDb, projectId) {
|
|
119
|
+
try {
|
|
120
|
+
const raw = rawDb.prepare(`SELECT indexing_errors FROM brain_index_status WHERE project_id = ?`).get(projectId);
|
|
121
|
+
const parsed = raw?.indexing_errors ? JSON.parse(String(raw.indexing_errors)) : null;
|
|
122
|
+
const n = Array.isArray(parsed) ? parsed.length : null;
|
|
123
|
+
return n ? ` \u2014 ${n} file(s) errored during the run` : "";
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
function stalenessFromReasons(total, reasons) {
|
|
129
|
+
const fresh = (reasons["metadata-fast-path"] ?? 0) + (reasons["hash-match"] ?? 0);
|
|
130
|
+
return {
|
|
131
|
+
fresh,
|
|
132
|
+
stale: reasons["hash-differs"] ?? 0,
|
|
133
|
+
missing: (reasons["file-gone"] ?? 0) + (reasons["unreadable"] ?? 0),
|
|
134
|
+
unclassified: reasons["no-row"] ?? 0,
|
|
135
|
+
sampleSize: fresh + (reasons["hash-differs"] ?? 0) + (reasons["file-gone"] ?? 0) + (reasons["unreadable"] ?? 0) + (reasons["no-row"] ?? 0),
|
|
136
|
+
total,
|
|
137
|
+
complete: true,
|
|
138
|
+
basis: "exact",
|
|
139
|
+
verifiedByContent: reasons["hash-match"] ?? 0
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function checkByteBounds(rawDb, projectId) {
|
|
143
|
+
const row = rawDb.prepare(`
|
|
144
|
+
SELECT
|
|
145
|
+
COUNT(*) as total,
|
|
146
|
+
SUM(CASE WHEN byte_start IS NULL OR byte_end IS NULL THEN 1 ELSE 0 END) as null_count
|
|
147
|
+
FROM brain_symbol WHERE project_id = ?
|
|
148
|
+
`).get(projectId);
|
|
149
|
+
const total = row?.total ?? 0;
|
|
150
|
+
const nullCount = row?.null_count ?? 0;
|
|
151
|
+
const pct = total > 0 ? nullCount / total * 100 : 0;
|
|
152
|
+
let sample = [];
|
|
153
|
+
if (nullCount > 0) {
|
|
154
|
+
const rows = rawDb.prepare(`
|
|
155
|
+
SELECT name, file_id FROM brain_symbol
|
|
156
|
+
WHERE project_id = ? AND (byte_start IS NULL OR byte_end IS NULL)
|
|
157
|
+
LIMIT 5
|
|
158
|
+
`).all(projectId);
|
|
159
|
+
sample = rows.map((r) => `${r.name} (${r.file_id})`);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
name: "Byte Bounds Coverage",
|
|
163
|
+
status: pct > 5 ? "error" : pct > 1 ? "warn" : "ok",
|
|
164
|
+
value: nullCount,
|
|
165
|
+
total,
|
|
166
|
+
percentage: Math.round((100 - pct) * 10) / 10,
|
|
167
|
+
threshold: { warn: 1, error: 5 },
|
|
168
|
+
description: `${nullCount} of ${total} symbols have NULL byte bounds. Without byte bounds, bb_edit cannot safely mutate these symbols.`,
|
|
169
|
+
remediation: nullCount > 0 ? "Run bb_health with refresh=true and force=true to reindex affected files." : void 0,
|
|
170
|
+
sample: sample.length > 0 ? sample : void 0
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async function checkLineBounds(rawDb, projectId) {
|
|
174
|
+
const row = rawDb.prepare(`
|
|
175
|
+
SELECT
|
|
176
|
+
COUNT(*) as total,
|
|
177
|
+
SUM(CASE WHEN line_start IS NULL THEN 1 ELSE 0 END) as null_count
|
|
178
|
+
FROM brain_symbol WHERE project_id = ?
|
|
179
|
+
`).get(projectId);
|
|
180
|
+
const total = row?.total ?? 0;
|
|
181
|
+
const nullCount = row?.null_count ?? 0;
|
|
182
|
+
const pct = total > 0 ? nullCount / total * 100 : 0;
|
|
183
|
+
return {
|
|
184
|
+
name: "Line Bounds Coverage",
|
|
185
|
+
status: pct > 10 ? "error" : pct > 2 ? "warn" : "ok",
|
|
186
|
+
value: nullCount,
|
|
187
|
+
total,
|
|
188
|
+
percentage: Math.round((100 - pct) * 10) / 10,
|
|
189
|
+
threshold: { warn: 2, error: 10 },
|
|
190
|
+
description: `${nullCount} symbols have NULL line_start. Tools fall back to file-level operations for these.`,
|
|
191
|
+
remediation: nullCount > 0 ? "These are typically top-level or auto-generated symbols. Generally safe." : void 0
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function checkOrphanTargets(rawDb, projectId) {
|
|
195
|
+
const row = rawDb.prepare(`
|
|
196
|
+
SELECT COUNT(*) as count FROM brain_relationship r
|
|
197
|
+
WHERE r.project_id = ?
|
|
198
|
+
AND r.target_symbol_id IS NOT NULL
|
|
199
|
+
AND r.target_symbol_id != ''
|
|
200
|
+
AND NOT EXISTS (SELECT 1 FROM brain_symbol s WHERE s.id = r.target_symbol_id AND s.project_id = r.project_id)
|
|
201
|
+
`).get(projectId);
|
|
202
|
+
const orphanCount = row?.count ?? 0;
|
|
203
|
+
let sample = [];
|
|
204
|
+
if (orphanCount > 0) {
|
|
205
|
+
const rows = rawDb.prepare(`
|
|
206
|
+
SELECT r.id, r.target_symbol_id, r.source_symbol_id FROM brain_relationship r
|
|
207
|
+
WHERE r.project_id = ?
|
|
208
|
+
AND r.target_symbol_id IS NOT NULL
|
|
209
|
+
AND r.target_symbol_id != ''
|
|
210
|
+
AND NOT EXISTS (SELECT 1 FROM brain_symbol s WHERE s.id = r.target_symbol_id AND s.project_id = r.project_id)
|
|
211
|
+
LIMIT 5
|
|
212
|
+
`).all(projectId);
|
|
213
|
+
sample = rows.map((r) => `rel ${r.id}: target ${r.target_symbol_id} (from ${r.source_symbol_id})`);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
name: "Orphan Target Relationships",
|
|
217
|
+
status: orphanCount > 100 ? "error" : orphanCount > 10 ? "warn" : "ok",
|
|
218
|
+
value: orphanCount,
|
|
219
|
+
threshold: { warn: 10, error: 100 },
|
|
220
|
+
description: `${orphanCount} relationships point to symbols that no longer exist. These inflate bb_analyze graph results but lead nowhere.`,
|
|
221
|
+
remediation: orphanCount > 0 ? "Run bb_health with refresh=true and force=true to rebuild the index." : void 0,
|
|
222
|
+
sample: sample.length > 0 ? sample : void 0
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async function checkDanglingSources(rawDb, projectId) {
|
|
226
|
+
const row = rawDb.prepare(`
|
|
227
|
+
SELECT COUNT(*) as count FROM brain_relationship r
|
|
228
|
+
WHERE r.project_id = ?
|
|
229
|
+
AND r.source_symbol_id IS NOT NULL AND r.source_symbol_id != ''
|
|
230
|
+
AND NOT EXISTS (SELECT 1 FROM brain_symbol s WHERE s.id = r.source_symbol_id AND s.project_id = r.project_id)
|
|
231
|
+
`).get(projectId);
|
|
232
|
+
const danglingCount = row?.count ?? 0;
|
|
233
|
+
return {
|
|
234
|
+
name: "Dangling Source Relationships",
|
|
235
|
+
status: danglingCount > 100 ? "error" : danglingCount > 10 ? "warn" : "ok",
|
|
236
|
+
value: danglingCount,
|
|
237
|
+
threshold: { warn: 10, error: 100 },
|
|
238
|
+
description: `${danglingCount} relationships have a non-empty source_symbol_id that matches no stored symbol. These are dead edges in the call graph. (External-source edges with a NULL source_symbol_id are legitimate and not counted.)`,
|
|
239
|
+
remediation: danglingCount > 0 ? "Re-index the affected files (`bb index <dir>`) so edges are written against stored symbols. A --force rebuild only helps if the danglers came from an older indexer version; if they persist on a fresh index, this is an indexer bug \u2014 report it with the sample ids." : void 0
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function checkOrphanOwners(rawDb, projectId) {
|
|
243
|
+
const genuine = rawDb.prepare(`
|
|
244
|
+
SELECT COUNT(*) as count FROM brain_symbol s
|
|
245
|
+
WHERE s.project_id = ?
|
|
246
|
+
AND s.owner_symbol_id IS NOT NULL
|
|
247
|
+
AND s.owner_symbol_id != ''
|
|
248
|
+
AND s.owner_symbol_id NOT LIKE '%:0'
|
|
249
|
+
AND NOT EXISTS (SELECT 1 FROM brain_symbol p WHERE p.id = s.owner_symbol_id AND p.project_id = s.project_id)
|
|
250
|
+
`).get(projectId);
|
|
251
|
+
const synthetic = rawDb.prepare(`
|
|
252
|
+
SELECT COUNT(*) as count FROM brain_symbol s
|
|
253
|
+
WHERE s.project_id = ?
|
|
254
|
+
AND s.owner_symbol_id IS NOT NULL
|
|
255
|
+
AND s.owner_symbol_id != ''
|
|
256
|
+
AND s.owner_symbol_id LIKE '%:0'
|
|
257
|
+
AND NOT EXISTS (SELECT 1 FROM brain_symbol p WHERE p.id = s.owner_symbol_id AND p.project_id = s.project_id)
|
|
258
|
+
`).get(projectId);
|
|
259
|
+
const samples = rawDb.prepare(`
|
|
260
|
+
SELECT s.owner_symbol_id as oid, COUNT(*) as c FROM brain_symbol s
|
|
261
|
+
WHERE s.project_id = ?
|
|
262
|
+
AND s.owner_symbol_id IS NOT NULL AND s.owner_symbol_id != ''
|
|
263
|
+
AND NOT EXISTS (SELECT 1 FROM brain_symbol p WHERE p.id = s.owner_symbol_id AND p.project_id = s.project_id)
|
|
264
|
+
GROUP BY s.owner_symbol_id ORDER BY c DESC LIMIT 5
|
|
265
|
+
`).all(projectId);
|
|
266
|
+
const genuineCount = genuine?.count ?? 0;
|
|
267
|
+
const syntheticCount = synthetic?.count ?? 0;
|
|
268
|
+
const sample = samples.map((r) => `${r.c}x ${String(r.oid).slice(-60)}`);
|
|
269
|
+
return [
|
|
270
|
+
{
|
|
271
|
+
name: "Orphan Owner References",
|
|
272
|
+
status: genuineCount > 100 ? "error" : genuineCount > 10 ? "warn" : "ok",
|
|
273
|
+
value: genuineCount,
|
|
274
|
+
threshold: { warn: 10, error: 100 },
|
|
275
|
+
description: `${genuineCount} symbols reference an owner_symbol_id that is not a synthetic container and matches no stored symbol. This breaks containment chains in bb_trace and the LSP document tree.`,
|
|
276
|
+
remediation: genuineCount > 0 ? "Re-index the affected files (`bb index <dir>`); if the count does not drop to zero on a fresh index, this is an indexer bug \u2014 report it with the sample ids. A --force rebuild is NOT guaranteed to fix this." : void 0,
|
|
277
|
+
sample: sample.length > 0 ? sample : void 0
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "Synthetic Container References",
|
|
281
|
+
status: "ok",
|
|
282
|
+
// M64.I3: informational. This population is by-design and its status does
|
|
283
|
+
// not depend on the data, so scoring it added a constant +7.7 points to
|
|
284
|
+
// every project's overallHealth and put a floor under the score that no
|
|
285
|
+
// number of real failures could cross.
|
|
286
|
+
kind: "informational",
|
|
287
|
+
value: syntheticCount,
|
|
288
|
+
threshold: { warn: 1e7, error: 1e7 },
|
|
289
|
+
description: `${syntheticCount} symbols carry owner ids of the form <file>:<name>:0 \u2014 module/synthetic containers the pre-M51 indexer wrote as an owner fallback. By design they never resolve (no stored row has line 0) and every consumer JOINs on owner_symbol_id, so they are invisible to reads. Informational, not a defect, and excluded from overallHealth.`,
|
|
290
|
+
remediation: syntheticCount > 0 ? "Optional: re-run `bb index` \u2014 rows written by pre-M51 indexers are rewritten with a resolvable owner (or NULL) and this count drops to 0." : void 0,
|
|
291
|
+
sample: sample.length > 0 ? sample : void 0
|
|
292
|
+
}
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
function totalFiles(rawDb, projectId) {
|
|
296
|
+
return rawDb.prepare(`SELECT COUNT(*) as count FROM brain_file WHERE project_id = ?`).get(projectId)?.count ?? 0;
|
|
297
|
+
}
|
|
298
|
+
async function checkHashDrift(rawDb, projectId, diskScan, preResolved) {
|
|
299
|
+
const total = totalFiles(rawDb, projectId);
|
|
300
|
+
if (preResolved) {
|
|
301
|
+
const drift = preResolved.reasons["hash-differs"] ?? 0;
|
|
302
|
+
const byContent = preResolved.reasons["hash-match"] ?? 0;
|
|
303
|
+
const pctAll = total > 0 ? drift / total * 100 : 0;
|
|
304
|
+
return {
|
|
305
|
+
check: {
|
|
306
|
+
name: "Hash Drift (Stale Files)",
|
|
307
|
+
status: pctAll > 10 ? "error" : pctAll > 3 ? "warn" : "ok",
|
|
308
|
+
value: drift,
|
|
309
|
+
total,
|
|
310
|
+
percentage: Math.round((100 - pctAll) * 10) / 10,
|
|
311
|
+
threshold: { warn: 3, error: 10 },
|
|
312
|
+
description: `${drift} of ${total} indexed files have changed on disk since indexing. EXACT: every indexed file was resolved against disk, and these verdicts are the SAME ones bb_health's own freshness scan just produced \u2014 this check reads no files of its own, so the two can never disagree and the corpus is not hashed twice. ${byContent} file(s) were verified by content hash; the rest by mtime+size.`,
|
|
313
|
+
remediation: drift > 0 ? "Run bb_health with refresh=true to reindex changed files." : void 0
|
|
314
|
+
},
|
|
315
|
+
classification: stalenessFromReasons(total, preResolved.reasons)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const allFiles = rawDb.prepare(`
|
|
319
|
+
SELECT id, path, relative_path, file_hash FROM brain_file WHERE project_id = ?
|
|
320
|
+
`).all(projectId);
|
|
321
|
+
const files = diskScan === "complete" ? selectSampleFiles(allFiles, Number.MAX_SAFE_INTEGER) : selectSampleFiles(allFiles, 200);
|
|
322
|
+
let driftCount = 0;
|
|
323
|
+
let freshCount = 0;
|
|
324
|
+
let goneCount = 0;
|
|
325
|
+
let unreadableCount = 0;
|
|
326
|
+
const driftSamples = [];
|
|
327
|
+
for (const f of files) {
|
|
328
|
+
let buf;
|
|
329
|
+
try {
|
|
330
|
+
buf = fs.readFileSync(f.path);
|
|
331
|
+
} catch (e) {
|
|
332
|
+
if (e?.code === "ENOENT") goneCount++;
|
|
333
|
+
else unreadableCount++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const hash = crypto.createHash("sha256").update(buf).digest("hex").slice(0, 16);
|
|
337
|
+
if (hash === f.file_hash) {
|
|
338
|
+
freshCount++;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
driftCount++;
|
|
342
|
+
if (driftSamples.length < 5) driftSamples.push(f.relative_path);
|
|
343
|
+
}
|
|
344
|
+
const sampleSize = files.length;
|
|
345
|
+
const pct = sampleSize > 0 ? driftCount / sampleSize * 100 : 0;
|
|
346
|
+
const isCompleteScan = sampleSize >= total;
|
|
347
|
+
const estimatedTotal = isCompleteScan ? driftCount : total > 0 ? Math.round(driftCount / Math.max(sampleSize, 1) * total) : 0;
|
|
348
|
+
return {
|
|
349
|
+
check: {
|
|
350
|
+
name: "Hash Drift (Stale Files)",
|
|
351
|
+
status: pct > 10 ? "error" : pct > 3 ? "warn" : "ok",
|
|
352
|
+
value: driftCount,
|
|
353
|
+
total: sampleSize,
|
|
354
|
+
percentage: Math.round((100 - pct) * 10) / 10,
|
|
355
|
+
threshold: { warn: 3, error: 10 },
|
|
356
|
+
description: isCompleteScan ? `${driftCount} of ${sampleSize} indexed files have changed on disk since indexing. EXACT: every indexed file was content-hashed, so this is the project's drift count and this status is a measurement, not an estimate.` : `${driftCount} of ${sampleSize} sampled files have changed on disk since indexing (estimated ${estimatedTotal} total). ESTIMATE: a deterministic unbiased sample of every indexed file \u2014 reproducible and independent of file ordering, but the rate and therefore this status carry sampling error. bb_health integrity=true runs this as a complete scan.`,
|
|
357
|
+
remediation: driftCount > 0 ? "Run bb_health with refresh=true to reindex changed files." : void 0,
|
|
358
|
+
sample: driftSamples.length > 0 ? driftSamples : void 0
|
|
359
|
+
},
|
|
360
|
+
classification: {
|
|
361
|
+
fresh: freshCount,
|
|
362
|
+
stale: driftCount,
|
|
363
|
+
missing: goneCount + unreadableCount,
|
|
364
|
+
unclassified: 0,
|
|
365
|
+
sampleSize,
|
|
366
|
+
total,
|
|
367
|
+
complete: isCompleteScan,
|
|
368
|
+
basis: isCompleteScan ? "exact" : "sample",
|
|
369
|
+
verifiedByContent: freshCount + driftCount
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
async function checkMissingFiles(rawDb, projectId, diskScan, preResolved) {
|
|
374
|
+
const total = totalFiles(rawDb, projectId);
|
|
375
|
+
if (preResolved) {
|
|
376
|
+
const gone = preResolved.reasons["file-gone"] ?? 0;
|
|
377
|
+
const denied = preResolved.reasons["unreadable"] ?? 0;
|
|
378
|
+
const missing2 = gone + denied;
|
|
379
|
+
return {
|
|
380
|
+
name: "Files Missing from Disk",
|
|
381
|
+
status: missing2 > 10 ? "error" : missing2 > 2 ? "warn" : "ok",
|
|
382
|
+
value: missing2,
|
|
383
|
+
total,
|
|
384
|
+
percentage: total > 0 ? Math.round((1 - missing2 / total) * 1e3) / 10 : 100,
|
|
385
|
+
threshold: { warn: 2, error: 10 },
|
|
386
|
+
description: `${missing2} of ${total} indexed files no longer exist on disk. EXACT: ${gone} deleted, ${denied} unreadable, both from the freshness scan this report already ran. Tools will fail when trying to read these.`,
|
|
387
|
+
remediation: missing2 > 0 ? "Run bb_health with refresh=true to remove deleted files from the index." : void 0
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const allFiles = rawDb.prepare(`
|
|
391
|
+
SELECT id, path, relative_path FROM brain_file WHERE project_id = ?
|
|
392
|
+
`).all(projectId);
|
|
393
|
+
const files = diskScan === "complete" ? selectSampleFiles(allFiles, Number.MAX_SAFE_INTEGER) : selectSampleFiles(allFiles, 100);
|
|
394
|
+
let missing = 0;
|
|
395
|
+
const missingSamples = [];
|
|
396
|
+
for (const f of files) {
|
|
397
|
+
if (!fs.existsSync(f.path)) {
|
|
398
|
+
missing++;
|
|
399
|
+
if (missingSamples.length < 5) missingSamples.push(f.path);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const isCompleteScan = files.length >= total;
|
|
403
|
+
const estimatedMissing = isCompleteScan ? missing : total > 0 ? Math.round(missing / Math.min(files.length, total) * total) : 0;
|
|
404
|
+
return {
|
|
405
|
+
name: "Files Missing from Disk",
|
|
406
|
+
status: missing > 10 ? "error" : missing > 2 ? "warn" : "ok",
|
|
407
|
+
value: missing,
|
|
408
|
+
total: files.length,
|
|
409
|
+
percentage: files.length > 0 ? Math.round((1 - missing / files.length) * 1e3) / 10 : 100,
|
|
410
|
+
threshold: { warn: 2, error: 10 },
|
|
411
|
+
description: isCompleteScan ? `${missing} of ${files.length} indexed files no longer exist on disk. EXACT: every indexed file was checked. Tools will fail when trying to read these.` : `${missing} of ${files.length} sampled files no longer exist on disk. Estimated ${estimatedMissing} total missing. ESTIMATE: a deterministic unbiased sample of every indexed file (reproducible; independent of file ordering). bb_health integrity=true runs this as a complete scan.`,
|
|
412
|
+
remediation: missing > 0 ? "Run bb_health with refresh=true to remove deleted files from the index." : void 0,
|
|
413
|
+
sample: missingSamples.length > 0 ? missingSamples : void 0
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
async function checkEmbeddingCoverage(rawDb, projectId) {
|
|
417
|
+
const hasTable = rawDb.prepare(`
|
|
418
|
+
SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name='brain_semantic_embedding'
|
|
419
|
+
`).get();
|
|
420
|
+
if (!hasTable?.count) {
|
|
421
|
+
return {
|
|
422
|
+
name: "Embedding Coverage",
|
|
423
|
+
status: "warn",
|
|
424
|
+
value: 0,
|
|
425
|
+
total: 0,
|
|
426
|
+
percentage: 0,
|
|
427
|
+
threshold: { warn: 50, error: 20 },
|
|
428
|
+
description: "Semantic embedding table does not exist. Semantic search is keyword-only.",
|
|
429
|
+
// M64.I4: this said `bb2 brain --post`. There is no `bb2` binary and no
|
|
430
|
+
// `brain` subcommand — the CLI registers tool/index/setup/show/doctor/
|
|
431
|
+
// stats/status/projects/search/health/onboard/mcp/version, so the advice
|
|
432
|
+
// sent a user to a command that cannot be typed. The real interface is
|
|
433
|
+
// the bb_index tool's post mode, which re-runs the derived phases.
|
|
434
|
+
remediation: 'Generate embeddings with the bb_index tool: { path: "<project path>", mode: "post" } (CLI: bb index "<project path>").'
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const totalRow = rawDb.prepare(`
|
|
438
|
+
SELECT COUNT(*) as count FROM brain_symbol WHERE project_id = ?
|
|
439
|
+
`).get(projectId);
|
|
440
|
+
const totalSymbols = totalRow?.count ?? 0;
|
|
441
|
+
const embedRow = rawDb.prepare(`
|
|
442
|
+
SELECT COUNT(DISTINCT entity_id) as count FROM brain_semantic_embedding WHERE project_id = ?
|
|
443
|
+
`).get(projectId);
|
|
444
|
+
const embedded = embedRow?.count ?? 0;
|
|
445
|
+
const targetKinds = rawDb.prepare(`
|
|
446
|
+
SELECT entity_type AS kind, COUNT(DISTINCT entity_id) AS c FROM brain_semantic_embedding
|
|
447
|
+
WHERE project_id = ? GROUP BY entity_type ORDER BY c DESC
|
|
448
|
+
`).all(projectId);
|
|
449
|
+
let targetSymbols = 0;
|
|
450
|
+
if (targetKinds.length > 0) {
|
|
451
|
+
const placeholders = targetKinds.map(() => "?").join(",");
|
|
452
|
+
const targetRow = rawDb.prepare(
|
|
453
|
+
`SELECT COUNT(*) as count FROM brain_symbol WHERE project_id = ? AND symbol_type IN (${placeholders})`
|
|
454
|
+
).get(projectId, ...targetKinds.map((k) => String(k.kind)));
|
|
455
|
+
targetSymbols = targetRow?.count ?? 0;
|
|
456
|
+
}
|
|
457
|
+
const pct = targetSymbols > 0 ? embedded / targetSymbols * 100 : 0;
|
|
458
|
+
const overallShare = totalSymbols > 0 ? embedded / totalSymbols * 100 : 0;
|
|
459
|
+
const kindDesc = targetKinds.map((k) => `${k.kind}=${k.c}`).join(" ");
|
|
460
|
+
if (embedded === 0) {
|
|
461
|
+
return {
|
|
462
|
+
name: "Embedding Coverage",
|
|
463
|
+
status: "error",
|
|
464
|
+
value: 0,
|
|
465
|
+
total: targetSymbols,
|
|
466
|
+
percentage: 0,
|
|
467
|
+
threshold: { warn: 50, error: 20 },
|
|
468
|
+
description: `The embedding table exists for this project but holds no vectors, so semantic search is keyword-only. ${totalSymbols} symbols are indexed.`,
|
|
469
|
+
remediation: 'Generate embeddings with the bb_index tool: { path: "<project path>", mode: "post" } (CLI: bb index "<project path>").'
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const models = rawDb.prepare(`
|
|
473
|
+
SELECT embedding_model, COUNT(*) as count FROM brain_semantic_embedding
|
|
474
|
+
WHERE project_id = ?
|
|
475
|
+
GROUP BY embedding_model
|
|
476
|
+
`).all(projectId);
|
|
477
|
+
const modelDesc = models.map((m) => `${m.embedding_model}: ${m.count}`).join(", ");
|
|
478
|
+
return {
|
|
479
|
+
name: "Embedding Coverage",
|
|
480
|
+
status: pct < 20 ? "error" : pct < 50 ? "warn" : "ok",
|
|
481
|
+
// M64.I4: `value`/`total` are now the TARGETED population (distinct
|
|
482
|
+
// embedded entities over the symbols of the kinds the embedder reaches),
|
|
483
|
+
// so the check's own numbers are the actionable ones. The all-symbols share
|
|
484
|
+
// is carried in the description rather than folded into the verdict.
|
|
485
|
+
value: embedded,
|
|
486
|
+
total: targetSymbols,
|
|
487
|
+
percentage: Math.round(pct * 10) / 10,
|
|
488
|
+
threshold: { warn: 50, error: 20 },
|
|
489
|
+
description: `${embedded} of ${targetSymbols} embeddable symbols have vectors (${Math.round(overallShare * 10) / 10}% of all ${totalSymbols} symbols; ${kindDesc || "no kinds embedded"}). Counts DISTINCT entities, not rows, so a symbol embedded by several models counts once. Kinds absent from the embedding table are never attempted \u2014 that share of the gap is not a coverage failure. Models: ${modelDesc || "none"}.`,
|
|
490
|
+
remediation: pct < 50 ? `Generate embeddings with the bb_index tool: { path: "<project path>", mode: "post" } (CLI: bb index "<project path>"). This closes the gap over the kinds already targeted; kinds that never appear in the embedding table need an analyzer that emits a snippet for them, not a reindex.` : void 0
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function checkSyntaxErrorFiles(rawDb, projectId) {
|
|
494
|
+
const totalRow = rawDb.prepare(`
|
|
495
|
+
SELECT COUNT(*) as count FROM brain_file WHERE project_id = ?
|
|
496
|
+
`).get(projectId);
|
|
497
|
+
const total = totalRow?.count ?? 0;
|
|
498
|
+
const errorRow = rawDb.prepare(`
|
|
499
|
+
SELECT COUNT(*) as count FROM brain_file WHERE project_id = ? AND has_syntax_errors = 1
|
|
500
|
+
`).get(projectId);
|
|
501
|
+
const errorCount = errorRow?.count ?? 0;
|
|
502
|
+
let sample = [];
|
|
503
|
+
if (errorCount > 0) {
|
|
504
|
+
const rows = rawDb.prepare(`
|
|
505
|
+
SELECT relative_path, language FROM brain_file
|
|
506
|
+
WHERE project_id = ? AND has_syntax_errors = 1
|
|
507
|
+
LIMIT 5
|
|
508
|
+
`).all(projectId);
|
|
509
|
+
sample = rows.map((r) => `${r.relative_path} (${r.language})`);
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
name: "Syntax Error Files",
|
|
513
|
+
status: errorCount > total * 0.1 ? "error" : errorCount > total * 0.02 ? "warn" : "ok",
|
|
514
|
+
value: errorCount,
|
|
515
|
+
total,
|
|
516
|
+
percentage: total > 0 ? Math.round((1 - errorCount / total) * 1e3) / 10 : 100,
|
|
517
|
+
threshold: { warn: 2, error: 10 },
|
|
518
|
+
description: `${errorCount} files have recorded syntax errors. Tools operating on these files may produce incorrect byte bounds and garbled ASTs.`,
|
|
519
|
+
remediation: errorCount > 0 ? "These files were parsed with errors. If they are hand-written code (not generated), fix the syntax errors. Otherwise, tools should check has_syntax_errors before operating." : void 0,
|
|
520
|
+
sample: sample.length > 0 ? sample : void 0
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async function checkWalSize(rawDb, projectId) {
|
|
524
|
+
try {
|
|
525
|
+
const pageCount = rawDb.prepare("PRAGMA page_count").get()?.page_count ?? 0;
|
|
526
|
+
const pageSize = rawDb.prepare("PRAGMA page_size").get()?.page_size ?? 4096;
|
|
527
|
+
const dbSizeBytes = pageCount * pageSize;
|
|
528
|
+
const { BrainDatabase: BDB } = await import("./database-76RP44JG.js");
|
|
529
|
+
const rawPath = BDB.getPath(projectId);
|
|
530
|
+
const walPath = rawPath + "-wal";
|
|
531
|
+
let walSizeBytes = 0;
|
|
532
|
+
try {
|
|
533
|
+
walSizeBytes = fs.statSync(walPath).size;
|
|
534
|
+
} catch {
|
|
535
|
+
walSizeBytes = 0;
|
|
536
|
+
}
|
|
537
|
+
const walMB = Math.round(walSizeBytes / (1024 * 1024) * 10) / 10;
|
|
538
|
+
return {
|
|
539
|
+
name: "WAL Size",
|
|
540
|
+
status: walMB > 500 ? "error" : walMB > 100 ? "warn" : "ok",
|
|
541
|
+
value: walSizeBytes,
|
|
542
|
+
total: dbSizeBytes,
|
|
543
|
+
percentage: dbSizeBytes > 0 ? Math.round((1 - walSizeBytes / (dbSizeBytes + walSizeBytes)) * 1e3) / 10 : 100,
|
|
544
|
+
threshold: { warn: 100, error: 500 },
|
|
545
|
+
description: `WAL file is ${walMB} MB (DB is ${Math.round(dbSizeBytes / (1024 * 1024) * 10) / 10} MB). Large WAL files slow down read queries.`,
|
|
546
|
+
remediation: walMB > 200 ? "WAL will auto-checkpoint. If this persists, restart the brain service." : void 0
|
|
547
|
+
};
|
|
548
|
+
} catch (e) {
|
|
549
|
+
return {
|
|
550
|
+
name: "WAL Size",
|
|
551
|
+
status: "warn",
|
|
552
|
+
value: 0,
|
|
553
|
+
threshold: { warn: 100, error: 500 },
|
|
554
|
+
description: `WAL size was NOT MEASURED (${e instanceof Error ? e.message : String(e)}). This check neither passed nor failed; it did not run.`
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function checkEmptyFiles(rawDb, projectId) {
|
|
559
|
+
const row = rawDb.prepare(`
|
|
560
|
+
SELECT COUNT(*) as count FROM brain_file f
|
|
561
|
+
WHERE f.project_id = ?
|
|
562
|
+
AND NOT EXISTS (SELECT 1 FROM brain_symbol s WHERE s.file_id = f.id AND s.project_id = f.project_id)
|
|
563
|
+
AND f.language IS NOT NULL
|
|
564
|
+
AND f.language != 'unknown'
|
|
565
|
+
`).get(projectId);
|
|
566
|
+
const emptyCount = row?.count ?? 0;
|
|
567
|
+
let sample = [];
|
|
568
|
+
if (emptyCount > 0) {
|
|
569
|
+
const rows = rawDb.prepare(`
|
|
570
|
+
SELECT f.relative_path, f.language FROM brain_file f
|
|
571
|
+
WHERE f.project_id = ?
|
|
572
|
+
AND NOT EXISTS (SELECT 1 FROM brain_symbol s WHERE s.file_id = f.id AND s.project_id = f.project_id)
|
|
573
|
+
AND f.language IS NOT NULL AND f.language != 'unknown'
|
|
574
|
+
LIMIT 5
|
|
575
|
+
`).all(projectId);
|
|
576
|
+
sample = rows.map((r) => `${r.relative_path} (${r.language})`);
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
name: "Files with Zero Symbols",
|
|
580
|
+
status: emptyCount > 1e3 ? "error" : emptyCount > 100 ? "warn" : "ok",
|
|
581
|
+
value: emptyCount,
|
|
582
|
+
threshold: { warn: 100, error: 1e3 },
|
|
583
|
+
description: `${emptyCount} indexed files have zero extracted symbols. These may be empty files, config files, or files the parser couldn't understand.`,
|
|
584
|
+
remediation: emptyCount > 500 ? "These are likely config, documentation, or generated files \u2014 generally expected. Investigate if code files are affected." : void 0,
|
|
585
|
+
sample: sample.length > 0 ? sample : void 0
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async function checkRelationshipRatio(rawDb, projectId) {
|
|
589
|
+
const symbolRow = rawDb.prepare(`
|
|
590
|
+
SELECT COUNT(*) as count FROM brain_symbol WHERE project_id = ?
|
|
591
|
+
`).get(projectId);
|
|
592
|
+
const totalSymbols = symbolRow?.count ?? 0;
|
|
593
|
+
const relRow = rawDb.prepare(`
|
|
594
|
+
SELECT COUNT(*) as count FROM brain_relationship WHERE project_id = ?
|
|
595
|
+
`).get(projectId);
|
|
596
|
+
const totalRels = relRow?.count ?? 0;
|
|
597
|
+
const ratio = totalSymbols > 0 ? totalRels / totalSymbols : 0;
|
|
598
|
+
const status = totalSymbols === 0 ? "ok" : ratio < 0.1 ? "error" : ratio < 0.5 ? "warn" : "ok";
|
|
599
|
+
return {
|
|
600
|
+
name: "Relationship Density",
|
|
601
|
+
status,
|
|
602
|
+
value: totalRels,
|
|
603
|
+
total: totalSymbols,
|
|
604
|
+
percentage: Math.round(ratio * 100) / 100,
|
|
605
|
+
threshold: { warn: 0.5, error: 0.1 },
|
|
606
|
+
description: `${ratio.toFixed(2)} relationships per symbol. Below 0.5 may indicate relationship extraction issues (some language analyzers may not produce relationships). Below 0.1 indicates a likely indexing failure.`,
|
|
607
|
+
remediation: ratio < 0.3 ? "Check if the project's primary language has relationship extraction support. Some languages (CSS, JSON, YAML) naturally have low relationship density." : void 0
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
async function preFlightCheck(projectId, context) {
|
|
611
|
+
const rawDb = await BrainDatabase.getRawInstance(projectId, { readonly: true });
|
|
612
|
+
const warnings = [];
|
|
613
|
+
const fixes = [];
|
|
614
|
+
if (context.fileId) {
|
|
615
|
+
const syntaxRow = rawDb.prepare(`
|
|
616
|
+
SELECT has_syntax_errors FROM brain_file WHERE id = ? AND project_id = ?
|
|
617
|
+
`).get(context.fileId, projectId);
|
|
618
|
+
if (syntaxRow?.has_syntax_errors) {
|
|
619
|
+
warnings.push("Target file has recorded syntax errors. Byte bounds may be incorrect.");
|
|
620
|
+
fixes.push("Use bb_search mode=symbol to manually review the file. Avoid bb_edit on this file.");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (context.symbolName && context.fileId) {
|
|
624
|
+
const boundsRow = rawDb.prepare(`
|
|
625
|
+
SELECT byte_start, byte_end FROM brain_symbol
|
|
626
|
+
WHERE file_id = ? AND name = ? AND project_id = ?
|
|
627
|
+
LIMIT 1
|
|
628
|
+
`).get(context.fileId, context.symbolName, projectId);
|
|
629
|
+
if (boundsRow && (boundsRow.byte_start === null || boundsRow.byte_end === null)) {
|
|
630
|
+
warnings.push(`Symbol "${context.symbolName}" has NULL byte bounds. Edits may be imprecise.`);
|
|
631
|
+
fixes.push("Run bb_health with refresh=true and wait=true to reindex this file, or use line-based editing.");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const symbolCount = rawDb.prepare(`
|
|
635
|
+
SELECT COUNT(*) as count FROM brain_symbol WHERE project_id = ?
|
|
636
|
+
`).get(projectId)?.count ?? 0;
|
|
637
|
+
if (symbolCount < 100) {
|
|
638
|
+
warnings.push("Brain index is nearly empty. Most tools will return no results.");
|
|
639
|
+
fixes.push("Run bb_health with refresh=true to index the project first.");
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
ok: warnings.length === 0,
|
|
643
|
+
warnings,
|
|
644
|
+
fixes
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function formatHealthReport(report) {
|
|
648
|
+
const barLen = 20;
|
|
649
|
+
const filled = Math.round(report.overallHealth / 100 * barLen);
|
|
650
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
651
|
+
const verdictWord = {
|
|
652
|
+
healthy: "HEALTHY",
|
|
653
|
+
degraded: "DEGRADED",
|
|
654
|
+
broken: "BROKEN",
|
|
655
|
+
empty: "EMPTY \u2014 nothing is indexed"
|
|
656
|
+
};
|
|
657
|
+
let output = `INDEX HEALTH: ${report.overallHealth}% [${bar}] ${verdictWord[report.verdict]}
|
|
658
|
+
`;
|
|
659
|
+
if (report.verdict === "empty") {
|
|
660
|
+
output += ` \u2191 the score above counts checks that pass because there is NO DATA to be malformed.
|
|
661
|
+
`;
|
|
662
|
+
output += ` It is not a measure of an index's quality. Run: bb index "<project path>"
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
output += `Project: ${report.projectId}
|
|
666
|
+
`;
|
|
667
|
+
output += `Time: ${new Date(report.timestamp).toISOString()}
|
|
668
|
+
`;
|
|
669
|
+
output += `Status: ${report.summary.ok} OK / ${report.summary.warn} WARN / ${report.summary.error} ERROR (${report.summary.scored} scored + ${report.summary.informational} informational)
|
|
670
|
+
`;
|
|
671
|
+
const s = report.staleness;
|
|
672
|
+
const stalenessScope = s.complete ? `all ${s.total} indexed files` : `a ${s.sampleSize}-file sample of ${s.total} \u2014 a LOWER BOUND, not a total`;
|
|
673
|
+
output += `Staleness (${s.basis}): ${s.fresh} fresh | ${s.stale} stale | ${s.missing} missing | ${s.unclassified} unclassified \u2014 ${stalenessScope}${s.verifiedByContent > 0 ? `, ${s.verifiedByContent} verified by content` : ""}
|
|
674
|
+
`;
|
|
675
|
+
if (!report.indexStatus.recorded) {
|
|
676
|
+
output += `Index status: UNRECORDED \u2014 this project has no brain_index_status row, so completeness is not knowable from the index.
|
|
677
|
+
`;
|
|
678
|
+
} else {
|
|
679
|
+
const i = report.indexStatus;
|
|
680
|
+
output += `Index status: recorded=${i.status ?? "null"} indexed=${i.indexedFiles ?? "?"}/${i.totalFiles ?? "?"}${i.shortfall ? ` \u2014 INCOMPLETE, ${i.shortfall} file(s) never written` : ""}
|
|
681
|
+
`;
|
|
682
|
+
}
|
|
683
|
+
output += "\n";
|
|
684
|
+
const icon = (s2) => s2 === "ok" ? "\u2705" : s2 === "warn" ? "\u26A0\uFE0F" : "\u274C";
|
|
685
|
+
for (const check of report.checks) {
|
|
686
|
+
const pctStr = check.percentage !== void 0 ? ` (${check.percentage}%)` : "";
|
|
687
|
+
const mark = check.kind === "informational" ? "\u2139\uFE0F " : icon(check.status);
|
|
688
|
+
const scope = check.kind === "informational" ? " [informational \u2014 not scored]" : "";
|
|
689
|
+
output += `${mark} ${check.name}: ${check.value.toLocaleString()}${check.total ? ` / ${check.total.toLocaleString()}` : ""}${pctStr}${scope}
|
|
690
|
+
`;
|
|
691
|
+
output += ` ${check.description}
|
|
692
|
+
`;
|
|
693
|
+
if (check.remediation) {
|
|
694
|
+
output += ` \u{1F527} ${check.remediation}
|
|
695
|
+
`;
|
|
696
|
+
}
|
|
697
|
+
if (check.sample && check.sample.length > 0) {
|
|
698
|
+
output += ` Sample: ${check.sample.slice(0, 3).join(", ")}
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
output += "\n";
|
|
702
|
+
}
|
|
703
|
+
return output;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export {
|
|
707
|
+
runIndexHealthCheck,
|
|
708
|
+
checkEmbeddingCoverage,
|
|
709
|
+
preFlightCheck,
|
|
710
|
+
formatHealthReport
|
|
711
|
+
};
|