@backbencher/brain 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-KY7K552E.js +17 -0
- package/dist/brain-relationship-enrichment-QH7OOHUF.js +558 -0
- package/dist/brain.sql-EIM4WS4D.js +29 -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-2RTN2DFU.js +711 -0
- package/dist/chunk-2WXYNLBA.js +29 -0
- package/dist/chunk-3CTFIJJB.js +17 -0
- package/dist/chunk-3LFTSMIV.js +174 -0
- package/dist/chunk-47HKX4AJ.js +930 -0
- package/dist/chunk-4H22TLOT.js +543 -0
- package/dist/chunk-5SD45FLC.js +818 -0
- package/dist/chunk-66A5ZFYA.js +1758 -0
- package/dist/chunk-6AIRWAO2.js +247 -0
- package/dist/chunk-6WNGD3NN.js +196 -0
- package/dist/chunk-7AN7U5IZ.js +371 -0
- package/dist/chunk-BHB7BYYI.js +626 -0
- package/dist/chunk-BQ3OVQ3P.js +125 -0
- package/dist/chunk-CW4XXSNN.js +1479 -0
- package/dist/chunk-D2Q4Y47A.js +53 -0
- package/dist/chunk-DIYAGQJL.js +34 -0
- package/dist/chunk-DYM4FQEE.js +90 -0
- package/dist/chunk-EWMWK7WN.js +75 -0
- package/dist/chunk-FVVQTSIM.js +3427 -0
- package/dist/chunk-FWSL4VG6.js +1290 -0
- package/dist/chunk-GBI7V7KY.js +69 -0
- package/dist/chunk-GEFFO6O2.js +152 -0
- package/dist/chunk-HOWAQXJO.js +236 -0
- package/dist/chunk-HQSPJDHI.js +613 -0
- package/dist/chunk-IFZNR5UJ.js +2863 -0
- package/dist/chunk-J4PMBXQQ.js +336 -0
- package/dist/chunk-KAET47ZL.js +125 -0
- package/dist/chunk-KCDECRJY.js +440 -0
- package/dist/chunk-KLK3PBTO.js +1119 -0
- package/dist/chunk-L5TDYKYK.js +76 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-OWISBAOF.js +61 -0
- package/dist/chunk-PJTNGD3Z.js +6924 -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-RH7KC5HU.js +12050 -0
- package/dist/chunk-RM3PY6TG.js +376 -0
- package/dist/chunk-ROMM6KMC.js +433 -0
- package/dist/chunk-SK6YVHFZ.js +211 -0
- package/dist/chunk-SM7RFQRO.js +214 -0
- package/dist/chunk-UFE77L67.js +782 -0
- package/dist/chunk-UGR4QIGS.js +67 -0
- package/dist/chunk-UUNHHX4M.js +13 -0
- package/dist/chunk-V44PGPA6.js +2104 -0
- package/dist/chunk-VL52PL2T.js +71 -0
- package/dist/chunk-VURYFIHW.js +201 -0
- package/dist/chunk-XJKJYIPP.js +7436 -0
- package/dist/chunk-YI6FF2ZR.js +340 -0
- package/dist/cli/index.js +37 -0
- package/dist/config-HZAAYDP3.js +10 -0
- package/dist/content-search-642F7E5V.js +593 -0
- package/dist/database-TCTZDLE5.js +9 -0
- package/dist/doctor-GXCKSJOX.js +395 -0
- package/dist/edit-engine-KM7MPFEC.js +51 -0
- package/dist/engine-42TZFNIZ.js +11 -0
- package/dist/engine-LO6L4JLQ.js +18 -0
- package/dist/fingerprint-XDXFRWO6.js +33 -0
- package/dist/framework-intelligence-DWFCKV44.js +860 -0
- package/dist/freshness-3K6VU5OT.js +174 -0
- package/dist/graph-engine-PL5YA7BA.js +21 -0
- package/dist/graph_request-MM4REP6H.js +350 -0
- package/dist/impact_request-WDE47IZ5.js +68 -0
- package/dist/index-space-IUUN5WYV.js +15 -0
- package/dist/index-status-6BD7UBHR.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-4PW3AFXP.js +88 -0
- package/dist/pool-worker.js +1153 -0
- package/dist/project-health-ZS5TS2Z6.js +94 -0
- package/dist/project-index-5SDPHITE.js +57 -0
- package/dist/project-registry-DGZBIZS7.js +355 -0
- package/dist/real-bash-analyzer-SXSV67E2.js +2201 -0
- package/dist/real-c-analyzer-SY7FTZ6W.js +5942 -0
- package/dist/real-clean-analyzer-IGDHHLZK.js +3470 -0
- package/dist/real-clojure-analyzer-4OH7IWLB.js +2818 -0
- package/dist/real-cpp-analyzer-7LTDRJRJ.js +4101 -0
- package/dist/real-csharp-analyzer-KICU3TLH.js +3298 -0
- package/dist/real-css-analyzer-HPU277Q6.js +15 -0
- package/dist/real-dart-analyzer-7LZ5LCMW.js +3944 -0
- package/dist/real-elixir-analyzer-BE3MEVZO.js +2192 -0
- package/dist/real-erlang-analyzer-BZMAKYBK.js +1731 -0
- package/dist/real-fsharp-analyzer-5QLTOV3E.js +2390 -0
- package/dist/real-go-analyzer-POD7MZQR.js +4126 -0
- package/dist/real-graphql-analyzer-MAQRZ6QW.js +1442 -0
- package/dist/real-groovy-analyzer-MEWSUPMJ.js +2285 -0
- package/dist/real-haskell-analyzer-I5L2GR3Y.js +2533 -0
- package/dist/real-hcl-analyzer-DBOPXSWJ.js +725 -0
- package/dist/real-html-analyzer-O63XZLZD.js +1759 -0
- package/dist/real-java-analyzer-CHCW3EKA.js +4677 -0
- package/dist/real-javascript-analyzer-QAAJQYZE.js +14 -0
- package/dist/real-json-analyzer-UGBR7YQN.js +1640 -0
- package/dist/real-julia-analyzer-35Q4GHOZ.js +2085 -0
- package/dist/real-kotlin-analyzer-GRAYCWW2.js +3956 -0
- package/dist/real-lua-analyzer-5BAPDZDF.js +1957 -0
- package/dist/real-makefile-analyzer-YJKAHNAS.js +920 -0
- package/dist/real-markdown-analyzer-UKUQZO4F.js +1215 -0
- package/dist/real-matlab-analyzer-FVIKQZKG.js +480 -0
- package/dist/real-nim-analyzer-M7FAIABG.js +3603 -0
- package/dist/real-objective-c-analyzer-XMYQIGQI.js +1518 -0
- package/dist/real-perl-analyzer-ZLY6M2DZ.js +1865 -0
- package/dist/real-php-analyzer-KI2ZAM7X.js +9368 -0
- package/dist/real-powershell-analyzer-B3T5ZP6U.js +2915 -0
- package/dist/real-protobuf-analyzer-23OZ75XV.js +867 -0
- package/dist/real-python-analyzer-I4TIUB6D.js +4880 -0
- package/dist/real-r-analyzer-FJZJXXS6.js +2596 -0
- package/dist/real-ruby-analyzer-M3UFIVTQ.js +2429 -0
- package/dist/real-rust-analyzer-LVSONPAN.js +3359 -0
- package/dist/real-scala-analyzer-CWPI33Q2.js +3293 -0
- package/dist/real-scss-analyzer-TGRJQ7TH.js +1515 -0
- package/dist/real-solidity-analyzer-4P2VBVWA.js +2608 -0
- package/dist/real-sql-analyzer-J7DYZOZR.js +3840 -0
- package/dist/real-svelte-analyzer-NHEN2SX5.js +2532 -0
- package/dist/real-swift-analyzer-2NSYULWI.js +2311 -0
- package/dist/real-toml-analyzer-L3C66L7Q.js +929 -0
- package/dist/real-typescript-analyzer-FTC6ETRQ.js +14 -0
- package/dist/real-vb-analyzer-ORY7LAQS.js +3145 -0
- package/dist/real-vba-analyzer-JH2TTUFI.js +466 -0
- package/dist/real-vue-analyzer-OO4FUGBV.js +2680 -0
- package/dist/real-xml-analyzer-6CQVR477.js +2405 -0
- package/dist/real-yaml-analyzer-J6MQG3YX.js +3528 -0
- package/dist/real-zig-analyzer-EFKCPJNU.js +2430 -0
- package/dist/regex-search-OPTLKUN5.js +239 -0
- package/dist/rules-I453JTRC.js +152 -0
- package/dist/scoped-path-MJYWJZQX.js +11 -0
- package/dist/setup-show-Y5GB5KXA.js +15 -0
- package/dist/setup-wizard-G5YYR5T6.js +24 -0
- package/dist/symbol-search-KB4MU4SI.js +16 -0
- package/dist/taint-YOTN5X55.js +202 -0
- package/dist/taint-analyzer-N7YLZU3N.js +163 -0
- package/dist/tool-handlers-G4NAGJIH.js +72 -0
- package/dist/tool-registry-BPEJNSZG.js +17 -0
- package/dist/tool-validation-RF675A3C.js +32 -0
- package/dist/type-inference-4Y7WTMFE.js +13 -0
- package/dist/vector-binary-LMY4QGAN.js +19 -0
- package/dist/version-LXGNFO37.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,1758 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildEmbeddingDiagnosis,
|
|
3
|
+
describeEmbeddingConfig,
|
|
4
|
+
renderChainSummary
|
|
5
|
+
} from "./chunk-J4PMBXQQ.js";
|
|
6
|
+
import {
|
|
7
|
+
runIndexHealthCheck
|
|
8
|
+
} from "./chunk-2RTN2DFU.js";
|
|
9
|
+
import {
|
|
10
|
+
IndexLockHeldError,
|
|
11
|
+
indexProjectParallel,
|
|
12
|
+
liveIndexLockHolder,
|
|
13
|
+
reindexFilesParallel,
|
|
14
|
+
wipeProjectData
|
|
15
|
+
} from "./chunk-PJTNGD3Z.js";
|
|
16
|
+
import {
|
|
17
|
+
Config
|
|
18
|
+
} from "./chunk-UGR4QIGS.js";
|
|
19
|
+
import {
|
|
20
|
+
makeProvenance
|
|
21
|
+
} from "./chunk-UUNHHX4M.js";
|
|
22
|
+
import {
|
|
23
|
+
resolveRowsFreshnessDetailed,
|
|
24
|
+
selectSampleFiles
|
|
25
|
+
} from "./chunk-QLK6ED4Q.js";
|
|
26
|
+
import {
|
|
27
|
+
BrainDatabase
|
|
28
|
+
} from "./chunk-KLK3PBTO.js";
|
|
29
|
+
import {
|
|
30
|
+
NON_CODE_LANGUAGES,
|
|
31
|
+
analysis_exclusions_exports,
|
|
32
|
+
isNonCodeLanguage
|
|
33
|
+
} from "./chunk-SM7RFQRO.js";
|
|
34
|
+
|
|
35
|
+
// src/intelligence/patterns/orphans.ts
|
|
36
|
+
var ENGINE = "orphans@intelligence";
|
|
37
|
+
var USAGE_TYPES = ["calls", "instantiates", "extends", "implements", "uses", "references", "imports", "overrides", "renders", "decorates", "mixes_in"];
|
|
38
|
+
var MIN_EDGE_CONFIDENCE = 0.7;
|
|
39
|
+
var SELF_LOOP_EXCLUDED = "selfLoop";
|
|
40
|
+
var INFERRED_EXCLUDED = "inferred";
|
|
41
|
+
var LOW_CONFIDENCE_EXCLUDED = "lowConfidence";
|
|
42
|
+
var REPORTED_SYMBOL_TYPES = /* @__PURE__ */ new Set(["function", "method", "class", "interface", "struct", "enum"]);
|
|
43
|
+
var CLASS_LIKE_TYPES = /* @__PURE__ */ new Set(["class", "interface", "struct", "enum"]);
|
|
44
|
+
var MANIFEST_FILENAMES = /* @__PURE__ */ new Set([
|
|
45
|
+
"package.json",
|
|
46
|
+
"package-lock.json",
|
|
47
|
+
"tsconfig.json",
|
|
48
|
+
"jsconfig.json",
|
|
49
|
+
"composer.json",
|
|
50
|
+
"composer.lock",
|
|
51
|
+
"gemfile",
|
|
52
|
+
"gemfile.lock",
|
|
53
|
+
"rakefile",
|
|
54
|
+
"gemfile",
|
|
55
|
+
"cargo.toml",
|
|
56
|
+
"cargo.lock",
|
|
57
|
+
"go.mod",
|
|
58
|
+
"go.sum",
|
|
59
|
+
"requirements.txt",
|
|
60
|
+
"pipfile",
|
|
61
|
+
"pyproject.toml",
|
|
62
|
+
"setup.py",
|
|
63
|
+
"setup.cfg",
|
|
64
|
+
"pom.xml",
|
|
65
|
+
"build.gradle",
|
|
66
|
+
"build.gradle.kts",
|
|
67
|
+
"settings.gradle",
|
|
68
|
+
"settings.gradle.kts",
|
|
69
|
+
"mix.exs",
|
|
70
|
+
"pubspec.yaml",
|
|
71
|
+
"pubspec.lock",
|
|
72
|
+
"cabal.project",
|
|
73
|
+
"stack.yaml",
|
|
74
|
+
"shard.yml",
|
|
75
|
+
"dune-project",
|
|
76
|
+
"dune",
|
|
77
|
+
"makefile",
|
|
78
|
+
"justfile",
|
|
79
|
+
"dockerfile",
|
|
80
|
+
"cmakelists.txt",
|
|
81
|
+
"deno.json",
|
|
82
|
+
"deno.jsonc",
|
|
83
|
+
"bun.lockb",
|
|
84
|
+
"pnpm-workspace.yaml"
|
|
85
|
+
]);
|
|
86
|
+
var ENTRY_POINT_NAMES = /* @__PURE__ */ new Set(["main", "__main__", "start", "run", "init", "setup", "handler", "default"]);
|
|
87
|
+
var ENTRY_POINT_SUFFIXES = ["Handler", "Controller", "Middleware", "Route", "Routes", "Command", "Plugin"];
|
|
88
|
+
var ENTRY_POINT_FILE_STEMS = /* @__PURE__ */ new Set(["index", "main", "app", "mod"]);
|
|
89
|
+
function stemOf(relativePath2) {
|
|
90
|
+
const slash = relativePath2.lastIndexOf("/");
|
|
91
|
+
const base = slash >= 0 ? relativePath2.slice(slash + 1) : relativePath2;
|
|
92
|
+
const dot = base.lastIndexOf(".");
|
|
93
|
+
return dot > 0 ? base.slice(0, dot) : base;
|
|
94
|
+
}
|
|
95
|
+
function baseNameOf(relativePath2) {
|
|
96
|
+
const slash = relativePath2.lastIndexOf("/");
|
|
97
|
+
return slash >= 0 ? relativePath2.slice(slash + 1) : relativePath2;
|
|
98
|
+
}
|
|
99
|
+
function isEntryPointName(name) {
|
|
100
|
+
if (ENTRY_POINT_NAMES.has(name)) return true;
|
|
101
|
+
return ENTRY_POINT_SUFFIXES.some((s) => name.endsWith(s));
|
|
102
|
+
}
|
|
103
|
+
function isManifestFile(relativePath2) {
|
|
104
|
+
return MANIFEST_FILENAMES.has(baseNameOf(relativePath2).toLowerCase());
|
|
105
|
+
}
|
|
106
|
+
function posixJoin(dir, spec) {
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const segment of ((dir === "" ? "" : dir + "/") + spec).split("/")) {
|
|
109
|
+
if (segment === "" || segment === ".") continue;
|
|
110
|
+
if (segment === "..") {
|
|
111
|
+
if (out.length > 0) out.pop();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
out.push(segment);
|
|
115
|
+
}
|
|
116
|
+
return out.join("/");
|
|
117
|
+
}
|
|
118
|
+
function isNonCodeFile(language) {
|
|
119
|
+
return isNonCodeLanguage(language);
|
|
120
|
+
}
|
|
121
|
+
function locFor(sym) {
|
|
122
|
+
return sym.lineEnd > sym.line ? sym.lineEnd - sym.line : 0;
|
|
123
|
+
}
|
|
124
|
+
async function detectOrphans(projectId, options = {}) {
|
|
125
|
+
const raw = await BrainDatabase.getRawInstance(projectId, { readonly: true });
|
|
126
|
+
const minConfidence = options.minConfidence ?? 0;
|
|
127
|
+
const maxResults = options.maxResults ?? 100;
|
|
128
|
+
const rules = new Set(options.rules ?? ["orphaned_function", "orphaned_class", "orphaned_file"]);
|
|
129
|
+
const includeTests = options.includeTests ?? false;
|
|
130
|
+
const files = raw.prepare(
|
|
131
|
+
"SELECT id, relative_path AS relativePath, language, line_count AS lineCount FROM brain_file WHERE project_id = ?"
|
|
132
|
+
).all(projectId);
|
|
133
|
+
const fileById = new Map(files.map((f) => [f.id, f]));
|
|
134
|
+
const reportedTypes = [...REPORTED_SYMBOL_TYPES];
|
|
135
|
+
const symbols = raw.prepare(
|
|
136
|
+
"SELECT s.id, s.name, s.symbol_type AS symbolType, s.visibility, COALESCE(s.line_start,0) AS line, COALESCE(s.line_end,0) AS lineEnd, f.relative_path AS file, f.id AS fileId, f.language AS language, f.line_count AS fileLineCount FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id WHERE s.project_id = ? AND s.symbol_type IN (" + reportedTypes.map(() => "?").join(",") + ")"
|
|
137
|
+
).all(projectId, ...reportedTypes);
|
|
138
|
+
const typeParams = {};
|
|
139
|
+
const typeList = USAGE_TYPES.map((t, i) => {
|
|
140
|
+
typeParams[`t${i}`] = t;
|
|
141
|
+
return `@t${i}`;
|
|
142
|
+
}).join(",");
|
|
143
|
+
const usageFilter = `r.project_id = @projectId AND r.relationship_type IN (${typeList})`;
|
|
144
|
+
const GATE_SQL = `(CASE WHEN r.source_symbol_id IS NOT NULL AND r.target_symbol_id IS NOT NULL AND r.source_symbol_id = r.target_symbol_id THEN '${SELF_LOOP_EXCLUDED}' WHEN r.evidence_class = 'inferred' THEN '${INFERRED_EXCLUDED}' WHEN r.confidence < @minConf THEN '${LOW_CONFIDENCE_EXCLUDED}' ELSE NULL END)`;
|
|
145
|
+
const CLEAN_SQL = `${GATE_SQL} IS NULL`;
|
|
146
|
+
const BIND = { projectId, minConf: MIN_EDGE_CONFIDENCE, ...typeParams };
|
|
147
|
+
const NOT_SELF_LOOP_SQL = `(r.target_symbol_id IS NOT NULL AND (r.source_symbol_id IS NULL OR r.source_symbol_id <> r.target_symbol_id))`;
|
|
148
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
149
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
150
|
+
const fileIncomingUsage = /* @__PURE__ */ new Map();
|
|
151
|
+
const symbolsWithAnyIncoming = /* @__PURE__ */ new Set();
|
|
152
|
+
const rejectedFor = /* @__PURE__ */ new Map();
|
|
153
|
+
const edgeRejects = { [SELF_LOOP_EXCLUDED]: 0, [INFERRED_EXCLUDED]: 0, [LOW_CONFIDENCE_EXCLUDED]: 0 };
|
|
154
|
+
const edgeRejectsByType = {};
|
|
155
|
+
{
|
|
156
|
+
const rows = raw.prepare(
|
|
157
|
+
`SELECT r.relationship_type AS rt, ${GATE_SQL} AS reason, r.target_symbol_id AS id, COUNT(*) AS n FROM brain_relationship r WHERE ${usageFilter} AND ${GATE_SQL} IS NOT NULL GROUP BY r.relationship_type, reason, r.target_symbol_id`
|
|
158
|
+
).all(BIND);
|
|
159
|
+
for (const row of rows) {
|
|
160
|
+
edgeRejects[row.reason] += row.n;
|
|
161
|
+
const bucket = edgeRejectsByType[row.rt] ??= {};
|
|
162
|
+
bucket[row.reason] = (bucket[row.reason] ?? 0) + row.n;
|
|
163
|
+
if (row.id) {
|
|
164
|
+
const entry = rejectedFor.get(row.id) ?? { rows: 0, reasons: [] };
|
|
165
|
+
entry.rows += row.n;
|
|
166
|
+
if (!entry.reasons.includes(row.reason)) entry.reasons.push(row.reason);
|
|
167
|
+
rejectedFor.set(row.id, entry);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
{
|
|
172
|
+
const rows = raw.prepare(
|
|
173
|
+
`SELECT r.target_symbol_id AS id, MAX(CASE WHEN ${CLEAN_SQL} AND r.source_symbol_id IS NOT NULL THEN 1 ELSE 0 END) AS used, MAX(CASE WHEN r.source_symbol_id IS NOT NULL AND r.target_symbol_id IS NOT NULL AND r.source_symbol_id <> r.target_symbol_id THEN 1 ELSE 0 END) AS incoming FROM brain_relationship r WHERE ${usageFilter} AND r.target_symbol_id IS NOT NULL GROUP BY r.target_symbol_id`
|
|
174
|
+
).all(BIND);
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
if (row.used) usedIds.add(row.id);
|
|
177
|
+
if (row.incoming) symbolsWithAnyIncoming.add(row.id);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
{
|
|
181
|
+
const leafName = (value) => {
|
|
182
|
+
const afterColon = value.includes(":") ? value.slice(value.lastIndexOf(":") + 1) : value;
|
|
183
|
+
const dot = afterColon.lastIndexOf(".");
|
|
184
|
+
return dot > 0 ? afterColon.slice(dot + 1) : afterColon;
|
|
185
|
+
};
|
|
186
|
+
const externals = raw.prepare(
|
|
187
|
+
`SELECT DISTINCT r.target_external AS ext FROM brain_relationship r WHERE ${usageFilter} AND ${CLEAN_SQL} AND r.target_external IS NOT NULL`
|
|
188
|
+
).all(BIND);
|
|
189
|
+
for (const row of externals) usedNames.add(leafName(row.ext));
|
|
190
|
+
const names = raw.prepare(
|
|
191
|
+
`SELECT DISTINCT t.name AS name FROM brain_relationship r JOIN brain_symbol t ON t.id = r.target_symbol_id WHERE ${usageFilter} AND ${CLEAN_SQL} AND r.source_symbol_id IS NULL`
|
|
192
|
+
).all(BIND);
|
|
193
|
+
for (const row of names) usedNames.add(row.name);
|
|
194
|
+
}
|
|
195
|
+
{
|
|
196
|
+
const rows = raw.prepare(
|
|
197
|
+
`SELECT t.file_id AS f, COUNT(*) AS edges, COUNT(DISTINCT COALESCE(r.source_symbol_id, '?')) AS sources FROM brain_relationship r JOIN brain_symbol t ON t.id = r.target_symbol_id WHERE ${usageFilter} AND ${NOT_SELF_LOOP_SQL} AND t.file_id IS NOT NULL GROUP BY t.file_id`
|
|
198
|
+
).all(BIND);
|
|
199
|
+
for (const row of rows) fileIncomingUsage.set(row.f, { edges: row.edges, sources: row.sources });
|
|
200
|
+
}
|
|
201
|
+
const referencedFiles = /* @__PURE__ */ new Set();
|
|
202
|
+
{
|
|
203
|
+
const rows = raw.prepare(
|
|
204
|
+
`SELECT DISTINCT t.file_id AS f FROM brain_relationship r JOIN brain_symbol s ON s.id = r.source_symbol_id JOIN brain_symbol t ON t.id = r.target_symbol_id WHERE ${usageFilter} AND ${CLEAN_SQL} AND s.file_id IS NOT NULL AND t.file_id IS NOT NULL AND s.file_id <> t.file_id`
|
|
205
|
+
).all(BIND);
|
|
206
|
+
for (const row of rows) referencedFiles.add(row.f);
|
|
207
|
+
}
|
|
208
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
209
|
+
{
|
|
210
|
+
const nameCounts = /* @__PURE__ */ new Map();
|
|
211
|
+
for (const symbol of symbols) nameCounts.set(symbol.name, (nameCounts.get(symbol.name) ?? 0) + 1);
|
|
212
|
+
for (const [name, count] of nameCounts) if (count > 1) ambiguousNames.add(name);
|
|
213
|
+
}
|
|
214
|
+
const importReferencedFiles = /* @__PURE__ */ new Set();
|
|
215
|
+
const importUnattributedStems = /* @__PURE__ */ new Set();
|
|
216
|
+
let importRowsSeen = 0;
|
|
217
|
+
let importRowsResolved = 0;
|
|
218
|
+
let importRowsUnattributed = 0;
|
|
219
|
+
{
|
|
220
|
+
const indexedPaths = /* @__PURE__ */ new Set();
|
|
221
|
+
const fileDirById = /* @__PURE__ */ new Map();
|
|
222
|
+
const fileIdByPath = /* @__PURE__ */ new Map();
|
|
223
|
+
for (const f of files) {
|
|
224
|
+
indexedPaths.add(f.relativePath);
|
|
225
|
+
fileIdByPath.set(f.relativePath, f.id);
|
|
226
|
+
const slash = f.relativePath.lastIndexOf("/");
|
|
227
|
+
fileDirById.set(f.id, slash > 0 ? f.relativePath.slice(0, slash) : "");
|
|
228
|
+
}
|
|
229
|
+
const RESOLVABLE_SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".rb", ".go", ".rs", ".lua", ".ex", ".exs", ".clj", ".java", ".kt", ".c", ".h", ".cpp", ".cs", ".php", ".pl", ".sh", ".erl", ".jl", ".swift", ".scala", ".json", ".yaml", ".yml", ".toml", ".md"];
|
|
230
|
+
const INDEX_STEMS = ["index", "mod", "init", "__init__"];
|
|
231
|
+
const resolveAgainst = (spec, importerDir) => {
|
|
232
|
+
const bases = spec.startsWith(".") ? [importerDir] : [""];
|
|
233
|
+
for (const base of bases) {
|
|
234
|
+
const joined = posixJoin(base, spec);
|
|
235
|
+
for (const suffix of RESOLVABLE_SUFFIXES) {
|
|
236
|
+
const candidate = joined + suffix;
|
|
237
|
+
if (indexedPaths.has(candidate)) return candidate;
|
|
238
|
+
}
|
|
239
|
+
for (const stem of INDEX_STEMS) {
|
|
240
|
+
for (const suffix of RESOLVABLE_SUFFIXES) {
|
|
241
|
+
if (!suffix) continue;
|
|
242
|
+
const candidate = joined + "/" + stem + suffix;
|
|
243
|
+
if (indexedPaths.has(candidate)) return candidate;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
};
|
|
249
|
+
const importTargets = raw.prepare(
|
|
250
|
+
"SELECT r.target_external AS ext, r.source_symbol_id AS src, s.file_id AS srcFile FROM brain_relationship r LEFT JOIN brain_symbol s ON s.id = r.source_symbol_id WHERE r.project_id = ? AND r.relationship_type = 'imports'"
|
|
251
|
+
).all(projectId);
|
|
252
|
+
for (const row of importTargets) {
|
|
253
|
+
if (!row.ext) continue;
|
|
254
|
+
importRowsSeen++;
|
|
255
|
+
const spec = row.ext.includes(":") ? row.ext.slice(0, row.ext.indexOf(":")) : row.ext;
|
|
256
|
+
const importerDir = row.srcFile ? fileDirById.get(row.srcFile) : void 0;
|
|
257
|
+
if (importerDir === void 0) {
|
|
258
|
+
importRowsUnattributed++;
|
|
259
|
+
const stem = stemOf(spec);
|
|
260
|
+
if (stem) importUnattributedStems.add(stem);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const resolved = resolveAgainst(spec, importerDir);
|
|
264
|
+
if (resolved === null) continue;
|
|
265
|
+
importRowsResolved++;
|
|
266
|
+
const resolvedId = fileIdByPath.get(resolved);
|
|
267
|
+
if (resolvedId && resolvedId !== row.srcFile) importReferencedFiles.add(resolvedId);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const uniqueFileStems = /* @__PURE__ */ new Set();
|
|
271
|
+
{
|
|
272
|
+
const stemCounts = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const f of files) {
|
|
274
|
+
const stem = stemOf(f.relativePath);
|
|
275
|
+
stemCounts.set(stem, (stemCounts.get(stem) ?? 0) + 1);
|
|
276
|
+
}
|
|
277
|
+
for (const [stem, count] of stemCounts) if (count === 1) uniqueFileStems.add(stem);
|
|
278
|
+
}
|
|
279
|
+
const importedBySomething = (fileId, relativePath2) => {
|
|
280
|
+
if (importReferencedFiles.has(fileId)) return true;
|
|
281
|
+
if (importUnattributedStems.size === 0) return false;
|
|
282
|
+
const stem = stemOf(relativePath2);
|
|
283
|
+
return uniqueFileStems.has(stem) && importUnattributedStems.has(stem);
|
|
284
|
+
};
|
|
285
|
+
const suppressed = {
|
|
286
|
+
testPath: 0,
|
|
287
|
+
hardNoise: 0,
|
|
288
|
+
noiseSymbolName: 0,
|
|
289
|
+
entryPointName: 0,
|
|
290
|
+
nonCodeFile: 0,
|
|
291
|
+
manifestFile: 0,
|
|
292
|
+
ruleNotRequested: 0,
|
|
293
|
+
belowMinConfidence: 0
|
|
294
|
+
};
|
|
295
|
+
const testPathMemo = /* @__PURE__ */ new Map();
|
|
296
|
+
const noisePathMemo = /* @__PURE__ */ new Map();
|
|
297
|
+
const isTestFile = (p) => {
|
|
298
|
+
let v = testPathMemo.get(p);
|
|
299
|
+
if (v === void 0) {
|
|
300
|
+
v = analysis_exclusions_exports.isTestPath(p);
|
|
301
|
+
testPathMemo.set(p, v);
|
|
302
|
+
}
|
|
303
|
+
return v;
|
|
304
|
+
};
|
|
305
|
+
const isNoiseFile = (p) => {
|
|
306
|
+
let v = noisePathMemo.get(p);
|
|
307
|
+
if (v === void 0) {
|
|
308
|
+
v = analysis_exclusions_exports.isHardNoisePath(p);
|
|
309
|
+
noisePathMemo.set(p, v);
|
|
310
|
+
}
|
|
311
|
+
return v;
|
|
312
|
+
};
|
|
313
|
+
const candidates = [];
|
|
314
|
+
const buildSymbolFinding = (sym, confidence, fileReferenced, nameAmbiguous, rejected, fileUsage) => {
|
|
315
|
+
const classLike = CLASS_LIKE_TYPES.has(sym.symbolType);
|
|
316
|
+
const rule = classLike ? "orphaned_class" : "orphaned_function";
|
|
317
|
+
return {
|
|
318
|
+
rule,
|
|
319
|
+
engine: ENGINE,
|
|
320
|
+
severity: "info",
|
|
321
|
+
confidence,
|
|
322
|
+
location: { file: sym.file, line: sym.line, symbol: sym.name },
|
|
323
|
+
linesOfCode: locFor(sym),
|
|
324
|
+
description: `${classLike ? "Type" : "Function"} '${sym.name}' has no incoming ${USAGE_TYPES.join("/")} relationship.`,
|
|
325
|
+
remediation: "Verify it is not referenced dynamically, via reflection, framework registration, or build/test config before considering removal.",
|
|
326
|
+
evidence: [
|
|
327
|
+
{ kind: "relationship", description: `No resolved incoming relationship of type ${USAGE_TYPES.join("/")} names this symbol id. (contains/exports are indexer bookkeeping and are not usage.)` },
|
|
328
|
+
{ kind: "reference", description: nameAmbiguous ? `No name-matched reference either, but '${sym.name}' is declared ${2}+ times in the reporting scope, so a name reference could not have been attributed to this declaration. Candidates in this class measured 14.4% false positive against 0.0% for unambiguous names.` : `No name-matched cross-file reference (target_external) names this symbol, and its name is declared exactly once in the reporting scope, so a name reference would have been decisive if one existed.` },
|
|
329
|
+
{ kind: "heuristic", description: fileReferenced ? `Its file IS reached from other files by a resolved cross-file edge, which correlates with a higher false-positive rate (11.4% vs 3.7% measured across a 9-repository corpus): a reachable file is where an unrecorded reference is most likely.` : `Its file has no incoming cross-file reference at all, which correlates with a lower false-positive rate (3.7% measured).` },
|
|
330
|
+
...rejected !== void 0 ? [{
|
|
331
|
+
kind: "heuristic",
|
|
332
|
+
description: `REFUSED EVIDENCE: ${rejected.rows} row(s) about this symbol were excluded as unverified (${rejected.reasons.join(", ")}). A row like that is not proof of a consumer \u2014 on rake an \`overrides\` row that is simultaneously a self-loop, evidenceClass "inferred" and published at 0.5 was the only thing standing between \`listen\` in lib/Mojo/IOLoop/Server.pm and this report, and that symbol has no recorded callers at all. If you believe the excluded row, the declaring ancestor or caller is outside the indexed files and the analyzer did not capture it; this finding is then a coverage gap, not dead code.`
|
|
333
|
+
}] : [],
|
|
334
|
+
...fileUsage ? [{
|
|
335
|
+
kind: "heuristic",
|
|
336
|
+
description: `COVERAGE GAP INDICATOR: this symbol has no incoming usage edge at all, but its file receives ${fileUsage.edges} incoming usage edge(s) from ${fileUsage.sources} distinct source(s). The analyzer reached this file and recorded its other members; it did not record this one. That asymmetry is the signature of a missing reference rather than of an unused declaration, and it is why this finding should be checked against the source before anything is removed.`
|
|
337
|
+
}] : []
|
|
338
|
+
],
|
|
339
|
+
limitations: [
|
|
340
|
+
"Name-only matching cannot see dynamic dispatch, reflection, or string-based lookups.",
|
|
341
|
+
...rejected !== void 0 ? [`${rejected.rows} relationship row(s) about this symbol were excluded as unverified. If the analyzer failed to capture a real caller, this finding is a coverage gap rather than dead code; the excluded rows are itemised in the evidence above.`] : [],
|
|
342
|
+
...fileUsage ? [`This symbol has no recorded incoming reference while its file has ${fileUsage.edges} from ${fileUsage.sources} source(s), so the absence of evidence here is probably the analyzer's, not the code's. Verify against the source before acting on it.`] : [],
|
|
343
|
+
...nameAmbiguous ? [`The name '${sym.name}' is declared more than once; unresolved name-only references are treated as uncertain and do not suppress this candidate.`] : [],
|
|
344
|
+
"Framework entry points not matched by the built-in name/suffix rules may be false positives.",
|
|
345
|
+
"Build configs, plugin manifests (XML), and test conventions are not scanned."
|
|
346
|
+
]
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
const buildFileFinding = (row, confidence) => {
|
|
350
|
+
const hasSymbols = symbols.some((s) => s.fileId === row.id);
|
|
351
|
+
return {
|
|
352
|
+
rule: "orphaned_file",
|
|
353
|
+
engine: ENGINE,
|
|
354
|
+
severity: "info",
|
|
355
|
+
confidence,
|
|
356
|
+
location: { file: row.relativePath, line: 1 },
|
|
357
|
+
linesOfCode: row.lineCount ?? 0,
|
|
358
|
+
description: `File '${row.relativePath}' has no incoming references from other files (no cross-file symbol usage, and no import specifier agreeing with its path).`,
|
|
359
|
+
remediation: "Confirm the file is not an entry point, dynamically loaded, or referenced by build/test tooling before removal.",
|
|
360
|
+
evidence: [
|
|
361
|
+
{ kind: "relationship", description: "No resolved cross-file usage relationship lands in this file." },
|
|
362
|
+
{ kind: "import", description: `No import specifier resolves to this file. The index attributed ${importRowsResolved} of ${importRowsSeen} import row(s) to a source file and resolved ${importReferencedFiles.size} target file(s); ${importRowsUnattributed} row(s) could not be attributed and fell back to a stem match, which applies only to a stem naming exactly one file in the project.` },
|
|
363
|
+
{ kind: "heuristic", description: importRowsSeen > 0 ? `The import channel was available as evidence (${importRowsSeen} row(s), ${importRowsUnattributed} unattributable).` : `The index records NO import relationships, so the only evidence channel available was cross-file symbol usage.` }
|
|
364
|
+
],
|
|
365
|
+
limitations: [
|
|
366
|
+
"Entry points and dynamically loaded files may appear orphaned.",
|
|
367
|
+
"Build systems and non-code manifests are not scanned.",
|
|
368
|
+
"An import cannot be attributed to the file that made it in this index, so a name collision inside a package cannot be used to clear a file.",
|
|
369
|
+
...hasSymbols ? [] : ["This file contributed no reportable symbols, so the cross-file symbol channel had nothing to match on."]
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
};
|
|
373
|
+
for (const sym of symbols) {
|
|
374
|
+
if (!includeTests && isTestFile(sym.file)) {
|
|
375
|
+
suppressed.testPath++;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (isNoiseFile(sym.file)) {
|
|
379
|
+
suppressed.hardNoise++;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (analysis_exclusions_exports.isNoiseSymbolName(sym.name)) {
|
|
383
|
+
suppressed.noiseSymbolName++;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (isEntryPointName(sym.name)) {
|
|
387
|
+
suppressed.entryPointName++;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (isNonCodeFile(sym.language)) {
|
|
391
|
+
suppressed.nonCodeFile++;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const used = usedIds.has(sym.id) || usedNames.has(sym.name) && !ambiguousNames.has(sym.name);
|
|
395
|
+
if (used) continue;
|
|
396
|
+
const rule = CLASS_LIKE_TYPES.has(sym.symbolType) ? "orphaned_class" : "orphaned_function";
|
|
397
|
+
if (!rules.has(rule)) {
|
|
398
|
+
suppressed.ruleNotRequested++;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const nameAmbiguous = ambiguousNames.has(sym.name);
|
|
402
|
+
const fileReferenced = referencedFiles.has(sym.fileId);
|
|
403
|
+
let confidence = 0.5;
|
|
404
|
+
confidence += nameAmbiguous ? -0.15 : 0.15;
|
|
405
|
+
confidence += fileReferenced ? -0.05 : 0.05;
|
|
406
|
+
if (confidence < 0.3) confidence = 0.3;
|
|
407
|
+
if (confidence > 0.7) confidence = 0.7;
|
|
408
|
+
if (confidence < minConfidence) {
|
|
409
|
+
suppressed.belowMinConfidence++;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const fileUsage = symbolsWithAnyIncoming.has(sym.id) ? void 0 : fileIncomingUsage.get(sym.fileId);
|
|
413
|
+
candidates.push({ conf: confidence, file: sym.file, loc: locFor(sym), symbol: sym, rejected: rejectedFor.get(sym.id), fileUsage });
|
|
414
|
+
}
|
|
415
|
+
const fileScope = /* @__PURE__ */ new Set();
|
|
416
|
+
if (rules.has("orphaned_file")) {
|
|
417
|
+
for (const file of files) {
|
|
418
|
+
if (!includeTests && isTestFile(file.relativePath)) continue;
|
|
419
|
+
if (isNoiseFile(file.relativePath)) continue;
|
|
420
|
+
if (isManifestFile(file.relativePath)) {
|
|
421
|
+
suppressed.manifestFile++;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const stem = stemOf(file.relativePath);
|
|
425
|
+
if (ENTRY_POINT_FILE_STEMS.has(stem.toLowerCase())) continue;
|
|
426
|
+
fileScope.add(file.id);
|
|
427
|
+
const referencedBySymbol = referencedFiles.has(file.id);
|
|
428
|
+
if (referencedBySymbol || importedBySomething(file.id, file.relativePath)) continue;
|
|
429
|
+
let confidence = importRowsSeen > 0 ? 0.5 : 0.3;
|
|
430
|
+
if (isNonCodeFile(file.language)) confidence += 0.05;
|
|
431
|
+
if (confidence < minConfidence) {
|
|
432
|
+
suppressed.belowMinConfidence++;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
candidates.push({ conf: confidence, file: file.relativePath, loc: file.lineCount ?? 0, fileRow: file });
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
for (const sym of symbols) if (!isTestFile(sym.file) && !isNoiseFile(sym.file)) fileScope.add(sym.fileId);
|
|
439
|
+
}
|
|
440
|
+
candidates.sort((a, b) => b.conf - a.conf || b.loc - a.loc || a.file.localeCompare(b.file));
|
|
441
|
+
const truncated = candidates.length > maxResults;
|
|
442
|
+
const page = candidates.slice(0, maxResults).map(
|
|
443
|
+
(c) => c.symbol ? buildSymbolFinding(c.symbol, c.conf, referencedFiles.has(c.symbol.fileId), ambiguousNames.has(c.symbol.name), c.rejected, c.fileUsage) : buildFileFinding(c.fileRow, c.conf)
|
|
444
|
+
);
|
|
445
|
+
const byRule = {};
|
|
446
|
+
for (const f of page) byRule[f.rule] = (byRule[f.rule] ?? 0) + 1;
|
|
447
|
+
const candidatesByRule = {};
|
|
448
|
+
let candidatesConfidenceSum = 0;
|
|
449
|
+
for (const c of candidates) {
|
|
450
|
+
const rule = c.symbol ? CLASS_LIKE_TYPES.has(c.symbol.symbolType) ? "orphaned_class" : "orphaned_function" : "orphaned_file";
|
|
451
|
+
candidatesByRule[rule] = (candidatesByRule[rule] ?? 0) + 1;
|
|
452
|
+
candidatesConfidenceSum += c.conf;
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
findings: page,
|
|
456
|
+
provenance: makeProvenance({
|
|
457
|
+
evidenceClass: "static_finding",
|
|
458
|
+
engine: ENGINE,
|
|
459
|
+
engineVersion: "m10.03",
|
|
460
|
+
projectId,
|
|
461
|
+
source: "brain_symbol+brain_relationship+brain_file",
|
|
462
|
+
limitations: [
|
|
463
|
+
"Dynamic dispatch, reflection, string references, and external manifests are not fully observable.",
|
|
464
|
+
...ambiguousNames.size ? [`${ambiguousNames.size} duplicate symbol name(s) in the reporting scope were treated as ambiguous rather than globally resolved.`] : [],
|
|
465
|
+
...importRowsSeen === 0 ? ["The index records no import relationships, so file-level findings rest on the cross-file symbol channel alone."] : [],
|
|
466
|
+
...importRowsUnattributed > 0 ? [`${importRowsUnattributed} of ${importRowsSeen} import row(s) name no source file, so they could only be matched by stem; a stem shared by two or more files carries no path evidence and was ignored.`] : [],
|
|
467
|
+
`Non-code files (${[...NON_CODE_LANGUAGES].slice(0, 6).join(", ")}, ...) contribute no symbol candidates; they are still indexed and still checked as whole files.`
|
|
468
|
+
],
|
|
469
|
+
truncated
|
|
470
|
+
}),
|
|
471
|
+
summary: {
|
|
472
|
+
total: page.length,
|
|
473
|
+
byRule,
|
|
474
|
+
scannedSymbols: symbols.length,
|
|
475
|
+
scannedFiles: fileScope.size,
|
|
476
|
+
truncated,
|
|
477
|
+
candidatesTotal: candidates.length,
|
|
478
|
+
candidatesByRule,
|
|
479
|
+
candidatesAverageConfidence: candidates.length ? candidatesConfidenceSum / candidates.length : 0,
|
|
480
|
+
// New: why the candidate set is the size it is. Every number the scan
|
|
481
|
+
// discarded, so "the tool found nothing" is distinguishable from "the
|
|
482
|
+
// tool suppressed everything it saw".
|
|
483
|
+
suppressedByReason: suppressed,
|
|
484
|
+
// How much usage evidence the scan refused to believe, and why. The
|
|
485
|
+
// orphan page is the product of what is believed, so the volume that was
|
|
486
|
+
// NOT believed is part of the result and is reported with it. These rows
|
|
487
|
+
// are the index's own published confidence and evidence class, so the
|
|
488
|
+
// numbers are the analyzer's, not this tool's opinion of them.
|
|
489
|
+
unverifiedUsageEdges: {
|
|
490
|
+
total: edgeRejects[SELF_LOOP_EXCLUDED] + edgeRejects[INFERRED_EXCLUDED] + edgeRejects[LOW_CONFIDENCE_EXCLUDED],
|
|
491
|
+
byReason: {
|
|
492
|
+
[SELF_LOOP_EXCLUDED]: edgeRejects[SELF_LOOP_EXCLUDED],
|
|
493
|
+
[INFERRED_EXCLUDED]: edgeRejects[INFERRED_EXCLUDED],
|
|
494
|
+
[LOW_CONFIDENCE_EXCLUDED]: edgeRejects[LOW_CONFIDENCE_EXCLUDED]
|
|
495
|
+
},
|
|
496
|
+
byRelationshipType: edgeRejectsByType,
|
|
497
|
+
minimumConfidence: MIN_EDGE_CONFIDENCE
|
|
498
|
+
},
|
|
499
|
+
importResolution: {
|
|
500
|
+
rowsSeen: importRowsSeen,
|
|
501
|
+
attributedToASourceFile: importRowsSeen - importRowsUnattributed,
|
|
502
|
+
unattributed: importRowsUnattributed,
|
|
503
|
+
resolvedToAFile: importRowsResolved,
|
|
504
|
+
filesClearedByImport: importReferencedFiles.size
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/ast/tools.ts
|
|
511
|
+
var BrainTools;
|
|
512
|
+
((BrainTools2) => {
|
|
513
|
+
async function searchCode(projectId, query, options = {}) {
|
|
514
|
+
const { searchSymbols } = await import("./symbol-search-KB4MU4SI.js");
|
|
515
|
+
return searchSymbols(projectId, query, options);
|
|
516
|
+
}
|
|
517
|
+
BrainTools2.searchCode = searchCode;
|
|
518
|
+
})(BrainTools || (BrainTools = {}));
|
|
519
|
+
var PatternBrainTools;
|
|
520
|
+
((PatternBrainTools2) => {
|
|
521
|
+
async function detectPatterns(projectId, options = {}) {
|
|
522
|
+
const opts = {
|
|
523
|
+
includeOrphaned: true,
|
|
524
|
+
includeFramework: true,
|
|
525
|
+
includeAntiPatterns: true,
|
|
526
|
+
confidenceThreshold: 0.5,
|
|
527
|
+
maxResults: 200,
|
|
528
|
+
...options
|
|
529
|
+
};
|
|
530
|
+
const patterns = [];
|
|
531
|
+
let orphanCandidates = 0;
|
|
532
|
+
const orphanByType = {};
|
|
533
|
+
let orphanConfidenceSum = 0;
|
|
534
|
+
if (opts.includeOrphaned) {
|
|
535
|
+
const orphanReport = await detectOrphans(projectId, {
|
|
536
|
+
minConfidence: opts.confidenceThreshold,
|
|
537
|
+
maxResults: opts.maxResults
|
|
538
|
+
});
|
|
539
|
+
for (const finding of orphanReport.findings) {
|
|
540
|
+
patterns.push({
|
|
541
|
+
type: finding.rule,
|
|
542
|
+
confidence: finding.confidence,
|
|
543
|
+
location: { file: finding.location.file, line: finding.location.line, symbol: finding.location.symbol },
|
|
544
|
+
description: finding.description,
|
|
545
|
+
severity: "info",
|
|
546
|
+
suggestion: finding.remediation,
|
|
547
|
+
evidence: finding.evidence.map((ev) => ({ type: ev.kind, description: ev.description }))
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
orphanCandidates = orphanReport.summary.candidatesTotal;
|
|
551
|
+
for (const [k, v] of Object.entries(orphanReport.summary.candidatesByRule)) orphanByType[k] = (orphanByType[k] ?? 0) + v;
|
|
552
|
+
orphanConfidenceSum = orphanReport.summary.candidatesAverageConfidence * orphanCandidates;
|
|
553
|
+
}
|
|
554
|
+
const { detectIndexRules } = await import("./rules-I453JTRC.js");
|
|
555
|
+
const ruleFindings = await detectIndexRules(projectId, {
|
|
556
|
+
confidenceThreshold: opts.confidenceThreshold,
|
|
557
|
+
maxResults: opts.maxResults,
|
|
558
|
+
includeFramework: opts.includeFramework,
|
|
559
|
+
includeAntiPatterns: opts.includeAntiPatterns
|
|
560
|
+
});
|
|
561
|
+
for (const finding of ruleFindings) patterns.push(finding);
|
|
562
|
+
const severityRank = { error: 0, warning: 1, info: 2 };
|
|
563
|
+
const orphanPage = patterns.filter((p) => String(p.type).startsWith("orphaned_"));
|
|
564
|
+
const rulePage = patterns.filter((p) => !String(p.type).startsWith("orphaned_"));
|
|
565
|
+
patterns.length = 0;
|
|
566
|
+
const ranked = [...orphanPage, ...rulePage].sort((a, b) => {
|
|
567
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
568
|
+
const sa = severityRank[a.severity] ?? 3;
|
|
569
|
+
const sb = severityRank[b.severity] ?? 3;
|
|
570
|
+
if (sa !== sb) return sa - sb;
|
|
571
|
+
return String(a.location?.file ?? "").localeCompare(String(b.location?.file ?? ""));
|
|
572
|
+
});
|
|
573
|
+
patterns.push(...ranked);
|
|
574
|
+
const byType = { ...orphanByType };
|
|
575
|
+
const bySeverity = {};
|
|
576
|
+
for (const k of Object.keys(orphanByType)) bySeverity["info"] = (bySeverity["info"] ?? 0) + orphanByType[k];
|
|
577
|
+
let confidenceSum = orphanConfidenceSum;
|
|
578
|
+
let ruleFindingCount = 0;
|
|
579
|
+
for (const pattern of patterns) {
|
|
580
|
+
if (String(pattern.type).startsWith("orphaned_")) continue;
|
|
581
|
+
ruleFindingCount++;
|
|
582
|
+
byType[pattern.type] = (byType[pattern.type] || 0) + 1;
|
|
583
|
+
bySeverity[pattern.severity] = (bySeverity[pattern.severity] || 0) + 1;
|
|
584
|
+
confidenceSum += pattern.confidence;
|
|
585
|
+
}
|
|
586
|
+
const REQUESTED = [
|
|
587
|
+
{ on: opts.includeOrphaned !== false, match: (t) => t.startsWith("orphaned_") },
|
|
588
|
+
{ on: opts.includeFramework !== false, match: (t) => t === "framework-detected" },
|
|
589
|
+
{
|
|
590
|
+
on: opts.includeAntiPatterns !== false,
|
|
591
|
+
match: (t) => ["high-complexity-function", "long-parameter-list", "god-class"].includes(t)
|
|
592
|
+
}
|
|
593
|
+
];
|
|
594
|
+
const page = patterns.slice(0, opts.maxResults);
|
|
595
|
+
if (patterns.length > opts.maxResults && page.length > 0) {
|
|
596
|
+
for (const req of REQUESTED) {
|
|
597
|
+
if (!req.on) continue;
|
|
598
|
+
const onPage = page.some((p) => req.match(String(p.type)));
|
|
599
|
+
if (onPage) continue;
|
|
600
|
+
const best = patterns.find((p) => req.match(String(p.type)));
|
|
601
|
+
if (!best) continue;
|
|
602
|
+
let victim = -1;
|
|
603
|
+
for (let i = page.length - 1; i >= 0; i--) {
|
|
604
|
+
const others = REQUESTED.filter((r) => r.on && r.match(String(page[i].type)));
|
|
605
|
+
if (others.length > 1) continue;
|
|
606
|
+
if (page.filter((p, j) => j < i && others.some((r) => r.match(String(p.type)))).length > 0) continue;
|
|
607
|
+
victim = i;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
if (victim === -1) continue;
|
|
611
|
+
page[victim] = best;
|
|
612
|
+
}
|
|
613
|
+
page.sort((a, b) => {
|
|
614
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
615
|
+
const sa = severityRank[a.severity] ?? 3;
|
|
616
|
+
const sb = severityRank[b.severity] ?? 3;
|
|
617
|
+
if (sa !== sb) return sa - sb;
|
|
618
|
+
return String(a.location?.file ?? "").localeCompare(String(b.location?.file ?? ""));
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const displayedPatterns = page;
|
|
622
|
+
const censusTotal = orphanCandidates + ruleFindingCount;
|
|
623
|
+
return {
|
|
624
|
+
patterns: displayedPatterns,
|
|
625
|
+
statistics: {
|
|
626
|
+
// The census, not the page. This field was the returned page's length
|
|
627
|
+
// while byType / bySeverity / averageConfidence beside it all described
|
|
628
|
+
// the census, so one object reported two different universes and the
|
|
629
|
+
// handler printed the page length under the words "patterns (N)".
|
|
630
|
+
// With a default limit of 20 the tool fetched 100, printed 20, and
|
|
631
|
+
// reported 100. `scan.displayed` is the page length.
|
|
632
|
+
totalFound: censusTotal,
|
|
633
|
+
byType,
|
|
634
|
+
bySeverity,
|
|
635
|
+
averageConfidence: censusTotal > 0 ? confidenceSum / censusTotal : 0
|
|
636
|
+
},
|
|
637
|
+
recommendations: [],
|
|
638
|
+
scan: {
|
|
639
|
+
candidatesEvaluated: censusTotal,
|
|
640
|
+
findingsBeforeDisplayLimit: censusTotal,
|
|
641
|
+
displayed: displayedPatterns.length,
|
|
642
|
+
truncated: censusTotal > displayedPatterns.length
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
PatternBrainTools2.detectPatterns = detectPatterns;
|
|
647
|
+
})(PatternBrainTools || (PatternBrainTools = {}));
|
|
648
|
+
|
|
649
|
+
// src/intelligence/health/health-provider.ts
|
|
650
|
+
import { relative as relativePath, resolve, isAbsolute, sep } from "path";
|
|
651
|
+
import { statSync } from "fs";
|
|
652
|
+
|
|
653
|
+
// src/intelligence/analysis/run-history.ts
|
|
654
|
+
import { createHash, randomUUID } from "crypto";
|
|
655
|
+
var ENGINE_VERSION = "m10.04";
|
|
656
|
+
var STATES = ["open", "resolved", "suppressed", "reopened", "mitigated", "stale"];
|
|
657
|
+
var STATE_ORDER = { open: 0, reopened: 1, mitigated: 2, stale: 3, suppressed: 4, resolved: 5 };
|
|
658
|
+
function now() {
|
|
659
|
+
return Date.now();
|
|
660
|
+
}
|
|
661
|
+
function json(value) {
|
|
662
|
+
return JSON.stringify(value ?? null);
|
|
663
|
+
}
|
|
664
|
+
function parseJson(value) {
|
|
665
|
+
if (Array.isArray(value)) return value;
|
|
666
|
+
if (typeof value !== "string") return [];
|
|
667
|
+
try {
|
|
668
|
+
const parsed = JSON.parse(value);
|
|
669
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
670
|
+
} catch {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function parseJsonStringArray(value) {
|
|
675
|
+
const list = parseJson(value);
|
|
676
|
+
return list.filter((item) => typeof item === "string");
|
|
677
|
+
}
|
|
678
|
+
function hash(value) {
|
|
679
|
+
return createHash("sha256").update(json(value)).digest("hex");
|
|
680
|
+
}
|
|
681
|
+
function stateOf(value) {
|
|
682
|
+
return typeof value === "string" && STATES.includes(value) ? value : "open";
|
|
683
|
+
}
|
|
684
|
+
function ensureColumn(raw, table, name, ddl) {
|
|
685
|
+
const columns = raw.prepare(`PRAGMA table_info(${table})`).all();
|
|
686
|
+
if (columns.some((col) => col.name === name)) return;
|
|
687
|
+
raw.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${ddl}`);
|
|
688
|
+
}
|
|
689
|
+
function ensureHistorySchema(raw) {
|
|
690
|
+
raw.exec(`
|
|
691
|
+
CREATE TABLE IF NOT EXISTS brain_analysis_run (
|
|
692
|
+
id TEXT PRIMARY KEY NOT NULL, project_id TEXT NOT NULL, kind TEXT NOT NULL,
|
|
693
|
+
status TEXT NOT NULL, engine_version TEXT NOT NULL, input_hash TEXT NOT NULL,
|
|
694
|
+
started_at INTEGER NOT NULL, completed_at INTEGER, finding_count INTEGER NOT NULL DEFAULT 0,
|
|
695
|
+
error TEXT, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL
|
|
696
|
+
);
|
|
697
|
+
CREATE TABLE IF NOT EXISTS brain_analysis_finding (
|
|
698
|
+
id TEXT PRIMARY KEY NOT NULL, project_id TEXT NOT NULL, kind TEXT NOT NULL,
|
|
699
|
+
fingerprint TEXT NOT NULL, rule TEXT NOT NULL, title TEXT NOT NULL, severity TEXT NOT NULL,
|
|
700
|
+
state TEXT NOT NULL, confidence REAL, evidence TEXT,
|
|
701
|
+
location TEXT, limitations TEXT, evidence_class TEXT, engine TEXT, engine_version TEXT,
|
|
702
|
+
first_seen_at INTEGER NOT NULL,
|
|
703
|
+
last_seen_at INTEGER NOT NULL, resolved_at INTEGER, suppressed_at INTEGER, mitigated_at INTEGER,
|
|
704
|
+
occurrence_count INTEGER NOT NULL DEFAULT 0, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL
|
|
705
|
+
);
|
|
706
|
+
CREATE TABLE IF NOT EXISTS brain_analysis_occurrence (
|
|
707
|
+
id TEXT PRIMARY KEY NOT NULL, finding_id TEXT NOT NULL, run_id TEXT NOT NULL, state TEXT NOT NULL,
|
|
708
|
+
location TEXT, evidence TEXT, limitations TEXT, evidence_class TEXT, engine TEXT, engine_version TEXT,
|
|
709
|
+
observed_at INTEGER NOT NULL, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL
|
|
710
|
+
);
|
|
711
|
+
CREATE UNIQUE INDEX IF NOT EXISTS brain_analysis_finding_identity_idx ON brain_analysis_finding(project_id, kind, fingerprint);
|
|
712
|
+
CREATE INDEX IF NOT EXISTS brain_analysis_run_project_idx ON brain_analysis_run(project_id);
|
|
713
|
+
CREATE INDEX IF NOT EXISTS brain_analysis_run_kind_idx ON brain_analysis_run(kind);
|
|
714
|
+
CREATE INDEX IF NOT EXISTS brain_analysis_run_started_idx ON brain_analysis_run(started_at);
|
|
715
|
+
CREATE INDEX IF NOT EXISTS brain_analysis_finding_project_idx ON brain_analysis_finding(project_id);
|
|
716
|
+
CREATE INDEX IF NOT EXISTS brain_analysis_finding_state_idx ON brain_analysis_finding(project_id, state);
|
|
717
|
+
CREATE INDEX IF NOT EXISTS brain_analysis_occurrence_finding_idx ON brain_analysis_occurrence(finding_id);
|
|
718
|
+
CREATE INDEX IF NOT EXISTS brain_analysis_occurrence_run_idx ON brain_analysis_occurrence(run_id);
|
|
719
|
+
`);
|
|
720
|
+
ensureColumn(raw, "brain_analysis_finding", "location", "TEXT");
|
|
721
|
+
ensureColumn(raw, "brain_analysis_finding", "limitations", "TEXT");
|
|
722
|
+
ensureColumn(raw, "brain_analysis_finding", "evidence_class", "TEXT");
|
|
723
|
+
ensureColumn(raw, "brain_analysis_finding", "engine", "TEXT");
|
|
724
|
+
ensureColumn(raw, "brain_analysis_finding", "engine_version", "TEXT");
|
|
725
|
+
ensureColumn(raw, "brain_analysis_occurrence", "limitations", "TEXT");
|
|
726
|
+
ensureColumn(raw, "brain_analysis_occurrence", "evidence_class", "TEXT");
|
|
727
|
+
ensureColumn(raw, "brain_analysis_occurrence", "engine", "TEXT");
|
|
728
|
+
ensureColumn(raw, "brain_analysis_occurrence", "engine_version", "TEXT");
|
|
729
|
+
}
|
|
730
|
+
function fingerprintFinding(kind, input) {
|
|
731
|
+
return input.fingerprint ?? hash({ kind, rule: input.rule, title: input.title, location: input.location ?? null, evidence: input.evidence ?? [], limitations: input.limitations ?? [], evidenceClass: input.evidenceClass ?? null, engine: input.engine ?? null, engineVersion: input.engineVersion ?? null });
|
|
732
|
+
}
|
|
733
|
+
function readRun(row) {
|
|
734
|
+
return { id: row.id, projectId: row.project_id, kind: row.kind, status: row.status, engineVersion: row.engine_version, inputHash: row.input_hash, startedAt: row.started_at, completedAt: row.completed_at ?? null, findingCount: row.finding_count, error: row.error ?? null };
|
|
735
|
+
}
|
|
736
|
+
function readFinding(row) {
|
|
737
|
+
return {
|
|
738
|
+
id: row.id,
|
|
739
|
+
projectId: row.project_id,
|
|
740
|
+
kind: row.kind,
|
|
741
|
+
fingerprint: row.fingerprint,
|
|
742
|
+
rule: row.rule,
|
|
743
|
+
title: row.title,
|
|
744
|
+
severity: row.severity,
|
|
745
|
+
state: stateOf(row.state),
|
|
746
|
+
confidence: row.confidence ?? null,
|
|
747
|
+
evidence: parseJson(row.evidence),
|
|
748
|
+
location: row.location ?? null,
|
|
749
|
+
limitations: parseJsonStringArray(row.limitations),
|
|
750
|
+
evidenceClass: row.evidence_class ?? null,
|
|
751
|
+
engine: row.engine ?? null,
|
|
752
|
+
engineVersion: row.engine_version ?? null,
|
|
753
|
+
firstSeenAt: row.first_seen_at,
|
|
754
|
+
lastSeenAt: row.last_seen_at,
|
|
755
|
+
resolvedAt: row.resolved_at ?? null,
|
|
756
|
+
suppressedAt: row.suppressed_at ?? null,
|
|
757
|
+
mitigatedAt: row.mitigated_at ?? null,
|
|
758
|
+
occurrenceCount: row.occurrence_count
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function orderedFindings(rows) {
|
|
762
|
+
return rows.map(readFinding).sort((a, b) => STATE_ORDER[a.state] - STATE_ORDER[b.state] || b.lastSeenAt - a.lastSeenAt || a.fingerprint.localeCompare(b.fingerprint));
|
|
763
|
+
}
|
|
764
|
+
async function startAnalysisRun(input) {
|
|
765
|
+
const raw = await BrainDatabase.getRawInstance(input.projectId);
|
|
766
|
+
ensureHistorySchema(raw);
|
|
767
|
+
const timestamp = now();
|
|
768
|
+
const id = randomUUID();
|
|
769
|
+
raw.prepare(`INSERT INTO brain_analysis_run (id,project_id,kind,status,engine_version,input_hash,started_at,finding_count,time_created,time_updated) VALUES (?,?,?,?,?,?,?,?,?,?)`).run(id, input.projectId, input.kind, "running", input.engineVersion ?? ENGINE_VERSION, hash(input.input ?? {}), timestamp, 0, timestamp, timestamp);
|
|
770
|
+
return readRun(raw.prepare("SELECT * FROM brain_analysis_run WHERE id=?").get(id));
|
|
771
|
+
}
|
|
772
|
+
async function completeAnalysisRun(projectId, runId, findings, options = {}) {
|
|
773
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
774
|
+
ensureHistorySchema(raw);
|
|
775
|
+
const timestamp = now();
|
|
776
|
+
const report = { run: {}, findings: [], newFindings: 0, repeatedFindings: 0, resolvedFindings: 0, reopenedFindings: 0, staleFindings: 0, orderedFindings: [] };
|
|
777
|
+
const finish = raw.transaction(() => {
|
|
778
|
+
const run = raw.prepare("SELECT * FROM brain_analysis_run WHERE id=? AND project_id=?").get(runId, projectId);
|
|
779
|
+
if (!run) throw new Error(`analysis run not found: ${runId}`);
|
|
780
|
+
if (run.status !== "running") throw new Error(`analysis run is not running: ${runId} (status=${run.status})`);
|
|
781
|
+
const runEngineVersion = run.engine_version;
|
|
782
|
+
const seen = /* @__PURE__ */ new Set();
|
|
783
|
+
for (const input of findings) {
|
|
784
|
+
const fingerprint = fingerprintFinding(run.kind, input);
|
|
785
|
+
if (seen.has(fingerprint)) continue;
|
|
786
|
+
seen.add(fingerprint);
|
|
787
|
+
const evidenceClass = input.evidenceClass ?? null;
|
|
788
|
+
const engine = input.engine ?? null;
|
|
789
|
+
const engineVersion = input.engineVersion ?? runEngineVersion;
|
|
790
|
+
const limitations = input.limitations ?? [];
|
|
791
|
+
const existing = raw.prepare("SELECT * FROM brain_analysis_finding WHERE project_id=? AND kind=? AND fingerprint=?").get(projectId, run.kind, fingerprint);
|
|
792
|
+
const priorState = existing ? stateOf(existing.state) : null;
|
|
793
|
+
const nextState = input.state ? stateOf(input.state) : priorState === "resolved" || priorState === "stale" ? "reopened" : priorState === "suppressed" || priorState === "mitigated" ? priorState : "open";
|
|
794
|
+
let findingId;
|
|
795
|
+
if (!existing) {
|
|
796
|
+
findingId = randomUUID();
|
|
797
|
+
raw.prepare(`INSERT INTO brain_analysis_finding (id,project_id,kind,fingerprint,rule,title,severity,state,confidence,evidence,location,limitations,evidence_class,engine,engine_version,first_seen_at,last_seen_at,occurrence_count,time_created,time_updated) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(findingId, projectId, run.kind, fingerprint, input.rule, input.title, input.severity, nextState, input.confidence ?? null, json(input.evidence), input.location ?? null, json(limitations), evidenceClass, engine, engineVersion, timestamp, timestamp, 1, timestamp, timestamp);
|
|
798
|
+
report.newFindings++;
|
|
799
|
+
} else {
|
|
800
|
+
findingId = existing.id;
|
|
801
|
+
raw.prepare(`UPDATE brain_analysis_finding SET state=?, rule=?, title=?, severity=?, confidence=?, evidence=?, location=?, limitations=?, evidence_class=?, engine=?, engine_version=?, last_seen_at=?, occurrence_count=occurrence_count+1, resolved_at=?, suppressed_at=?, mitigated_at=?, time_updated=? WHERE id=?`).run(nextState, input.rule ?? existing.rule, input.title ?? existing.title, input.severity ?? existing.severity, input.confidence ?? existing.confidence ?? null, json(input.evidence), input.location ?? existing.location ?? null, json(input.limitations ?? (existing.limitations ? parseJsonStringArray(existing.limitations) : [])), evidenceClass ?? existing.evidence_class ?? null, engine ?? existing.engine ?? null, engineVersion ?? existing.engine_version ?? null, timestamp, nextState === "resolved" ? timestamp : existing.resolved_at, nextState === "suppressed" ? timestamp : existing.suppressed_at, nextState === "mitigated" ? timestamp : existing.mitigated_at, timestamp, findingId);
|
|
802
|
+
if (nextState === "reopened") report.reopenedFindings++;
|
|
803
|
+
else report.repeatedFindings++;
|
|
804
|
+
}
|
|
805
|
+
raw.prepare("INSERT INTO brain_analysis_occurrence (id,finding_id,run_id,state,location,evidence,limitations,evidence_class,engine,engine_version,observed_at,time_created,time_updated) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)").run(randomUUID(), findingId, runId, nextState, input.location ?? null, json(input.evidence), json(limitations), evidenceClass, engine, engineVersion, timestamp, timestamp, timestamp);
|
|
806
|
+
}
|
|
807
|
+
const prior = options.incremental ? (options.touchedFingerprints ?? []).length > 0 ? raw.prepare("SELECT * FROM brain_analysis_finding WHERE project_id=? AND kind=? AND state IN ('open','reopened') AND fingerprint IN (" + options.touchedFingerprints.map(() => "?").join(",") + ")").all(projectId, run.kind, ...options.touchedFingerprints) : [] : raw.prepare("SELECT * FROM brain_analysis_finding WHERE project_id=? AND kind=? AND state IN ('open','reopened') AND last_seen_at < ?").all(projectId, run.kind, timestamp);
|
|
808
|
+
for (const row of prior) {
|
|
809
|
+
if (!seen.has(row.fingerprint)) {
|
|
810
|
+
raw.prepare("UPDATE brain_analysis_finding SET state='resolved', resolved_at=?, time_updated=? WHERE id=?").run(timestamp, timestamp, row.id);
|
|
811
|
+
report.resolvedFindings++;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const retentionMs = (options.retentionDays ?? 90) * 864e5;
|
|
815
|
+
raw.prepare("DELETE FROM brain_analysis_occurrence WHERE observed_at < ?").run(timestamp - retentionMs);
|
|
816
|
+
raw.prepare("UPDATE brain_analysis_run SET status='completed', completed_at=?, finding_count=?, time_updated=? WHERE id=?").run(timestamp, seen.size, timestamp, runId);
|
|
817
|
+
report.run = readRun(raw.prepare("SELECT * FROM brain_analysis_run WHERE id=?").get(runId));
|
|
818
|
+
const allRows = raw.prepare("SELECT * FROM brain_analysis_finding WHERE project_id=? AND kind=?").all(projectId, run.kind);
|
|
819
|
+
report.findings = orderedFindings(allRows);
|
|
820
|
+
report.orderedFindings = report.findings;
|
|
821
|
+
});
|
|
822
|
+
finish();
|
|
823
|
+
return report;
|
|
824
|
+
}
|
|
825
|
+
async function failAnalysisRun(projectId, runId, error) {
|
|
826
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
827
|
+
ensureHistorySchema(raw);
|
|
828
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
829
|
+
const timestamp = now();
|
|
830
|
+
raw.prepare("UPDATE brain_analysis_run SET status='failed', completed_at=?, error=?, time_updated=? WHERE id=? AND project_id=?").run(timestamp, message, timestamp, runId, projectId);
|
|
831
|
+
return readRun(raw.prepare("SELECT * FROM brain_analysis_run WHERE id=?").get(runId));
|
|
832
|
+
}
|
|
833
|
+
async function getAnalysisHistory(projectId, kind, limit = 20) {
|
|
834
|
+
const raw = await BrainDatabase.getRawInstance(projectId, { readonly: true });
|
|
835
|
+
try {
|
|
836
|
+
return readHistory(raw, projectId, kind, limit);
|
|
837
|
+
} catch {
|
|
838
|
+
return { runs: [], findings: [], summary: Object.fromEntries(STATES.map((state) => [state, 0])) };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function readHistory(raw, projectId, kind, limit) {
|
|
842
|
+
const runs = kind ? raw.prepare("SELECT * FROM brain_analysis_run WHERE project_id=? AND kind=? ORDER BY started_at DESC LIMIT ?").all(projectId, kind, limit) : raw.prepare("SELECT * FROM brain_analysis_run WHERE project_id=? ORDER BY started_at DESC LIMIT ?").all(projectId, limit);
|
|
843
|
+
const findingRows = kind ? raw.prepare("SELECT * FROM brain_analysis_finding WHERE project_id=? AND kind=?").all(projectId, kind) : raw.prepare("SELECT * FROM brain_analysis_finding WHERE project_id=?").all(projectId);
|
|
844
|
+
const findings = orderedFindings(findingRows);
|
|
845
|
+
const summary = Object.fromEntries(STATES.map((state) => [state, findings.filter((row) => row.state === state).length]));
|
|
846
|
+
return { runs: runs.map(readRun), findings, summary };
|
|
847
|
+
}
|
|
848
|
+
async function executeAnalysis(args) {
|
|
849
|
+
const run = await startAnalysisRun({ projectId: args.projectId, kind: args.kind, input: args.input, engineVersion: args.engineVersion, retentionDays: args.retentionDays });
|
|
850
|
+
try {
|
|
851
|
+
const inputs = await args.persist(run);
|
|
852
|
+
const report = await completeAnalysisRun(args.projectId, run.id, inputs, { retentionDays: args.retentionDays, incremental: args.incremental, touchedFingerprints: args.touchedFingerprints });
|
|
853
|
+
return { run: report.run, report, findings: report.findings };
|
|
854
|
+
} catch (error) {
|
|
855
|
+
await failAnalysisRun(args.projectId, run.id, error);
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/intelligence/health/health-provider.ts
|
|
861
|
+
function parseArray(value) {
|
|
862
|
+
if (Array.isArray(value)) return value;
|
|
863
|
+
if (typeof value !== "string") return [];
|
|
864
|
+
try {
|
|
865
|
+
const parsed = JSON.parse(value);
|
|
866
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
867
|
+
} catch {
|
|
868
|
+
return [];
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function kindsWithNoEmbeddings(raw, projectId) {
|
|
872
|
+
try {
|
|
873
|
+
const rows = raw.prepare(
|
|
874
|
+
"SELECT s.symbol_type AS kind, COUNT(*) AS c FROM brain_symbol s WHERE s.project_id = ? AND NOT EXISTS (SELECT 1 FROM brain_semantic_embedding e WHERE e.entity_id = s.id) GROUP BY s.symbol_type ORDER BY c DESC"
|
|
875
|
+
).all(projectId);
|
|
876
|
+
return rows.map((row) => ({ kind: String(row.kind), count: Number(row.c) }));
|
|
877
|
+
} catch {
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
var refreshQueues = /* @__PURE__ */ new Map();
|
|
882
|
+
var detachedRefreshes = /* @__PURE__ */ new Map();
|
|
883
|
+
function queueRefresh(projectId, projectPath, force, logger) {
|
|
884
|
+
const previous = refreshQueues.get(projectId) ?? Promise.resolve();
|
|
885
|
+
const current = previous.catch(() => void 0).then(async () => {
|
|
886
|
+
const database = await BrainDatabase.getRawInstance(projectId);
|
|
887
|
+
if (force) wipeProjectData(database);
|
|
888
|
+
return indexProjectParallel(projectId, projectPath, {
|
|
889
|
+
isIncremental: !force,
|
|
890
|
+
...logger ? { logger } : {}
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
refreshQueues.set(projectId, current);
|
|
894
|
+
void current.finally(() => {
|
|
895
|
+
if (refreshQueues.get(projectId) === current) refreshQueues.delete(projectId);
|
|
896
|
+
}).catch(() => void 0);
|
|
897
|
+
return current;
|
|
898
|
+
}
|
|
899
|
+
function emptyIndexReport(projectId, projectPath, options, warnings, timing, startedCall, refresh, error) {
|
|
900
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
901
|
+
warnings.push({
|
|
902
|
+
code: "EMPTY_INDEX",
|
|
903
|
+
message: `no usable index for ${projectPath}: ${reason}. This is an unindexed project, not a verified healthy one. Run bb_health with refresh=true, wait=true (or: bb index "${projectPath}") to build it.`,
|
|
904
|
+
severity: "error"
|
|
905
|
+
});
|
|
906
|
+
let pathChecks = null;
|
|
907
|
+
let scope = null;
|
|
908
|
+
if (options.paths && options.paths.length > 0) {
|
|
909
|
+
const resolved = [];
|
|
910
|
+
const refused = [];
|
|
911
|
+
for (const requested of options.paths) {
|
|
912
|
+
const trimmed = String(requested).trim();
|
|
913
|
+
const abs = resolve(projectPath, trimmed);
|
|
914
|
+
const rel = relativePath(projectPath, abs);
|
|
915
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
916
|
+
refused.push({ requested: trimmed, reason: `path escapes project root: ${trimmed}` });
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
let onDisk = false;
|
|
920
|
+
try {
|
|
921
|
+
onDisk = statSync(abs).isFile();
|
|
922
|
+
} catch {
|
|
923
|
+
onDisk = false;
|
|
924
|
+
}
|
|
925
|
+
resolved.push({
|
|
926
|
+
requested: trimmed,
|
|
927
|
+
path: rel.split(sep).join("/"),
|
|
928
|
+
status: onDisk ? "not-indexed" : "absent",
|
|
929
|
+
reason: onDisk ? "on disk but the project has no index \u2014 run refresh=true (or bb index) to index it" : "not in the index and not on disk \u2014 this path does not exist",
|
|
930
|
+
language: null,
|
|
931
|
+
symbols: 0,
|
|
932
|
+
verifiedBy: null
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
pathChecks = resolved;
|
|
936
|
+
scope = {
|
|
937
|
+
kind: "paths",
|
|
938
|
+
requested: options.paths.map((p) => String(p).trim()),
|
|
939
|
+
refused,
|
|
940
|
+
note: "no index exists, so no freshness can be verified. Every other line in this report is a zero over an empty index, not a project measurement."
|
|
941
|
+
};
|
|
942
|
+
const notIndexed = resolved.filter((r) => r.status === "not-indexed").map((r) => r.path);
|
|
943
|
+
if (notIndexed.length > 0) {
|
|
944
|
+
warnings.push({ code: "PATHS_NOT_INDEXED", message: `${notIndexed.length} of the ${resolved.length} path(s) you named exist on disk but the project has no index: ${notIndexed.slice(0, 5).join(", ")}. Index the project (refresh=true) before asking for per-file freshness.`, severity: "warning" });
|
|
945
|
+
}
|
|
946
|
+
if (refused.length > 0) {
|
|
947
|
+
warnings.push({ code: "PATHS_REFUSED", message: `${refused.length} path(s) were REFUSED and are NOT checked: ${refused.map((r) => `${r.requested} (${r.reason})`).join("; ")}. A refused path is not a clean one.`, severity: "error" });
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
timing.totalMs = Date.now() - startedCall;
|
|
951
|
+
const zeroReasons = { "metadata-fast-path": 0, "hash-match": 0, "hash-differs": 0, "file-gone": 0, unreadable: 0, "no-row": 0 };
|
|
952
|
+
return {
|
|
953
|
+
projectId,
|
|
954
|
+
projectPath,
|
|
955
|
+
timestamp: Date.now(),
|
|
956
|
+
counts: { files: 0, symbols: 0, relationships: 0, imports: 0, syntaxErrors: 0 },
|
|
957
|
+
indexStatus: { lastFullIndex: null, lastIncrementalIndex: null, indexedFiles: null, totalFilesDiscovered: null, indexShortfall: 0, isIncomplete: false, indexingVersion: null, status: null, errors: [], recordPresent: false },
|
|
958
|
+
performanceStats: { schemaVersion: null, runs: [], latestRun: null },
|
|
959
|
+
freshness: {
|
|
960
|
+
mode: "complete",
|
|
961
|
+
requestedMode: options.freshness ?? "sampled",
|
|
962
|
+
checked: 0,
|
|
963
|
+
total: 0,
|
|
964
|
+
fresh: 0,
|
|
965
|
+
stale: 0,
|
|
966
|
+
missing: 0,
|
|
967
|
+
staleFiles: [],
|
|
968
|
+
missingFiles: [],
|
|
969
|
+
unresolved: 0,
|
|
970
|
+
isComplete: true,
|
|
971
|
+
reasons: { byKind: zeroReasons, freshVerifiedByContent: 0, freshVerifiedByMetadata: 0, isSample: false }
|
|
972
|
+
},
|
|
973
|
+
embeddings: { total: 0, embeddedEntities: 0, symbolCoveragePercent: 0, models: [], embeddedByEntityType: [], unembeddedKinds: [], byLanguage: [] },
|
|
974
|
+
schema: { version: null, migration: null, tables: 0 },
|
|
975
|
+
runtime: { runtime: typeof Bun !== "undefined" ? "bun" : "node", version: process.version, database: "sqlite" },
|
|
976
|
+
timing,
|
|
977
|
+
integrity: null,
|
|
978
|
+
scope,
|
|
979
|
+
pathChecks,
|
|
980
|
+
unindexed: null,
|
|
981
|
+
analysisRuns: null,
|
|
982
|
+
refresh,
|
|
983
|
+
effectiveConfig: null,
|
|
984
|
+
embeddingChain: null,
|
|
985
|
+
languages: [],
|
|
986
|
+
verbose: options.verbose === true,
|
|
987
|
+
warnings
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
async function getUnifiedHealth(projectId, projectPath, options = {}) {
|
|
991
|
+
const timing = { totalMs: 0, aggregatesMs: 0, freshnessMs: 0, integrityMs: 0, refreshMs: 0, pathsMs: 0, unindexedMs: 0 };
|
|
992
|
+
const startedCall = Date.now();
|
|
993
|
+
const now2 = () => Date.now();
|
|
994
|
+
const warnings = [];
|
|
995
|
+
const refresh = { requested: options.refresh === true || options.force === true, force: options.force === true, wait: options.wait !== false, completed: false, durationMs: null, processedFiles: null, derived: null };
|
|
996
|
+
const priorDetached = detachedRefreshes.get(projectId);
|
|
997
|
+
if (priorDetached) {
|
|
998
|
+
if (priorDetached.state === "running") {
|
|
999
|
+
warnings.push({ code: "REFRESH_IN_PROGRESS", message: `a detached refresh started ${Math.round((Date.now() - priorDetached.startedAt) / 1e3)}s ago is still running${priorDetached.force ? " AFTER A FORCE WIPE \u2014 the index is mid-rebuild and every count below is transient" : ""}`, severity: "warning" });
|
|
1000
|
+
} else if (priorDetached.state === "failed") {
|
|
1001
|
+
warnings.push({ code: "REFRESH_FAILED_ASYNC", message: `the detached refresh started ${Math.round((Date.now() - priorDetached.startedAt) / 1e3)}s ago FAILED${priorDetached.force ? " after a force wipe, so the index may be empty" : ""}: ${priorDetached.error ?? "unknown error"}. Re-run with refresh=true, wait=true.`, severity: "error" });
|
|
1002
|
+
detachedRefreshes.delete(projectId);
|
|
1003
|
+
} else {
|
|
1004
|
+
warnings.push({ code: "REFRESH_COMPLETED", message: `the detached refresh started ${Math.round((Date.now() - priorDetached.startedAt) / 1e3)}s ago completed in ${priorDetached.durationMs ?? "?"}ms (${priorDetached.processedFiles ?? "?"} file(s) processed, derived=${priorDetached.derived ?? "unknown"}). This notice is one-shot.`, severity: "warning" });
|
|
1005
|
+
detachedRefreshes.delete(projectId);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const writeRequested = options.refresh === true || options.force === true;
|
|
1009
|
+
let raw;
|
|
1010
|
+
try {
|
|
1011
|
+
raw = await BrainDatabase.getRawInstance(projectId, { readonly: !writeRequested });
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
if (writeRequested) {
|
|
1014
|
+
warnings.push({ code: "REFRESH_FAILED", message: `the refresh could not open the index: ${error instanceof Error ? error.message : String(error)}. Nothing was reindexed.`, severity: "error" });
|
|
1015
|
+
}
|
|
1016
|
+
return emptyIndexReport(projectId, projectPath, options, warnings, timing, startedCall, refresh, error);
|
|
1017
|
+
}
|
|
1018
|
+
if (options.refresh === true || options.force === true) {
|
|
1019
|
+
const started = Date.now();
|
|
1020
|
+
const refreshPromise = queueRefresh(projectId, projectPath, options.force === true, options.logger);
|
|
1021
|
+
if (options.wait === false) {
|
|
1022
|
+
refresh.wait = false;
|
|
1023
|
+
const record = { startedAt: started, force: options.force === true, state: "running" };
|
|
1024
|
+
detachedRefreshes.set(projectId, record);
|
|
1025
|
+
warnings.push({
|
|
1026
|
+
code: "REFRESH_STARTED",
|
|
1027
|
+
message: `refresh started in the background; returning the current snapshot${record.force ? ". force=true WIPES the index before rebuilding, so these counts are transient and the next call will report whether it finished" : ""}`,
|
|
1028
|
+
severity: "warning"
|
|
1029
|
+
});
|
|
1030
|
+
void refreshPromise.then(
|
|
1031
|
+
(stats) => {
|
|
1032
|
+
if (detachedRefreshes.get(projectId) !== record) return;
|
|
1033
|
+
record.durationMs = Date.now() - started;
|
|
1034
|
+
record.processedFiles = stats?.processedFiles ?? stats?.totalFiles ?? 0;
|
|
1035
|
+
record.derived = typeof stats?.summary?.derivedPass === "string" ? stats.summary.derivedPass : "unknown";
|
|
1036
|
+
if (record.force && record.processedFiles === 0) {
|
|
1037
|
+
record.state = "failed";
|
|
1038
|
+
record.error = "the forced rebuild completed but indexed 0 files \u2014 force=true wiped the index and the rebuild put nothing back, so the index is now empty";
|
|
1039
|
+
} else {
|
|
1040
|
+
record.state = "completed";
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
(error) => {
|
|
1044
|
+
if (detachedRefreshes.get(projectId) !== record) return;
|
|
1045
|
+
record.state = "failed";
|
|
1046
|
+
record.durationMs = Date.now() - started;
|
|
1047
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
1048
|
+
}
|
|
1049
|
+
);
|
|
1050
|
+
} else {
|
|
1051
|
+
try {
|
|
1052
|
+
const stats = await refreshPromise;
|
|
1053
|
+
refresh.completed = true;
|
|
1054
|
+
refresh.durationMs = Date.now() - started;
|
|
1055
|
+
timing.refreshMs = refresh.durationMs;
|
|
1056
|
+
refresh.processedFiles = stats.processedFiles ?? stats.totalFiles ?? 0;
|
|
1057
|
+
refresh.derived = typeof stats?.summary?.derivedPass === "string" ? stats.summary.derivedPass : "unknown";
|
|
1058
|
+
try {
|
|
1059
|
+
raw = await BrainDatabase.getRawInstance(projectId, { readonly: true });
|
|
1060
|
+
} catch {
|
|
1061
|
+
raw = await BrainDatabase.getRawInstance(projectId);
|
|
1062
|
+
}
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
warnings.push({ code: "REFRESH_FAILED", message: error instanceof Error ? error.message : String(error), severity: "error" });
|
|
1065
|
+
refresh.durationMs = Date.now() - started;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const count = (table) => raw.prepare(`SELECT COUNT(*) c FROM ${table} WHERE project_id=?`).get(projectId)?.c ?? 0;
|
|
1070
|
+
const files = count("brain_file"), symbols = count("brain_symbol"), relationships = count("brain_relationship"), imports = count("brain_import");
|
|
1071
|
+
const syntaxErrorRows = raw.prepare("SELECT relative_path FROM brain_file WHERE project_id=? AND COALESCE(has_syntax_errors,0)=1 ORDER BY relative_path LIMIT 8").all(projectId);
|
|
1072
|
+
const syntaxErrors = raw.prepare("SELECT COUNT(*) c FROM brain_file WHERE project_id=? AND COALESCE(has_syntax_errors,0)=1").get(projectId)?.c ?? 0;
|
|
1073
|
+
const status = raw.prepare("SELECT * FROM brain_index_status WHERE project_id=?").get(projectId);
|
|
1074
|
+
const freshnessMode = options.freshness ?? "sampled";
|
|
1075
|
+
const sampleSize = options.sampleSize ?? 200;
|
|
1076
|
+
const freshnessStarted = now2();
|
|
1077
|
+
const allFileRows = raw.prepare("SELECT id, relative_path FROM brain_file WHERE project_id=?").all(projectId);
|
|
1078
|
+
const fileRows = freshnessMode === "complete" ? selectSampleFiles(allFileRows, Number.MAX_SAFE_INTEGER) : selectSampleFiles(allFileRows, sampleSize);
|
|
1079
|
+
const resolved = resolveRowsFreshnessDetailed(raw, projectId, fileRows.map((row) => ({ fileId: row.id })), { freshnessMode });
|
|
1080
|
+
timing.freshnessMs = now2() - freshnessStarted;
|
|
1081
|
+
const statuses = resolved.statuses;
|
|
1082
|
+
let fresh = 0, stale = 0, missing = 0;
|
|
1083
|
+
const staleFiles = [], missingFiles = [];
|
|
1084
|
+
const reasonCounts = {
|
|
1085
|
+
"metadata-fast-path": 0,
|
|
1086
|
+
"hash-match": 0,
|
|
1087
|
+
"hash-differs": 0,
|
|
1088
|
+
"file-gone": 0,
|
|
1089
|
+
unreadable: 0,
|
|
1090
|
+
"no-row": 0
|
|
1091
|
+
};
|
|
1092
|
+
for (const row of fileRows) {
|
|
1093
|
+
const value = statuses.get(row.id);
|
|
1094
|
+
const why = resolved.reasons.get(row.id);
|
|
1095
|
+
if (why) reasonCounts[why]++;
|
|
1096
|
+
if (value === "fresh") fresh++;
|
|
1097
|
+
else if (value === "missing") {
|
|
1098
|
+
missing++;
|
|
1099
|
+
missingFiles.push(row.relative_path);
|
|
1100
|
+
} else if (value === "stale") {
|
|
1101
|
+
stale++;
|
|
1102
|
+
staleFiles.push(row.relative_path);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const unresolved = fileRows.length - (fresh + stale + missing);
|
|
1106
|
+
const reasonTotal = Object.values(reasonCounts).reduce((a, b) => a + b, 0) + unresolved;
|
|
1107
|
+
const aggregatesStarted = now2();
|
|
1108
|
+
const fileLangRows = raw.prepare(
|
|
1109
|
+
"SELECT language, COUNT(DISTINCT id) AS files FROM brain_file WHERE project_id = ? GROUP BY language"
|
|
1110
|
+
).all(projectId);
|
|
1111
|
+
let languageRows = [];
|
|
1112
|
+
if (options.verbose === true) {
|
|
1113
|
+
const symLangRows = raw.prepare(
|
|
1114
|
+
"SELECT f.language AS language, COUNT(*) AS symbols FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id AND f.project_id = ? WHERE s.project_id = ? GROUP BY f.language"
|
|
1115
|
+
).all(projectId, projectId);
|
|
1116
|
+
const langMap = /* @__PURE__ */ new Map();
|
|
1117
|
+
for (const fr of fileLangRows) langMap.set(String(fr.language), { language: fr.language, files: Number(fr.files), symbols: 0 });
|
|
1118
|
+
for (const sr of symLangRows) {
|
|
1119
|
+
const bucket = langMap.get(String(sr.language));
|
|
1120
|
+
if (bucket) bucket.symbols = Number(sr.symbols);
|
|
1121
|
+
}
|
|
1122
|
+
languageRows = [...langMap.values()].sort((a, b) => {
|
|
1123
|
+
if (b.files !== a.files) return b.files - a.files;
|
|
1124
|
+
if (a.language === b.language) return 0;
|
|
1125
|
+
if (a.language == null) return -1;
|
|
1126
|
+
if (b.language == null) return 1;
|
|
1127
|
+
return String(a.language) < String(b.language) ? -1 : 1;
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
const modelRows = raw.prepare("SELECT embedding_model AS model, COUNT(*) c, GROUP_CONCAT(DISTINCT vector_dimension) dims FROM brain_semantic_embedding WHERE project_id=? GROUP BY embedding_model ORDER BY c DESC").all(projectId);
|
|
1131
|
+
const entityTypeRows = options.verbose === true ? raw.prepare("SELECT entity_type, COUNT(*) c FROM brain_semantic_embedding WHERE project_id=? GROUP BY entity_type ORDER BY c DESC").all(projectId) : [];
|
|
1132
|
+
const embeddingTotal = modelRows.reduce((sum, row) => sum + Number(row.c ?? 0), 0);
|
|
1133
|
+
const embeddedEntities = raw.prepare("SELECT COUNT(DISTINCT entity_id) c FROM brain_semantic_embedding WHERE project_id=?").get(projectId)?.c ?? 0;
|
|
1134
|
+
const schema = raw.prepare("SELECT version, migration_name FROM brain_schema_version ORDER BY version DESC LIMIT 1").get();
|
|
1135
|
+
const tableCount = raw.prepare("SELECT COUNT(*) c FROM sqlite_master WHERE type='table' AND name LIKE 'brain_%'").get()?.c ?? 0;
|
|
1136
|
+
let effectiveConfig = null;
|
|
1137
|
+
let embeddingChain = null;
|
|
1138
|
+
if (options.verbose === true) {
|
|
1139
|
+
try {
|
|
1140
|
+
embeddingChain = buildEmbeddingDiagnosis({ state: describeEmbeddingConfig() });
|
|
1141
|
+
} catch {
|
|
1142
|
+
embeddingChain = null;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
const { resolveEmbeddingConfig } = await import("./config-HZAAYDP3.js");
|
|
1146
|
+
const fileCfg = await Config.get();
|
|
1147
|
+
const effFull = resolveEmbeddingConfig(fileCfg);
|
|
1148
|
+
effectiveConfig = { baseURL: effFull.baseURL, model: effFull.model, configured: effFull.configured, sources: effFull.sources };
|
|
1149
|
+
} catch {
|
|
1150
|
+
effectiveConfig = null;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const embeddedByLanguage = (() => {
|
|
1154
|
+
if (options.verbose !== true) return [];
|
|
1155
|
+
try {
|
|
1156
|
+
const rows = raw.prepare(
|
|
1157
|
+
"SELECT f.language AS language, COUNT(DISTINCT s.id) AS embedded FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id AND f.project_id = ? WHERE s.project_id = ? AND EXISTS (SELECT 1 FROM brain_semantic_embedding e WHERE e.entity_id = s.id) GROUP BY f.language"
|
|
1158
|
+
).all(projectId, projectId);
|
|
1159
|
+
const have = new Map(rows.map((r) => [String(r.language), Number(r.embedded)]));
|
|
1160
|
+
return languageRows.map((l) => ({
|
|
1161
|
+
language: l.language,
|
|
1162
|
+
symbols: l.symbols,
|
|
1163
|
+
embedded: have.get(String(l.language)) ?? 0,
|
|
1164
|
+
searchablePercent: l.symbols > 0 ? Math.round((have.get(String(l.language)) ?? 0) / l.symbols * 1e3) / 10 : 0
|
|
1165
|
+
}));
|
|
1166
|
+
} catch {
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
})();
|
|
1170
|
+
timing.aggregatesMs = now2() - aggregatesStarted;
|
|
1171
|
+
let unindexed = null;
|
|
1172
|
+
const unindexedStarted = now2();
|
|
1173
|
+
if (options.checkUnindexed === true) {
|
|
1174
|
+
const walkStarted = now2();
|
|
1175
|
+
try {
|
|
1176
|
+
const rootStat = statSync(projectPath);
|
|
1177
|
+
if (!rootStat.isDirectory()) throw new Error(`project path is not a directory: ${projectPath}`);
|
|
1178
|
+
const { discoverIndexableFiles } = await import("./parallel-indexer-4PW3AFXP.js");
|
|
1179
|
+
const onDisk = await discoverIndexableFiles(projectPath);
|
|
1180
|
+
const indexed = new Set(
|
|
1181
|
+
raw.prepare("SELECT path FROM brain_file WHERE project_id=?").all(projectId).map((r) => String(r.path))
|
|
1182
|
+
);
|
|
1183
|
+
const missingFromIndex = onDisk.filter((p) => !indexed.has(p)).map((p) => relativePath(projectPath, p)).sort();
|
|
1184
|
+
unindexed = {
|
|
1185
|
+
eligibleOnDisk: onDisk.length,
|
|
1186
|
+
indexed: indexed.size,
|
|
1187
|
+
unindexedCount: missingFromIndex.length,
|
|
1188
|
+
unindexedFiles: missingFromIndex,
|
|
1189
|
+
walkMs: now2() - walkStarted,
|
|
1190
|
+
population: "files this project would index under the indexer's own rules (.gitignore, .bbignore, skip directories, the compiled-mode path filter, extension and shell-shebang gates, and the >5MB size skip). A file excluded by those rules is correctly absent from this count and is NOT a gap in the index."
|
|
1191
|
+
};
|
|
1192
|
+
if (missingFromIndex.length > 0) {
|
|
1193
|
+
warnings.push({
|
|
1194
|
+
code: "UNINDEXED_FILES",
|
|
1195
|
+
message: `${missingFromIndex.length} file(s) on disk are not in the index: ${missingFromIndex.slice(0, 5).join(", ")}${missingFromIndex.length > 5 ? `, +${missingFromIndex.length - 5} more` : ""}. The indexer would index these, so it has not run since they appeared. Run refresh=true, or bb_index on the specific paths.`,
|
|
1196
|
+
severity: "warning"
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
warnings.push({ code: "UNINDEXED_SCAN_FAILED", message: `could not compare the index against disk: ${error instanceof Error ? error.message : String(error)}. Whether the index is COMPLETE is UNKNOWN \u2014 this is not a verified result.`, severity: "warning" });
|
|
1201
|
+
unindexed = null;
|
|
1202
|
+
}
|
|
1203
|
+
timing.unindexedMs = now2() - unindexedStarted;
|
|
1204
|
+
}
|
|
1205
|
+
let pathChecks = null;
|
|
1206
|
+
let scope = null;
|
|
1207
|
+
if (options.paths && options.paths.length > 0) {
|
|
1208
|
+
const scopeStarted = now2();
|
|
1209
|
+
const { resolveSafeEditPath } = await import("./edit-engine-KM7MPFEC.js");
|
|
1210
|
+
const resolved2 = [];
|
|
1211
|
+
const refused = [];
|
|
1212
|
+
const wanted = [];
|
|
1213
|
+
for (const requested of options.paths) {
|
|
1214
|
+
const trimmed = String(requested).trim();
|
|
1215
|
+
const key = resolve(projectPath, trimmed);
|
|
1216
|
+
const asRelative = relativePath(projectPath, key);
|
|
1217
|
+
if (asRelative === "" || asRelative === ".." || asRelative.startsWith(`..${sep}`) || isAbsolute(asRelative)) {
|
|
1218
|
+
refused.push({ requested: trimmed, reason: `path escapes project root: ${trimmed}` });
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
let abs;
|
|
1222
|
+
try {
|
|
1223
|
+
abs = resolveSafeEditPath(projectPath, asRelative);
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
refused.push({ requested: trimmed, reason: e instanceof Error ? e.message : String(e) });
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
const row = raw.prepare("SELECT id, language FROM brain_file WHERE project_id=? AND path=?").get(projectId, key);
|
|
1229
|
+
wanted.push({ requested: trimmed, abs: key, realAbs: abs, rel: asRelative, fileId: row ? String(row.id) : null, row });
|
|
1230
|
+
}
|
|
1231
|
+
const ids = wanted.map((w) => w.fileId).filter((x) => !!x);
|
|
1232
|
+
const scoped = ids.length ? resolveRowsFreshnessDetailed(raw, projectId, ids.map((id) => ({ fileId: id })), { freshnessMode: "complete" }) : null;
|
|
1233
|
+
for (const w of wanted) {
|
|
1234
|
+
if (!w.fileId || !w.row) {
|
|
1235
|
+
let onDisk = false;
|
|
1236
|
+
try {
|
|
1237
|
+
onDisk = statSync(w.abs).isFile();
|
|
1238
|
+
} catch {
|
|
1239
|
+
onDisk = false;
|
|
1240
|
+
}
|
|
1241
|
+
resolved2.push({
|
|
1242
|
+
requested: w.requested,
|
|
1243
|
+
path: w.rel,
|
|
1244
|
+
status: onDisk ? "not-indexed" : "absent",
|
|
1245
|
+
reason: onDisk ? "on disk but not in the index \u2014 the indexer would index this, so it has not run since the file appeared" : "not in the index and not on disk \u2014 this path does not exist",
|
|
1246
|
+
language: null,
|
|
1247
|
+
symbols: 0,
|
|
1248
|
+
verifiedBy: null
|
|
1249
|
+
});
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const status2 = scoped.statuses.get(w.fileId);
|
|
1253
|
+
const reason = scoped.reasons.get(w.fileId) ?? null;
|
|
1254
|
+
const symbolCount = raw.prepare("SELECT COUNT(*) c FROM brain_symbol WHERE project_id=? AND file_id=?").get(projectId, w.fileId)?.c ?? 0;
|
|
1255
|
+
resolved2.push({
|
|
1256
|
+
requested: w.requested,
|
|
1257
|
+
path: w.rel,
|
|
1258
|
+
// The resolver's `missing` covers BOTH "deleted" and "unreadable"; for a
|
|
1259
|
+
// caller asking about ONE file those are different, so the reason is
|
|
1260
|
+
// carried rather than flattened into one word.
|
|
1261
|
+
status: status2 === "missing" ? reason === "file-gone" ? "gone" : "gone" : status2 ?? "absent",
|
|
1262
|
+
reason,
|
|
1263
|
+
language: w.row.language ?? null,
|
|
1264
|
+
symbols: Number(symbolCount),
|
|
1265
|
+
verifiedBy: reason === "metadata-fast-path" ? "mtime+size only (file not read)" : reason === "hash-match" ? "content hash" : null
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
pathChecks = resolved2;
|
|
1269
|
+
scope = {
|
|
1270
|
+
kind: "paths",
|
|
1271
|
+
requested: options.paths.map((p) => String(p).trim()),
|
|
1272
|
+
refused,
|
|
1273
|
+
note: "This block answers the paths you named. Every other line in this report \u2014 counts, embeddings, freshness, index \u2014 is PROJECT-WIDE and is NOT scoped to these paths."
|
|
1274
|
+
};
|
|
1275
|
+
const unindexedPaths = resolved2.filter((r) => r.status === "not-indexed").map((r) => r.path);
|
|
1276
|
+
if (unindexedPaths.length > 0) {
|
|
1277
|
+
warnings.push({ code: "PATHS_NOT_INDEXED", message: `${unindexedPaths.length} of the ${resolved2.length} path(s) you named exist on disk but are NOT in the index: ${unindexedPaths.slice(0, 5).join(", ")}. A freshness answer about a file the index has never seen would be meaningless; index them with bb_index on those paths.`, severity: "warning" });
|
|
1278
|
+
}
|
|
1279
|
+
const stalePaths = resolved2.filter((r) => r.status === "stale").map((r) => r.path);
|
|
1280
|
+
if (stalePaths.length > 0) {
|
|
1281
|
+
warnings.push({ code: "PATHS_STALE", message: `${stalePaths.length} of the ${resolved2.length} path(s) you named changed on disk since indexing: ${stalePaths.slice(0, 5).join(", ")}.`, severity: "warning" });
|
|
1282
|
+
}
|
|
1283
|
+
if (refused.length > 0) {
|
|
1284
|
+
warnings.push({ code: "PATHS_REFUSED", message: `${refused.length} path(s) were REFUSED and are NOT checked: ${refused.map((r) => `${r.requested} (${r.reason})`).join("; ")}. A refused path is not a clean one.`, severity: "error" });
|
|
1285
|
+
}
|
|
1286
|
+
timing.pathsMs = now2() - scopeStarted;
|
|
1287
|
+
}
|
|
1288
|
+
let integrity = null;
|
|
1289
|
+
if (options.integrity === true) {
|
|
1290
|
+
const integrityStarted = now2();
|
|
1291
|
+
const coversEveryFile = fileRows.length >= files;
|
|
1292
|
+
try {
|
|
1293
|
+
integrity = await runIndexHealthCheck(projectId, {
|
|
1294
|
+
diskScan: "complete",
|
|
1295
|
+
preResolved: coversEveryFile ? { total: files, reasons: reasonCounts } : null
|
|
1296
|
+
});
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
warnings.push({ code: "INTEGRITY_FAILED", message: error instanceof Error ? error.message : String(error), severity: "error" });
|
|
1299
|
+
}
|
|
1300
|
+
timing.integrityMs = now2() - integrityStarted;
|
|
1301
|
+
}
|
|
1302
|
+
let analysisRuns = null;
|
|
1303
|
+
if (options.includeAnalysisRuns === true) {
|
|
1304
|
+
try {
|
|
1305
|
+
const history = await getAnalysisHistory(projectId, void 0, 100);
|
|
1306
|
+
const runTotal = raw.prepare("SELECT COUNT(*) c FROM brain_analysis_run WHERE project_id=?").get(projectId)?.c ?? null;
|
|
1307
|
+
analysisRuns = { runs: runTotal ?? history.runs.length, findings: history.findings.length, byState: history.summary, runTotalKnown: runTotal !== null };
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
warnings.push({ code: "ANALYSIS_HISTORY_FAILED", message: error instanceof Error ? error.message : String(error), severity: "warning" });
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (syntaxErrors > 0) {
|
|
1313
|
+
const named = syntaxErrorRows.map((r) => r.relative_path);
|
|
1314
|
+
const shown = named.length === syntaxErrors ? named.join(", ") : `${named.join(", ")}${named.length > 0 ? "\u2026" : ""}`;
|
|
1315
|
+
warnings.push({
|
|
1316
|
+
code: "SYNTAX_ERRORS",
|
|
1317
|
+
message: `${syntaxErrors} indexed files have syntax errors${shown ? `: ${shown}` : ""}`,
|
|
1318
|
+
severity: "warning"
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (unresolved > 0) warnings.push({ code: "UNRESOLVED_FILES", message: `${unresolved} indexed files could not be classified and are counted as neither fresh nor stale`, severity: "warning" });
|
|
1322
|
+
if (stale + missing > 0) {
|
|
1323
|
+
const edited = reasonCounts["hash-differs"];
|
|
1324
|
+
const deleted = reasonCounts["file-gone"];
|
|
1325
|
+
const denied = reasonCounts["unreadable"] + reasonCounts["no-row"];
|
|
1326
|
+
const mix = [edited ? `${edited} changed on disk` : "", deleted ? `${deleted} deleted from disk` : "", denied ? `${denied} unreadable or unindexed` : ""].filter(Boolean).join(", ");
|
|
1327
|
+
const scope2 = fileRows.length >= files ? `of ${files} indexed files` : `in a ${fileRows.length}-file sample of ${files} \u2014 a LOWER BOUND on the project, not a total (freshness="complete" counts them all)`;
|
|
1328
|
+
warnings.push({
|
|
1329
|
+
code: "STALE_FILES",
|
|
1330
|
+
message: `${stale} stale and ${missing} missing ${scope2}${mix ? ` (${mix})` : ""}`,
|
|
1331
|
+
severity: "warning"
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
const statusRecordPresent = status != null;
|
|
1335
|
+
if (files === 0) {
|
|
1336
|
+
warnings.push({
|
|
1337
|
+
code: "EMPTY_INDEX",
|
|
1338
|
+
message: "the index holds 0 files and 0 symbols \u2014 this is either an unindexed project or an index whose rebuild did not finish (a forced refresh wipes before it rebuilds). Run refresh=true, wait=true to (re)build it.",
|
|
1339
|
+
severity: "error"
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
const recordedTotal = typeof status?.total_files === "number" ? status.total_files : null;
|
|
1343
|
+
const recordedIndexed = typeof status?.indexed_files === "number" ? status.indexed_files : null;
|
|
1344
|
+
const statusCountersDisagree = statusRecordPresent && recordedTotal !== null && files > 0 && recordedTotal < files;
|
|
1345
|
+
const indexShortfall = recordedTotal !== null && recordedIndexed !== null && recordedIndexed < recordedTotal ? { missing: recordedTotal - recordedIndexed, total: recordedTotal, indexed: recordedIndexed } : null;
|
|
1346
|
+
if (indexShortfall) {
|
|
1347
|
+
const contradicted = status?.status === "complete";
|
|
1348
|
+
warnings.push({
|
|
1349
|
+
code: "INCOMPLETE_INDEX",
|
|
1350
|
+
message: `the last index run recorded ${indexShortfall.indexed} of ${indexShortfall.total} files as indexed \u2014 ${indexShortfall.missing} file(s) were found by the walk but never written, so the index is INCOMPLETE${contradicted ? ' even though the status field says "complete"' : ""}. counts below describe a partial index and any answer derived from it covers only what was indexed. Run refresh=true, force=true to rebuild from scratch.`,
|
|
1351
|
+
severity: "error"
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
if (statusCountersDisagree) {
|
|
1355
|
+
warnings.push({
|
|
1356
|
+
code: "INDEX_STATUS_COUNTERS",
|
|
1357
|
+
message: `the index status record's last-run counters (${recordedIndexed ?? "?"} indexed of ${recordedTotal ?? "?"} discovered) describe a smaller population than the ${files} files the index actually holds \u2014 these counters are the LAST RUN's numbers, not index-wide totals, and must not be read as either a complete count or a shortfall of this index`,
|
|
1358
|
+
severity: "warning"
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
if (!statusRecordPresent) {
|
|
1362
|
+
warnings.push({
|
|
1363
|
+
code: "NO_INDEX_STATUS",
|
|
1364
|
+
message: "no brain_index_status row for this project \u2014 indexedFiles/status/errors/lastFullIndex are UNRECORDED, not zero. counts.files is measured from brain_file. Reindex (refresh=true) to restore the record.",
|
|
1365
|
+
severity: "warning"
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
const indexingErrors = parseArray(status?.indexing_errors);
|
|
1369
|
+
if (indexingErrors.length > 0) warnings.push({ code: "INDEXING_ERRORS", message: `${indexingErrors.length} indexing errors recorded`, severity: "warning" });
|
|
1370
|
+
const perfRaw = status?.performance_stats;
|
|
1371
|
+
const performanceStats = typeof perfRaw === "string" && perfRaw ? (() => {
|
|
1372
|
+
try {
|
|
1373
|
+
const p = JSON.parse(perfRaw);
|
|
1374
|
+
return Array.isArray(p?.runs) ? p : null;
|
|
1375
|
+
} catch {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
})() : null;
|
|
1379
|
+
const perfRuns = performanceStats?.runs ?? [];
|
|
1380
|
+
const latestPerfRun = perfRuns.length > 0 ? perfRuns[perfRuns.length - 1] : null;
|
|
1381
|
+
timing.totalMs = now2() - startedCall;
|
|
1382
|
+
return {
|
|
1383
|
+
projectId,
|
|
1384
|
+
projectPath,
|
|
1385
|
+
timestamp: Date.now(),
|
|
1386
|
+
counts: { files, symbols, relationships, imports, syntaxErrors },
|
|
1387
|
+
indexStatus: {
|
|
1388
|
+
lastFullIndex: status?.last_full_index ?? null,
|
|
1389
|
+
lastIncrementalIndex: status?.last_incremental_index ?? null,
|
|
1390
|
+
indexedFiles: status?.indexed_files ?? null,
|
|
1391
|
+
// the walk-vs-write shortfall, exposed as data. A consumer can
|
|
1392
|
+
// branch on this without parsing a warning message.
|
|
1393
|
+
totalFilesDiscovered: recordedTotal,
|
|
1394
|
+
indexShortfall: indexShortfall ? indexShortfall.missing : 0,
|
|
1395
|
+
isIncomplete: indexShortfall !== null,
|
|
1396
|
+
indexingVersion: status?.indexing_version ?? null,
|
|
1397
|
+
status: status?.status ?? null,
|
|
1398
|
+
errors: indexingErrors,
|
|
1399
|
+
recordPresent: statusRecordPresent
|
|
1400
|
+
},
|
|
1401
|
+
performanceStats: { schemaVersion: performanceStats?.schemaVersion ?? null, runs: perfRuns, latestRun: latestPerfRun },
|
|
1402
|
+
// `mode` reports what was ACTUALLY checked, not what was asked
|
|
1403
|
+
// for. A caller passing sampleSize >= the file count gets every file, and
|
|
1404
|
+
// saying "sampled" about a complete check is the mirror image of the bug
|
|
1405
|
+
// this replaced. `isComplete` lets a JSON consumer refuse to read an
|
|
1406
|
+
// estimate as a census.
|
|
1407
|
+
freshness: {
|
|
1408
|
+
mode: fileRows.length >= files ? "complete" : "sampled",
|
|
1409
|
+
requestedMode: freshnessMode,
|
|
1410
|
+
checked: fileRows.length,
|
|
1411
|
+
total: files,
|
|
1412
|
+
fresh,
|
|
1413
|
+
stale,
|
|
1414
|
+
missing,
|
|
1415
|
+
staleFiles,
|
|
1416
|
+
missingFiles,
|
|
1417
|
+
unresolved,
|
|
1418
|
+
isComplete: fileRows.length >= files,
|
|
1419
|
+
reasons: {
|
|
1420
|
+
byKind: reasonCounts,
|
|
1421
|
+
freshVerifiedByContent: reasonCounts["hash-match"],
|
|
1422
|
+
freshVerifiedByMetadata: reasonCounts["metadata-fast-path"],
|
|
1423
|
+
isSample: fileRows.length < files
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
embeddings: {
|
|
1427
|
+
total: embeddingTotal,
|
|
1428
|
+
embeddedEntities,
|
|
1429
|
+
symbolCoveragePercent: symbols > 0 ? Math.round(embeddedEntities / symbols * 1e3) / 10 : 0,
|
|
1430
|
+
models: modelRows.map((row) => ({ model: row.model, count: Number(row.c), dimensions: String(row.dims ?? "").split(",").filter(Boolean).map(Number) })),
|
|
1431
|
+
// the percentage above is a ratio of two counts, and what it
|
|
1432
|
+
// measures is narrower than the label admits. On this repo: 4,569
|
|
1433
|
+
// function + 2,836 class + 1,838 method + 652 interface = 9,895 embedded
|
|
1434
|
+
// entities against 45,018 symbols = "22%". All 9,895 are real
|
|
1435
|
+
// brain_symbol rows (0 orphans), so the arithmetic is sound — but 22% is
|
|
1436
|
+
// the share of the corpus made of the four kinds the embedder supports,
|
|
1437
|
+
// not the share of the corpus that is semantically searchable. The other
|
|
1438
|
+
// kinds are not partially covered; they are not attempted at all, and
|
|
1439
|
+
// that is a fact the reader could not see.
|
|
1440
|
+
embeddedByEntityType: entityTypeRows.map((row) => ({ entityType: String(row.entity_type), count: Number(row.c) })),
|
|
1441
|
+
unembeddedKinds: kindsWithNoEmbeddings(raw, projectId),
|
|
1442
|
+
// the join. `searchablePercent` is the share of that language's
|
|
1443
|
+
// symbols a semantic query can reach at all; a language at 0% is invisible
|
|
1444
|
+
// to bb_ask no matter how healthy the project looks overall.
|
|
1445
|
+
byLanguage: embeddedByLanguage
|
|
1446
|
+
},
|
|
1447
|
+
schema: { version: schema?.version ?? null, migration: schema?.migration_name ?? null, tables: tableCount },
|
|
1448
|
+
// `parserStatus` was typed `"available" | "unknown"`, hardcoded to
|
|
1449
|
+
// "unknown", and read by no code path in any mode. A field that cannot
|
|
1450
|
+
// vary and is never printed is not a measurement; it is a claim in a type.
|
|
1451
|
+
runtime: { runtime: typeof Bun !== "undefined" ? "bun" : "node", version: process.version, database: raw.driver ?? "sqlite" },
|
|
1452
|
+
timing,
|
|
1453
|
+
unindexed,
|
|
1454
|
+
scope,
|
|
1455
|
+
pathChecks,
|
|
1456
|
+
integrity,
|
|
1457
|
+
analysisRuns,
|
|
1458
|
+
refresh,
|
|
1459
|
+
effectiveConfig,
|
|
1460
|
+
embeddingChain,
|
|
1461
|
+
languages: languageRows,
|
|
1462
|
+
verbose: options.verbose === true,
|
|
1463
|
+
warnings
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function renderFileList(label, files, cap = 20) {
|
|
1467
|
+
if (files.length === 0) return `${label}: none`;
|
|
1468
|
+
if (files.length <= cap) return `${label}: ${files.join(", ")}`;
|
|
1469
|
+
return `${label}: ${files.length} total, first ${cap}: ${files.slice(0, cap).join(", ")} \u2014 use freshness="complete" and read freshness.staleFiles in the JSON for the rest`;
|
|
1470
|
+
}
|
|
1471
|
+
function renderTiming(t) {
|
|
1472
|
+
const parts = [];
|
|
1473
|
+
if (t.refreshMs) parts.push(`refresh=${t.refreshMs}ms`);
|
|
1474
|
+
if (t.integrityMs) parts.push(`integrity=${t.integrityMs}ms`);
|
|
1475
|
+
if (t.unindexedMs) parts.push(`unindexed-walk=${t.unindexedMs}ms`);
|
|
1476
|
+
if (t.freshnessMs) parts.push(`freshness=${t.freshnessMs}ms`);
|
|
1477
|
+
if (t.aggregatesMs) parts.push(`aggregates=${t.aggregatesMs}ms`);
|
|
1478
|
+
return `total=${t.totalMs}ms${parts.length ? ` (${parts.join(" ")})` : ""}`;
|
|
1479
|
+
}
|
|
1480
|
+
function firstSentence(text) {
|
|
1481
|
+
const trimmed = text.trim();
|
|
1482
|
+
let cut = trimmed.length;
|
|
1483
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
1484
|
+
const ch = trimmed[i];
|
|
1485
|
+
if (ch !== "." && ch !== "!" && ch !== "?") continue;
|
|
1486
|
+
const next = trimmed[i + 1];
|
|
1487
|
+
if (next === void 0 || next === " " || next === " " || next === "\n" || next === "\r") {
|
|
1488
|
+
cut = i + 1;
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
let out = "";
|
|
1493
|
+
let inSpace = false;
|
|
1494
|
+
for (let i = 0; i < cut; i++) {
|
|
1495
|
+
const ch = trimmed[i];
|
|
1496
|
+
const space = ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
1497
|
+
if (space) {
|
|
1498
|
+
if (!inSpace) out += " ";
|
|
1499
|
+
inSpace = true;
|
|
1500
|
+
} else {
|
|
1501
|
+
out += ch;
|
|
1502
|
+
inSpace = false;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return out.length > 400 ? `${out.slice(0, 397)}\u2026` : out;
|
|
1506
|
+
}
|
|
1507
|
+
function renderReasonMix(f) {
|
|
1508
|
+
const r = f.reasons.byKind;
|
|
1509
|
+
const parts = [];
|
|
1510
|
+
if (r["hash-match"]) parts.push(`${r["hash-match"]} fresh by content hash`);
|
|
1511
|
+
if (r["metadata-fast-path"]) parts.push(`${r["metadata-fast-path"]} fresh by mtime+size only (file not read \u2014 a same-size edit preserving the mtime reads as fresh)`);
|
|
1512
|
+
if (r["hash-differs"]) parts.push(`${r["hash-differs"]} stale: content changed (reindex these)`);
|
|
1513
|
+
if (r["file-gone"]) parts.push(`${r["file-gone"]} missing: deleted from disk (a refresh prunes them)`);
|
|
1514
|
+
if (r.unreadable) parts.push(`${r.unreadable} missing: unreadable (permissions or a non-file \u2014 these will fail every read)`);
|
|
1515
|
+
if (r["no-row"]) parts.push(`${r["no-row"]} missing: no index row for the id`);
|
|
1516
|
+
if (f.unresolved) parts.push(`${f.unresolved} unclassified`);
|
|
1517
|
+
if (!parts.length) return "nothing checked";
|
|
1518
|
+
return `${parts.join("; ")}${f.reasons.isSample ? ` \u2014 over the ${f.checked}-file SAMPLE, not the project` : ""}`;
|
|
1519
|
+
}
|
|
1520
|
+
function formatUnifiedHealth(report) {
|
|
1521
|
+
const c = report.counts, f = report.freshness, e = report.embeddings;
|
|
1522
|
+
const lastRunCountersOnly = report.indexStatus.totalFilesDiscovered !== null && report.indexStatus.totalFilesDiscovered < report.counts.files;
|
|
1523
|
+
const pre = [];
|
|
1524
|
+
if (report.scope && report.pathChecks) {
|
|
1525
|
+
pre.push(`SCOPED ANSWER for ${report.pathChecks.length} path(s) you named \u2014 everything below this block is PROJECT-WIDE:`);
|
|
1526
|
+
for (const p of report.pathChecks) {
|
|
1527
|
+
const bare = p.status === "gone" || p.status === "not-indexed" || p.status === "absent";
|
|
1528
|
+
const detail = [bare ? null : `${p.symbols} symbol(s)`, p.language, p.verifiedBy ? `verified by ${p.verifiedBy}` : null].filter(Boolean).join(", ");
|
|
1529
|
+
const stateWord = p.status === "gone" && p.reason === "unreadable" ? "gone?" : p.status;
|
|
1530
|
+
const goneNote = p.status === "gone" && p.reason === "unreadable" ? " (exists but could not be read \u2014 not deleted)" : "";
|
|
1531
|
+
pre.push(` ${stateWord.padEnd(12)} ${p.path}${detail ? ` \u2014 ${detail}` : ""}${bare ? ` \u2014 ${p.reason}${goneNote}` : ""}`);
|
|
1532
|
+
}
|
|
1533
|
+
if (report.scope.refused.length) pre.push(` REFUSED (not checked): ${report.scope.refused.map((r) => `${r.requested} \u2014 ${r.reason}`).join("; ")}`);
|
|
1534
|
+
pre.push("");
|
|
1535
|
+
}
|
|
1536
|
+
const staleIsEstimate = !f.isComplete;
|
|
1537
|
+
const staleShown = staleIsEstimate ? `${f.stale} of ${f.checked} sampled stale (~${Math.round(f.stale / Math.max(1, f.checked) * f.total)} of ${f.total} estimated)` : `${f.stale} of ${f.total} stale`;
|
|
1538
|
+
const lines = [
|
|
1539
|
+
`project: ${report.projectPath}`,
|
|
1540
|
+
`projectId: ${report.projectId}`,
|
|
1541
|
+
`files=${c.files} symbols=${c.symbols} relationships=${c.relationships} imports=${c.imports} syntaxErrors=${c.syntaxErrors}`,
|
|
1542
|
+
`freshness: ${f.fresh} fresh / ${staleShown} / ${f.missing} missing (${f.checked}/${f.total} checked, ${f.mode}${f.mode === "sampled" ? " \u2014 SAMPLE, not a census" : ""})`,
|
|
1543
|
+
// .R: the breakdown is in the DEFAULT output, not verbose, because the
|
|
1544
|
+
// defect is that "215 stale" is not actionable. "stale" is three conditions
|
|
1545
|
+
// with three different fixes, and a caller who has to opt in to learn which
|
|
1546
|
+
// one they have will not learn it.
|
|
1547
|
+
//
|
|
1548
|
+
// The two fresh kinds are named apart because they are different claims:
|
|
1549
|
+
// `hash-match` read the bytes, `metadata-fast-path` accepted an mtime+size
|
|
1550
|
+
// match and never opened the file. Collapsing them into "fresh" hides the
|
|
1551
|
+
// error bound of the whole verdict.
|
|
1552
|
+
`freshness why: ${renderReasonMix(f)}`,
|
|
1553
|
+
// the percentage is a ratio of two counts, so the two counts and
|
|
1554
|
+
// what they divide are all named. "22% symbol coverage" alone reads as
|
|
1555
|
+
// "22% of my code is semantically searchable"; 9,895 of 45,018 says the
|
|
1556
|
+
// share of the corpus that consists of the four kinds the embedder writes.
|
|
1557
|
+
`embeddings: ${e.total} rows / ${e.embeddedEntities} entities / ${e.symbolCoveragePercent}% of symbols`,
|
|
1558
|
+
`models: ${e.models.map((model) => `${model.model}:${model.count}[${model.dimensions.join(",")}]`).join(" ") || "none"}`,
|
|
1559
|
+
`schema: ${report.schema.version ?? "unknown"} (${report.schema.migration ?? "no migration record"}), ${report.schema.tables} tables`,
|
|
1560
|
+
// `indexedFiles=0 errors=0` for a project with no status record is
|
|
1561
|
+
// two zeros that are not measurements. Absence now prints as absence, so
|
|
1562
|
+
// the line cannot be read as a healthy zero next to files=1654.
|
|
1563
|
+
// a recorded shortfall prints as INCOMPLETE, so a half-built index
|
|
1564
|
+
// cannot be skimmed as a normal one.
|
|
1565
|
+
`index: version=${report.indexStatus.indexingVersion ?? "unrecorded"} indexedFiles=${report.indexStatus.indexedFiles ?? "unrecorded"}/${report.indexStatus.totalFilesDiscovered ?? "unrecorded"}${report.indexStatus.isIncomplete ? ` \u2014 INCOMPLETE, ${report.indexStatus.indexShortfall} file(s) found by the walk were never written` : ""}${lastRunCountersOnly ? " (last-run counters, not an index total)" : ""} status=${report.indexStatus.status ?? "unrecorded"} errors=${report.indexStatus.recordPresent ? report.indexStatus.errors.length : "unrecorded"}${report.indexStatus.recordPresent ? "" : " (no brain_index_status row for this project)"}`,
|
|
1566
|
+
// The last INDEX RUN, labelled as such: this is not the cost of the call you
|
|
1567
|
+
// just made, and the two were previously on adjacent lines under a bare
|
|
1568
|
+
// `perf:` with nothing to say which was which.
|
|
1569
|
+
`index run: schemaVersion=${report.performanceStats.schemaVersion ?? "none"} runs=${report.performanceStats.runs.length} latestWallMs=${report.performanceStats.latestRun?.phases?.totalWallMs ?? "n/a"} (the last INDEX RUN, not this call)`,
|
|
1570
|
+
// this call, measured. `total` is the number to act on; the phases
|
|
1571
|
+
// explain it, and are named only when non-zero so a warm call stays short.
|
|
1572
|
+
`this call: ${renderTiming(report.timing)}`,
|
|
1573
|
+
`runtime: ${report.runtime.runtime} ${report.runtime.version} / ${report.runtime.database}`
|
|
1574
|
+
];
|
|
1575
|
+
if (report.refresh.requested) {
|
|
1576
|
+
const d = report.refresh.derived;
|
|
1577
|
+
const dNote = d === "deferred" ? " \u2014 derived data DEFERRED; run: bb index --post" : d === "bounded" ? " (bounded pass)" : d === "full" ? " (full pass)" : d === "skipped-unchanged" ? " (unchanged \u2014 derived data still consistent)" : "";
|
|
1578
|
+
lines.push(`refresh: ${report.refresh.completed ? "completed" : report.refresh.wait ? "failed" : "started (pending)"} force=${report.refresh.force} wait=${report.refresh.wait} durationMs=${report.refresh.durationMs} derived=${d ?? "n/a (pending/failed)"}${dNote}`);
|
|
1579
|
+
}
|
|
1580
|
+
if (report.unindexed) {
|
|
1581
|
+
lines.push(report.unindexed.unindexedCount > 0 ? `unindexed: ${report.unindexed.unindexedCount} file(s) on disk that the indexer would index are NOT in the index (${report.unindexed.eligibleOnDisk} eligible on disk, ${report.unindexed.indexed} indexed) \u2014 e.g. ${report.unindexed.unindexedFiles.slice(0, 3).join(", ")}${report.unindexed.unindexedCount > 3 ? `, +${report.unindexed.unindexedCount - 3} more` : ""}` : `unindexed: none \u2014 all ${report.unindexed.eligibleOnDisk} indexable file(s) on disk are in the index (excludes files the indexer skips by rule: gitignored, node_modules, unsupported extension, >5MB)`);
|
|
1582
|
+
} else if (report.warnings.some((w) => w.code === "UNINDEXED_SCAN_FAILED")) {
|
|
1583
|
+
lines.push("unindexed: UNKNOWN \u2014 the scan failed, so index completeness is NOT verified");
|
|
1584
|
+
}
|
|
1585
|
+
if (report.analysisRuns) lines.push(`analysis: ${report.analysisRuns.runs} runs / ${report.analysisRuns.findings} findings (both are full-table counts, not page lengths)`);
|
|
1586
|
+
if (report.integrity) {
|
|
1587
|
+
const s = report.integrity.summary;
|
|
1588
|
+
lines.push(`integrity: ${report.integrity.overallHealth}% (${s.error} errors, ${s.warn} warnings, ${s.scored} scored + ${s.informational} informational checks)`);
|
|
1589
|
+
const failing = report.integrity.checks.filter((c2) => c2.status !== "ok");
|
|
1590
|
+
for (const c2 of failing.slice(0, 3)) {
|
|
1591
|
+
const fix = c2.remediation ? ` -> ${firstSentence(c2.remediation)}` : "";
|
|
1592
|
+
lines.push(` ${c2.status.toUpperCase().padEnd(5)} ${c2.name}: ${firstSentence(c2.description)}${fix}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (failing.length > 3) lines.push(` \u2026 and ${failing.length - 3} more non-ok check(s); read integrity.checks[] in the JSON for the full set`);
|
|
1595
|
+
}
|
|
1596
|
+
if (report.verbose && report.effectiveConfig) {
|
|
1597
|
+
const ec = report.effectiveConfig;
|
|
1598
|
+
lines.push(`embedding config: ${ec.configured ? "remote provider" : "not configured (local/tfidf fallback)"} \u2014 base-url=${ec.baseURL} (source: ${ec.sources.baseURL}), model=${ec.model} (source: ${ec.sources.model}), api-key source: ${ec.sources.apiKey}${ec.configured ? "" : ""}`);
|
|
1599
|
+
lines.push(`embedding precedence: env BB_EMBEDDING_* > config file (BB_CONFIG_FILE > bb.json > config.json) > default`);
|
|
1600
|
+
if (report.embeddingChain) {
|
|
1601
|
+
lines.push(`embedding chain: ${report.embeddingChain.disabled ? "disabled (BB_EMBEDDING_DISABLED) \u2014 local only" : report.embeddingChain.remoteReady ? "remote ready" : "remote NOT usable"}`);
|
|
1602
|
+
for (const line of renderChainSummary(report.embeddingChain)) lines.push(line);
|
|
1603
|
+
for (const p of report.embeddingChain.problems.filter((x) => !x.message.startsWith("No embedding provider is configured"))) {
|
|
1604
|
+
lines.push(`embedding problem: ${p.envVar ? `${p.envVar}: ` : ""}${p.message}`);
|
|
1605
|
+
}
|
|
1606
|
+
for (const f2 of report.embeddingChain.observedFailures) {
|
|
1607
|
+
lines.push(`embedding failure: ${f2.status} ${f2.kind} from ${f2.baseURL} \u2014 ${f2.advice}`);
|
|
1608
|
+
}
|
|
1609
|
+
if (report.embeddingChain.tierOrderDisagreement) {
|
|
1610
|
+
lines.push(`embedding warning: ${report.embeddingChain.tierOrderDisagreement.detail}`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (report.verbose) {
|
|
1615
|
+
if (report.languages.length) lines.push(`languages: ${report.languages.map((l) => `${l.language}:${l.files}f/${l.symbols}s`).join(" ")}`);
|
|
1616
|
+
const byLang = report.embeddings.byLanguage.filter((l) => l.symbols > 0);
|
|
1617
|
+
if (byLang.length) {
|
|
1618
|
+
lines.push(`searchable by language: ${byLang.map((l) => `${l.language}:${l.embedded}/${l.symbols}(${l.searchablePercent}%)`).join(" ")}`);
|
|
1619
|
+
const dead = byLang.filter((l) => l.embedded === 0).map((l) => l.language);
|
|
1620
|
+
if (dead.length) lines.push(`unsearchable languages: ${dead.join(", ")} \u2014 no symbol in these has a vector, so bb_ask cannot return them`);
|
|
1621
|
+
}
|
|
1622
|
+
if (report.embeddings.embeddedByEntityType.length) {
|
|
1623
|
+
lines.push(`embedded kinds: ${report.embeddings.embeddedByEntityType.map((k) => `${k.entityType}=${k.count}`).join(" ")}`);
|
|
1624
|
+
}
|
|
1625
|
+
if (report.embeddings.unembeddedKinds.length) {
|
|
1626
|
+
lines.push(`unembedded kinds: ${report.embeddings.unembeddedKinds.map((k) => `${k.kind}=${k.count}`).join(" ")} \u2014 these symbols are NOT semantically searchable (never attempted, not partially covered)`);
|
|
1627
|
+
}
|
|
1628
|
+
lines.push(renderFileList("stale files", report.freshness.staleFiles));
|
|
1629
|
+
lines.push(renderFileList("missing files", report.freshness.missingFiles));
|
|
1630
|
+
}
|
|
1631
|
+
for (const warning of report.warnings) lines.push(`${warning.severity}: [${warning.code}] ${warning.message}`);
|
|
1632
|
+
return [...pre, ...lines].join("\n");
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/ast/auto-refresh.ts
|
|
1636
|
+
import { statSync as statSync2 } from "fs";
|
|
1637
|
+
var AUTO_REFRESH_DEFAULTS = { maxFiles: 25, maxBytes: 2 * 1024 * 1024 };
|
|
1638
|
+
function autoRefreshEnabled(perCallOptOut) {
|
|
1639
|
+
if (perCallOptOut === true) return false;
|
|
1640
|
+
if (process.env.BB_NO_AUTO_REFRESH === "1") return false;
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
function autoRefreshBudget() {
|
|
1644
|
+
const n = Number(process.env.BB_AUTO_REFRESH_MAX_FILES);
|
|
1645
|
+
const b = Number(process.env.BB_AUTO_REFRESH_MAX_BYTES);
|
|
1646
|
+
return {
|
|
1647
|
+
maxFiles: Number.isInteger(n) && n >= 1 ? n : AUTO_REFRESH_DEFAULTS.maxFiles,
|
|
1648
|
+
maxBytes: Number.isFinite(b) && b >= 1 ? b : AUTO_REFRESH_DEFAULTS.maxBytes
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
function collectStaleFiles(raw, rows, statuses) {
|
|
1652
|
+
const staleIds = [...new Set(rows.filter((r) => r.fileId && statuses.get(r.fileId) !== "fresh").map((r) => r.fileId))];
|
|
1653
|
+
const pathById = /* @__PURE__ */ new Map();
|
|
1654
|
+
for (let i = 0; i < staleIds.length; i += 500) {
|
|
1655
|
+
const slice = staleIds.slice(i, i + 500);
|
|
1656
|
+
for (const r of raw.prepare(`SELECT id, path FROM brain_file WHERE id IN (${slice.map(() => "?").join(",")})`).all(...slice)) {
|
|
1657
|
+
pathById.set(r.id, r.path);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const out = [];
|
|
1661
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1662
|
+
for (const r of rows) {
|
|
1663
|
+
const key = r.fileId ?? r.filePath ?? "";
|
|
1664
|
+
const status = statuses.get(key);
|
|
1665
|
+
if (!key || !status || status === "fresh") continue;
|
|
1666
|
+
let abs;
|
|
1667
|
+
if (r.fileId) abs = pathById.get(r.fileId);
|
|
1668
|
+
else if (r.filePath && r.filePath.startsWith("/")) abs = r.filePath;
|
|
1669
|
+
if (!abs || seen.has(abs)) continue;
|
|
1670
|
+
seen.add(abs);
|
|
1671
|
+
out.push({ absolutePath: abs, type: status === "missing" ? "deleted" : "modified" });
|
|
1672
|
+
}
|
|
1673
|
+
return out;
|
|
1674
|
+
}
|
|
1675
|
+
async function maybeAutoRefresh(projectId, projectPath, files, opts = {}) {
|
|
1676
|
+
const modified = files.filter((f) => f.type === "modified");
|
|
1677
|
+
const deleted = files.filter((f) => f.type === "deleted");
|
|
1678
|
+
if (modified.length + deleted.length === 0) return { action: "clean" };
|
|
1679
|
+
if (!autoRefreshEnabled(opts.perCallOptOut)) {
|
|
1680
|
+
return { action: "declined", reason: "disabled", detail: "auto-refresh disabled (BB_NO_AUTO_REFRESH or refresh=false)" };
|
|
1681
|
+
}
|
|
1682
|
+
const { maxFiles, maxBytes } = autoRefreshBudget();
|
|
1683
|
+
const total = modified.length + deleted.length;
|
|
1684
|
+
if (total > maxFiles) {
|
|
1685
|
+
return {
|
|
1686
|
+
action: "declined",
|
|
1687
|
+
reason: "budget-files",
|
|
1688
|
+
detail: `auto-refresh declined: ${total} stale/missing files exceeds the ${maxFiles}-file budget (BB_AUTO_REFRESH_MAX_FILES)`
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
let bytes = 0;
|
|
1692
|
+
for (const f of modified) {
|
|
1693
|
+
try {
|
|
1694
|
+
bytes += statSync2(f.absolutePath).size;
|
|
1695
|
+
} catch {
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (bytes > maxBytes) {
|
|
1699
|
+
return {
|
|
1700
|
+
action: "declined",
|
|
1701
|
+
reason: "budget-bytes",
|
|
1702
|
+
detail: `auto-refresh declined: ${bytes} bytes of modified files exceeds the ${maxBytes}-byte budget (BB_AUTO_REFRESH_MAX_BYTES)`
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
const holder = liveIndexLockHolder(projectId);
|
|
1706
|
+
if (holder) {
|
|
1707
|
+
return {
|
|
1708
|
+
action: "declined",
|
|
1709
|
+
reason: "indexer-active",
|
|
1710
|
+
detail: `auto-refresh declined: another index run is active (pid ${holder.pid}, kind=${holder.kind}); answering from existing data with the staleness envelope`
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
const t0 = performance.now();
|
|
1714
|
+
try {
|
|
1715
|
+
const r = await reindexFilesParallel(projectId, projectPath, files.map((f) => ({ path: f.absolutePath, type: f.type })));
|
|
1716
|
+
return {
|
|
1717
|
+
action: "refreshed",
|
|
1718
|
+
files: r.indexed,
|
|
1719
|
+
deleted: r.deleted,
|
|
1720
|
+
errors: r.errors,
|
|
1721
|
+
tookMs: Math.round(performance.now() - t0),
|
|
1722
|
+
derivedPass: r.derived?.pass
|
|
1723
|
+
};
|
|
1724
|
+
} catch (e) {
|
|
1725
|
+
if (e instanceof IndexLockHeldError) {
|
|
1726
|
+
return {
|
|
1727
|
+
action: "declined",
|
|
1728
|
+
reason: "indexer-active",
|
|
1729
|
+
detail: `auto-refresh declined: another index run is active (pid ${e.holder.pid}); answering from existing data with the staleness envelope`
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
return {
|
|
1733
|
+
action: "declined",
|
|
1734
|
+
reason: "error",
|
|
1735
|
+
detail: `auto-refresh failed: ${String(e?.message ?? e).slice(0, 200)}; answering from existing data with the staleness envelope`
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function autoRefreshLine(decision) {
|
|
1740
|
+
if (decision.action === "refreshed") {
|
|
1741
|
+
return `auto-refreshed ${decision.files} file${decision.files === 1 ? "" : "s"}${decision.deleted ? `, removed ${decision.deleted} deleted` : ""} in ${decision.tookMs}ms${decision.derivedPass ? ` (derived pass: ${decision.derivedPass})` : ""}`;
|
|
1742
|
+
}
|
|
1743
|
+
if (decision.action === "declined") return decision.detail;
|
|
1744
|
+
return "";
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
export {
|
|
1748
|
+
detectOrphans,
|
|
1749
|
+
BrainTools,
|
|
1750
|
+
PatternBrainTools,
|
|
1751
|
+
getAnalysisHistory,
|
|
1752
|
+
executeAnalysis,
|
|
1753
|
+
getUnifiedHealth,
|
|
1754
|
+
formatUnifiedHealth,
|
|
1755
|
+
collectStaleFiles,
|
|
1756
|
+
maybeAutoRefresh,
|
|
1757
|
+
autoRefreshLine
|
|
1758
|
+
};
|