@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,4301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
USAGE_COUNT_UNIT_LABEL,
|
|
3
|
+
USAGE_SITE_FILTER_LABEL,
|
|
4
|
+
countUnresolvedNameMatches,
|
|
5
|
+
isDeclarationRelationship,
|
|
6
|
+
isUsageSiteRow,
|
|
7
|
+
usageSites
|
|
8
|
+
} from "../chunk-GBI7V7KY.js";
|
|
9
|
+
import {
|
|
10
|
+
TypeInference
|
|
11
|
+
} from "../chunk-UFE77L67.js";
|
|
12
|
+
import {
|
|
13
|
+
UNRESOLVED_TYPE,
|
|
14
|
+
crossLanguageSection,
|
|
15
|
+
factsFromRows,
|
|
16
|
+
parseParameters,
|
|
17
|
+
renderSignature,
|
|
18
|
+
storedCrossLanguageRows
|
|
19
|
+
} from "../chunk-RM3PY6TG.js";
|
|
20
|
+
import {
|
|
21
|
+
classifyStoredRow,
|
|
22
|
+
orderTiers,
|
|
23
|
+
primaryTier
|
|
24
|
+
} from "../chunk-RCMTFSEF.js";
|
|
25
|
+
import {
|
|
26
|
+
ADDRESS_NOTES,
|
|
27
|
+
STORED_LINE_SELECT,
|
|
28
|
+
addressLine0,
|
|
29
|
+
deriveCallSiteAddress,
|
|
30
|
+
isExactAddress,
|
|
31
|
+
storedLineFromRow,
|
|
32
|
+
traverseGraph
|
|
33
|
+
} from "../chunk-47HKX4AJ.js";
|
|
34
|
+
import {
|
|
35
|
+
isCommentNodeType,
|
|
36
|
+
isIdentifierNodeType,
|
|
37
|
+
parseForSafety
|
|
38
|
+
} from "../chunk-YI6FF2ZR.js";
|
|
39
|
+
import {
|
|
40
|
+
getProjectId
|
|
41
|
+
} from "../chunk-DIYAGQJL.js";
|
|
42
|
+
import "../chunk-UUNHHX4M.js";
|
|
43
|
+
import {
|
|
44
|
+
BACKBENCHER_BRAIN_VERSION
|
|
45
|
+
} from "../chunk-6WNGD3NN.js";
|
|
46
|
+
import {
|
|
47
|
+
checkFileFreshness,
|
|
48
|
+
hashFileContent
|
|
49
|
+
} from "../chunk-QLK6ED4Q.js";
|
|
50
|
+
import {
|
|
51
|
+
BrainDatabase
|
|
52
|
+
} from "../chunk-KLK3PBTO.js";
|
|
53
|
+
import {
|
|
54
|
+
ASTEngine
|
|
55
|
+
} from "../chunk-V44PGPA6.js";
|
|
56
|
+
import "../chunk-BQ3OVQ3P.js";
|
|
57
|
+
import "../chunk-SK6YVHFZ.js";
|
|
58
|
+
import "../chunk-2BSKD7O3.js";
|
|
59
|
+
import "../chunk-KAET47ZL.js";
|
|
60
|
+
import "../chunk-KCDECRJY.js";
|
|
61
|
+
import "../chunk-SM7RFQRO.js";
|
|
62
|
+
import "../chunk-L5TDYKYK.js";
|
|
63
|
+
import "../chunk-ROMM6KMC.js";
|
|
64
|
+
import "../chunk-MLKGABMK.js";
|
|
65
|
+
|
|
66
|
+
// src/lsp/server.ts
|
|
67
|
+
import path2 from "path";
|
|
68
|
+
import { realpathSync, appendFileSync } from "fs";
|
|
69
|
+
|
|
70
|
+
// src/lsp/document_highlight.ts
|
|
71
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
72
|
+
|
|
73
|
+
// src/lsp/hover.ts
|
|
74
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
75
|
+
|
|
76
|
+
// src/lsp/positions.ts
|
|
77
|
+
var PositionError = class extends Error {
|
|
78
|
+
constructor(message) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = "PositionError";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
function resolveRowByteColumns(sourceText, line, byte, linesIn) {
|
|
84
|
+
if (!Number.isInteger(line) || line < 1 || !Number.isInteger(byte) || byte < 0) return null;
|
|
85
|
+
const lines = linesIn ?? sourceText.split("\n");
|
|
86
|
+
if (line > lines.length) return null;
|
|
87
|
+
if (byte === 0) return line === 1 ? { line: 1, column: 1 } : null;
|
|
88
|
+
const line0 = line - 1;
|
|
89
|
+
const lineByteLen = Buffer.byteLength(lines[line0], "utf8");
|
|
90
|
+
if (byte <= lineByteLen + 1) return { line, column: byte };
|
|
91
|
+
let cumStart = 0;
|
|
92
|
+
for (let i = 0; i < lines.length; i++) {
|
|
93
|
+
const len = Buffer.byteLength(lines[i], "utf8") + 1;
|
|
94
|
+
if (byte >= cumStart && byte < cumStart + len) {
|
|
95
|
+
const col = byte - cumStart + 1;
|
|
96
|
+
if (col <= Buffer.byteLength(lines[i], "utf8") + 1) return { line: i + 1, column: col };
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
cumStart += len;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
var PositionMapper = class {
|
|
104
|
+
/** 0-based line -> UTF-16 code unit length of that line (excluding terminator) */
|
|
105
|
+
lineUtf16Lengths;
|
|
106
|
+
/** 0-based line -> byte length of that line (excluding terminator) */
|
|
107
|
+
lineByteLengths;
|
|
108
|
+
/** 0-based line -> the line's text (excluding terminator) */
|
|
109
|
+
lineTexts;
|
|
110
|
+
constructor(sourceText) {
|
|
111
|
+
this.lineTexts = sourceText.split(/\r\n|\r|\n/);
|
|
112
|
+
this.lineUtf16Lengths = this.lineTexts.map((line) => line.length);
|
|
113
|
+
this.lineByteLengths = this.lineTexts.map((line) => Buffer.byteLength(line, "utf8"));
|
|
114
|
+
}
|
|
115
|
+
assertLine(line0) {
|
|
116
|
+
if (!Number.isInteger(line0) || line0 < 0 || line0 >= this.lineTexts.length) {
|
|
117
|
+
throw new PositionError(`line ${line0} out of range (0..${this.lineTexts.length - 1})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Convert a byte offset within a line to a UTF-16 code unit offset.
|
|
122
|
+
* The byte offset must land on a code point boundary.
|
|
123
|
+
*/
|
|
124
|
+
byteOffsetToUtf16Offset(line0, byteOffset) {
|
|
125
|
+
this.assertLine(line0);
|
|
126
|
+
const text = this.lineTexts[line0];
|
|
127
|
+
const byteLen = this.lineByteLengths[line0];
|
|
128
|
+
if (!Number.isInteger(byteOffset) || byteOffset < 0 || byteOffset > byteLen) {
|
|
129
|
+
throw new PositionError(`byte offset ${byteOffset} out of range (0..${byteLen}) on line ${line0 + 1}`);
|
|
130
|
+
}
|
|
131
|
+
if (byteOffset === 0) return 0;
|
|
132
|
+
if (byteOffset === byteLen) return text.length;
|
|
133
|
+
const buf = Buffer.from(text, "utf8");
|
|
134
|
+
if ((buf[byteOffset] & 192) === 128) {
|
|
135
|
+
throw new PositionError(`byte offset ${byteOffset} on line ${line0 + 1} lands mid-code-point`);
|
|
136
|
+
}
|
|
137
|
+
return buf.subarray(0, byteOffset).toString("utf8").length;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Convert a UTF-16 code unit offset within a line to a byte offset.
|
|
141
|
+
* A low-surrogate offset is not a character start — fail rather than guess.
|
|
142
|
+
*/
|
|
143
|
+
utf16OffsetToByteOffset(line0, utf16Offset) {
|
|
144
|
+
this.assertLine(line0);
|
|
145
|
+
const text = this.lineTexts[line0];
|
|
146
|
+
const utf16Len = this.lineUtf16Lengths[line0];
|
|
147
|
+
if (!Number.isInteger(utf16Offset) || utf16Offset < 0 || utf16Offset > utf16Len) {
|
|
148
|
+
throw new PositionError(`utf16 offset ${utf16Offset} out of range (0..${utf16Len}) on line ${line0 + 1}`);
|
|
149
|
+
}
|
|
150
|
+
if (utf16Offset === 0) return 0;
|
|
151
|
+
if (utf16Offset === utf16Len) return this.lineByteLengths[line0];
|
|
152
|
+
const unit = text.charCodeAt(utf16Offset);
|
|
153
|
+
if (unit >= 56320 && unit <= 57343) {
|
|
154
|
+
throw new PositionError(`utf16 offset ${utf16Offset} on line ${line0 + 1} lands on a surrogate trailing half`);
|
|
155
|
+
}
|
|
156
|
+
return Buffer.byteLength(text.slice(0, utf16Offset), "utf8");
|
|
157
|
+
}
|
|
158
|
+
indexToLsp(pos) {
|
|
159
|
+
if (!Number.isInteger(pos.line) || pos.line < 1) {
|
|
160
|
+
throw new PositionError(`index line must be >= 1, got ${pos.line}`);
|
|
161
|
+
}
|
|
162
|
+
if (!Number.isInteger(pos.column) || pos.column < 1) {
|
|
163
|
+
throw new PositionError(`index column must be >= 1, got ${pos.column}`);
|
|
164
|
+
}
|
|
165
|
+
const line0 = pos.line - 1;
|
|
166
|
+
const character = this.byteOffsetToUtf16Offset(line0, pos.column - 1);
|
|
167
|
+
return { line: line0, character };
|
|
168
|
+
}
|
|
169
|
+
lspToIndex(pos) {
|
|
170
|
+
if (!Number.isInteger(pos.line) || pos.line < 0) {
|
|
171
|
+
throw new PositionError(`LSP line must be >= 0, got ${pos.line}`);
|
|
172
|
+
}
|
|
173
|
+
if (!Number.isInteger(pos.character) || pos.character < 0) {
|
|
174
|
+
throw new PositionError(`LSP character must be >= 0, got ${pos.character}`);
|
|
175
|
+
}
|
|
176
|
+
const line0 = pos.line;
|
|
177
|
+
const byteOffset = this.utf16OffsetToByteOffset(line0, pos.character);
|
|
178
|
+
return { line: line0 + 1, column: byteOffset + 1 };
|
|
179
|
+
}
|
|
180
|
+
indexRangeToLsp(range) {
|
|
181
|
+
return {
|
|
182
|
+
start: this.indexToLsp(range.start),
|
|
183
|
+
end: this.indexToLsp(range.end)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
lspRangeToIndex(range) {
|
|
187
|
+
return {
|
|
188
|
+
start: this.lspToIndex(range.start),
|
|
189
|
+
end: this.lspToIndex(range.end)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function createPositionMapper(sourceText) {
|
|
194
|
+
const mapper = new PositionMapper(sourceText);
|
|
195
|
+
return Object.assign(mapper, { get lineCount() {
|
|
196
|
+
return mapper.lineTexts.length;
|
|
197
|
+
} });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/lsp/annotation_ui.ts
|
|
201
|
+
function escapeAnnotationText(text) {
|
|
202
|
+
return text.replace(/[\r\n\u2028\u2029]+/g, " ").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
203
|
+
}
|
|
204
|
+
function formatAnnotationTime(epochMs) {
|
|
205
|
+
return new Date(epochMs).toISOString().slice(0, 10);
|
|
206
|
+
}
|
|
207
|
+
function annotationHoverSection(rows) {
|
|
208
|
+
if (!Array.isArray(rows) || rows.length === 0) return "";
|
|
209
|
+
const lines = ["**notes** (agent/human annotations \u2014 provenance recorded, not index-derived):"];
|
|
210
|
+
for (const row of rows) {
|
|
211
|
+
const who = `${escapeAnnotationText(row.author_class)} note by ${escapeAnnotationText(row.tool_identity)}`;
|
|
212
|
+
const when = formatAnnotationTime(row.time_updated);
|
|
213
|
+
const scope = row.symbol_path === null || row.symbol_path === void 0 ? "name-level" : "this file";
|
|
214
|
+
lines.push(`- [${who}, ${when}, ${scope}]: ${escapeAnnotationText(row.body)}`);
|
|
215
|
+
}
|
|
216
|
+
return lines.join("\n");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/lsp/doc_comment.ts
|
|
220
|
+
function stripCommentMarkup(raw) {
|
|
221
|
+
const lines = raw.split("\n");
|
|
222
|
+
const out = [];
|
|
223
|
+
for (let i = 0; i < lines.length; i++) {
|
|
224
|
+
let l = lines[i].replace(/\r$/, "");
|
|
225
|
+
if (i === 0) l = l.replace(/^\s*\/\*+\s?/, "").replace(/^\s*#+\s?/, "").replace(/^\s*--+\s?/, "");
|
|
226
|
+
if (i === lines.length - 1) l = l.replace(/\s?\*+\/\s*$/, "");
|
|
227
|
+
l = l.replace(/^\s*\*\s?/, "");
|
|
228
|
+
l = l.replace(/^\s*\/\/\/\s?/, "");
|
|
229
|
+
l = l.replace(/^\s*#\s?/, "");
|
|
230
|
+
out.push(l.replace(/\s+$/, ""));
|
|
231
|
+
}
|
|
232
|
+
while (out.length > 0 && out[0].trim() === "") out.shift();
|
|
233
|
+
while (out.length > 0 && out[out.length - 1].trim() === "") out.pop();
|
|
234
|
+
return out.join("\n").trim();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/lsp/hover_cache.ts
|
|
238
|
+
var CACHE_BYTE_CEILING = 4 * 1024 * 1024;
|
|
239
|
+
var CACHE_ENTRY_CEILING = 8;
|
|
240
|
+
var EXCLUDED_LANGUAGES = {
|
|
241
|
+
lua: "ASTEngine.parseRaw gives lua a private parser and grammar per parse so external-scanner state cannot leak between files (engine.ts parseRaw); a retained lua tree would carry that state into the next request"
|
|
242
|
+
};
|
|
243
|
+
var entries = /* @__PURE__ */ new Map();
|
|
244
|
+
var retainedBytes = 0;
|
|
245
|
+
var tick = 0;
|
|
246
|
+
var hits = 0;
|
|
247
|
+
var misses = 0;
|
|
248
|
+
var bypasses = 0;
|
|
249
|
+
var evictionsBytes = 0;
|
|
250
|
+
var evictionsEntries = 0;
|
|
251
|
+
function cacheStats() {
|
|
252
|
+
return {
|
|
253
|
+
entries: entries.size,
|
|
254
|
+
retainedBytes,
|
|
255
|
+
byteCeiling: CACHE_BYTE_CEILING,
|
|
256
|
+
entryCeiling: CACHE_ENTRY_CEILING,
|
|
257
|
+
hits,
|
|
258
|
+
misses,
|
|
259
|
+
bypasses,
|
|
260
|
+
evictionsBytes,
|
|
261
|
+
evictionsEntries,
|
|
262
|
+
excludedLanguages: { ...EXCLUDED_LANGUAGES }
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function evict() {
|
|
266
|
+
while (entries.size > CACHE_ENTRY_CEILING || retainedBytes > CACHE_BYTE_CEILING) {
|
|
267
|
+
let oldestKey = null;
|
|
268
|
+
let oldest = Number.POSITIVE_INFINITY;
|
|
269
|
+
for (const [k, v] of entries) {
|
|
270
|
+
if (v.lastUsed < oldest) {
|
|
271
|
+
oldest = v.lastUsed;
|
|
272
|
+
oldestKey = k;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (oldestKey === null) break;
|
|
276
|
+
const dropped = entries.get(oldestKey);
|
|
277
|
+
try {
|
|
278
|
+
if (dropped.tree && typeof dropped.tree.delete === "function") dropped.tree.delete();
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
entries.delete(oldestKey);
|
|
282
|
+
retainedBytes -= dropped.bytes;
|
|
283
|
+
if (retainedBytes < 0) retainedBytes = 0;
|
|
284
|
+
if (entries.size > CACHE_ENTRY_CEILING) evictionsEntries++;
|
|
285
|
+
else evictionsBytes++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function parseForCursor(filePath, content) {
|
|
289
|
+
const contentHash = hashFileContent(content);
|
|
290
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
291
|
+
const detected = languageFor(filePath, content);
|
|
292
|
+
if (detected !== null && EXCLUDED_LANGUAGES[detected] !== void 0) {
|
|
293
|
+
bypasses++;
|
|
294
|
+
return guardedParse(filePath, content, contentHash, bytes, "cache-bypass");
|
|
295
|
+
}
|
|
296
|
+
const entry = entries.get(contentHash);
|
|
297
|
+
if (entry !== void 0) {
|
|
298
|
+
hits++;
|
|
299
|
+
tick++;
|
|
300
|
+
entry.lastUsed = tick;
|
|
301
|
+
return { tree: entry.tree, language: entry.language, source: "cache-hit", contentHash, bytes, cache: cacheStats() };
|
|
302
|
+
}
|
|
303
|
+
misses++;
|
|
304
|
+
return guardedParse(filePath, content, contentHash, bytes, "fresh");
|
|
305
|
+
}
|
|
306
|
+
var ParseUnavailable = class extends Error {
|
|
307
|
+
/** Why the file could not be parsed, verbatim from the engine. */
|
|
308
|
+
reason;
|
|
309
|
+
constructor(reason) {
|
|
310
|
+
super(`the file could not be parsed: ${reason}`);
|
|
311
|
+
this.name = "ParseUnavailable";
|
|
312
|
+
this.reason = reason;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
async function guardedParse(filePath, content, contentHash, bytes, source) {
|
|
316
|
+
let parsed;
|
|
317
|
+
try {
|
|
318
|
+
parsed = await ASTEngine.parseTree(filePath, content);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
throw new ParseUnavailable(error instanceof Error ? error.message : String(error));
|
|
321
|
+
}
|
|
322
|
+
const { language, tree } = parsed;
|
|
323
|
+
if (source === "fresh") {
|
|
324
|
+
entries.set(contentHash, { hash: contentHash, language, bytes, tree, lastUsed: ++tick });
|
|
325
|
+
retainedBytes += bytes;
|
|
326
|
+
evict();
|
|
327
|
+
}
|
|
328
|
+
return { tree, language, source, contentHash, bytes, cache: cacheStats() };
|
|
329
|
+
}
|
|
330
|
+
function languageFor(filePath, content) {
|
|
331
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
332
|
+
if (ext === ".lua") return "lua";
|
|
333
|
+
if (content.startsWith("#!")) return "lua";
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
function occurrenceAtPoint(tree, line, byteColumn) {
|
|
337
|
+
if (!Number.isInteger(line) || line < 1 || !Number.isInteger(byteColumn) || byteColumn < 1) return null;
|
|
338
|
+
let node = tree.rootNode.descendantForPosition({ row: line - 1, column: byteColumn - 1 });
|
|
339
|
+
while (node !== null && node !== void 0) {
|
|
340
|
+
const type = node.type;
|
|
341
|
+
if (!isCommentNodeType(type) && node.isNamed && isIdentifierNodeType(type) && node.namedChildCount === 0) {
|
|
342
|
+
const text = String(node.text ?? "");
|
|
343
|
+
if (text.length > 0) {
|
|
344
|
+
return {
|
|
345
|
+
text,
|
|
346
|
+
startLine: node.startPosition.row + 1,
|
|
347
|
+
startColumn: node.startPosition.column + 1,
|
|
348
|
+
endColumn: node.endPosition.column + 1
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
node = node.parent;
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
function docCommentAt(tree, declarationLine) {
|
|
357
|
+
if (!tree || declarationLine === null || declarationLine === void 0) return "";
|
|
358
|
+
const targetRow = declarationLine - 2;
|
|
359
|
+
if (targetRow < 0) return "";
|
|
360
|
+
let found = null;
|
|
361
|
+
const visit = (n) => {
|
|
362
|
+
if (found !== null || n === null || n === void 0) return;
|
|
363
|
+
const startRow = Number(n.startPosition?.row ?? 0);
|
|
364
|
+
const endRow = Number(n.endPosition?.row ?? 0);
|
|
365
|
+
if (startRow > targetRow || endRow < targetRow) return;
|
|
366
|
+
if (isCommentNodeType(String(n.type))) {
|
|
367
|
+
if (endRow === targetRow) found = stripCommentMarkup(String(n.text ?? ""));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const kids = n.children ?? [];
|
|
371
|
+
for (let i = 0; i < kids.length; i++) visit(kids[i]);
|
|
372
|
+
};
|
|
373
|
+
visit(tree.rootNode);
|
|
374
|
+
return found ?? "";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// src/lsp/type_inference_surface.ts
|
|
378
|
+
import { readFileSync } from "fs";
|
|
379
|
+
var ENGINE_CONFIDENCE_FLOOR = 0.9;
|
|
380
|
+
var MAX_WITNESS_BYTES = 48 * 1024;
|
|
381
|
+
var STRING_NODES = /* @__PURE__ */ new Set([
|
|
382
|
+
"string",
|
|
383
|
+
"string_literal",
|
|
384
|
+
"template_string",
|
|
385
|
+
"string_fragment",
|
|
386
|
+
"raw_string",
|
|
387
|
+
"verbatim_string_literal",
|
|
388
|
+
"interpolated_string_expression",
|
|
389
|
+
"char_literal"
|
|
390
|
+
]);
|
|
391
|
+
var NUMBER_NODES = /* @__PURE__ */ new Set([
|
|
392
|
+
"number",
|
|
393
|
+
"integer_literal",
|
|
394
|
+
"float_literal",
|
|
395
|
+
"decimal_integer_literal",
|
|
396
|
+
"decimal_floating_point_literal",
|
|
397
|
+
"hex_integer_literal",
|
|
398
|
+
"int_literal",
|
|
399
|
+
"float_literal_node",
|
|
400
|
+
"integer"
|
|
401
|
+
]);
|
|
402
|
+
var BOOLEAN_NODES = /* @__PURE__ */ new Set(["true", "false", "boolean", "bool_literal", "true_literal", "false_literal"]);
|
|
403
|
+
var RETURN_NODES = /* @__PURE__ */ new Set(["return_statement", "return_expression", "return_token", "yield_statement"]);
|
|
404
|
+
var CALLABLE_NODES = /* @__PURE__ */ new Set([
|
|
405
|
+
"function_declaration",
|
|
406
|
+
"function_definition",
|
|
407
|
+
"function_item",
|
|
408
|
+
"method_declaration",
|
|
409
|
+
"method_definition",
|
|
410
|
+
"method",
|
|
411
|
+
"constructor_declaration",
|
|
412
|
+
"singleton_method",
|
|
413
|
+
"arrow_function",
|
|
414
|
+
"function_expression",
|
|
415
|
+
"function",
|
|
416
|
+
"def",
|
|
417
|
+
"defn",
|
|
418
|
+
"fn",
|
|
419
|
+
"func",
|
|
420
|
+
"local_function_statement",
|
|
421
|
+
"function_signature",
|
|
422
|
+
"subroutine"
|
|
423
|
+
]);
|
|
424
|
+
var ENGINE_DECL_TYPES = [
|
|
425
|
+
"variable_declaration",
|
|
426
|
+
"let_declaration",
|
|
427
|
+
"const_declaration",
|
|
428
|
+
"var_declaration",
|
|
429
|
+
"local_variable_declaration",
|
|
430
|
+
"assignment_statement"
|
|
431
|
+
];
|
|
432
|
+
var ENGINE_FUNC_TYPES = [
|
|
433
|
+
"function_declaration",
|
|
434
|
+
"function_definition",
|
|
435
|
+
"method_declaration",
|
|
436
|
+
"def",
|
|
437
|
+
"defn",
|
|
438
|
+
"fn",
|
|
439
|
+
"func"
|
|
440
|
+
];
|
|
441
|
+
function literalKindOf(node) {
|
|
442
|
+
if (node === null || node === void 0) return "valueless";
|
|
443
|
+
const t = node.type;
|
|
444
|
+
if (STRING_NODES.has(t)) return "string";
|
|
445
|
+
if (NUMBER_NODES.has(t)) return "number";
|
|
446
|
+
if (BOOLEAN_NODES.has(t)) return "boolean";
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
function kindForCandidate(candidate) {
|
|
450
|
+
const c = candidate.trim();
|
|
451
|
+
const table = {
|
|
452
|
+
string: "string",
|
|
453
|
+
String: "string",
|
|
454
|
+
number: "number",
|
|
455
|
+
int: "number",
|
|
456
|
+
long: "number",
|
|
457
|
+
double: "number",
|
|
458
|
+
float: "number",
|
|
459
|
+
i32: "number",
|
|
460
|
+
i64: "number",
|
|
461
|
+
f32: "number",
|
|
462
|
+
f64: "number",
|
|
463
|
+
decimal: "number",
|
|
464
|
+
boolean: "boolean",
|
|
465
|
+
bool: "boolean"
|
|
466
|
+
};
|
|
467
|
+
return Object.prototype.hasOwnProperty.call(table, c) ? table[c] : null;
|
|
468
|
+
}
|
|
469
|
+
function collectFacts(root) {
|
|
470
|
+
const nodeTypes = /* @__PURE__ */ new Set();
|
|
471
|
+
const callables = [];
|
|
472
|
+
const returns = [];
|
|
473
|
+
const stack = [root];
|
|
474
|
+
while (stack.length > 0) {
|
|
475
|
+
const n = stack.pop();
|
|
476
|
+
nodeTypes.add(n.type);
|
|
477
|
+
if (CALLABLE_NODES.has(n.type)) callables.push(n);
|
|
478
|
+
if (RETURN_NODES.has(n.type)) {
|
|
479
|
+
const value = n.childForFieldName?.("expression") ?? n.childForFieldName?.("value") ?? n.childForFieldName?.("result") ?? n.namedChildren?.find((c) => c !== null && c.type !== "return" && c.type !== "yield") ?? null;
|
|
480
|
+
returns.push({ kind: literalKindOf(value), node: n });
|
|
481
|
+
}
|
|
482
|
+
const children = n.children ?? [];
|
|
483
|
+
for (let i = children.length - 1; i >= 0; i--) if (children[i]) stack.push(children[i]);
|
|
484
|
+
}
|
|
485
|
+
return { nodeTypes, callables, returns };
|
|
486
|
+
}
|
|
487
|
+
function locateByStoredRange(callables, lineStart, lineEnd) {
|
|
488
|
+
let best = null;
|
|
489
|
+
let bestWidth = Number.MAX_SAFE_INTEGER;
|
|
490
|
+
for (const n of callables) {
|
|
491
|
+
const s = n.startPosition.row + 1;
|
|
492
|
+
const e = n.endPosition.row + 1;
|
|
493
|
+
if (s > lineStart || e < lineEnd) continue;
|
|
494
|
+
const width = e - s;
|
|
495
|
+
if (width < bestWidth) {
|
|
496
|
+
bestWidth = width;
|
|
497
|
+
best = n;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return best;
|
|
501
|
+
}
|
|
502
|
+
function storedReturnType(raw) {
|
|
503
|
+
const t = (raw ?? "").trim();
|
|
504
|
+
return t === "" ? null : t;
|
|
505
|
+
}
|
|
506
|
+
function unknownLine(reason, withheld, engine, witness = null) {
|
|
507
|
+
return {
|
|
508
|
+
provenance: "unknown",
|
|
509
|
+
text: UNRESOLVED_TYPE,
|
|
510
|
+
confidence: null,
|
|
511
|
+
reason,
|
|
512
|
+
withheld,
|
|
513
|
+
withheldText: engine === null ? null : engine.name,
|
|
514
|
+
storedProvenanceUnknown: false,
|
|
515
|
+
stale: false,
|
|
516
|
+
witness
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function renderTypeLine(line) {
|
|
520
|
+
const staleMark = line.stale && line.provenance !== "unknown" ? " \u26A0\uFE0F stale" : "";
|
|
521
|
+
if (line.provenance === "inferred") {
|
|
522
|
+
const conf = line.confidence === null ? "n/a" : String(line.confidence);
|
|
523
|
+
return `type: ${line.text} _(inferred \xB7 engine confidence ${conf} \xB7 ${line.reason})_${staleMark}`;
|
|
524
|
+
}
|
|
525
|
+
if (line.provenance === "stored") {
|
|
526
|
+
const origin = line.storedProvenanceUnknown ? "its explicit-vs-inferred origin is not recorded" : "origin recorded";
|
|
527
|
+
return `type: ${line.text} _(stored in the bb index \xB7 ${origin})_${staleMark}`;
|
|
528
|
+
}
|
|
529
|
+
return `type: ${line.text} _(not resolved \u2014 ${line.reason})_`;
|
|
530
|
+
}
|
|
531
|
+
async function resolveTypeLine(filePath, content, symbol, raw) {
|
|
532
|
+
const stored = storedReturnType(raw);
|
|
533
|
+
const language = symbol.language ?? null;
|
|
534
|
+
if (!CALLABLE_SYMBOL_TYPES.has((symbol.symbol_type ?? "").toLowerCase())) {
|
|
535
|
+
return {
|
|
536
|
+
line: unknownLine("not a callable symbol; no return type exists to report", "not-a-function-symbol", null),
|
|
537
|
+
engine: null,
|
|
538
|
+
engineCoverage: { language: language ?? "unknown", covered: false, matchedNodeTypes: [] }
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (symbol.line_start === null || symbol.line_end === null) {
|
|
542
|
+
return {
|
|
543
|
+
line: unknownLine("the index stored no line range for this symbol, so nothing can be witnessed", "symbol-not-locatable-in-tree", null),
|
|
544
|
+
engine: null,
|
|
545
|
+
engineCoverage: { language: language ?? "unknown", covered: false, matchedNodeTypes: [] }
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (stored !== null) {
|
|
549
|
+
return {
|
|
550
|
+
line: {
|
|
551
|
+
provenance: "stored",
|
|
552
|
+
text: stored,
|
|
553
|
+
confidence: null,
|
|
554
|
+
reason: "type as stored in the index; the index records no marker of whether the analyzer read it from an explicit annotation or inferred it, so its own provenance is unstated",
|
|
555
|
+
withheld: null,
|
|
556
|
+
withheldText: null,
|
|
557
|
+
// Declared, measured: the column mixes explicit and inferred values
|
|
558
|
+
// indistinguishably. Reported on the wire, never resolved by guessing.
|
|
559
|
+
storedProvenanceUnknown: true,
|
|
560
|
+
stale: false,
|
|
561
|
+
witness: null
|
|
562
|
+
},
|
|
563
|
+
engine: null,
|
|
564
|
+
engineCoverage: { language: language ?? "unknown", covered: false, matchedNodeTypes: [] }
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return await inferWithWitness(filePath, content, symbol, language, symbol.line_start, symbol.line_end);
|
|
568
|
+
}
|
|
569
|
+
var CALLABLE_SYMBOL_TYPES = /* @__PURE__ */ new Set([
|
|
570
|
+
"function",
|
|
571
|
+
"method",
|
|
572
|
+
"constructor",
|
|
573
|
+
"procedure",
|
|
574
|
+
"subroutine",
|
|
575
|
+
"getter",
|
|
576
|
+
"setter"
|
|
577
|
+
]);
|
|
578
|
+
async function inferWithWitness(filePath, content, symbol, language, symStart, symEnd) {
|
|
579
|
+
let facts = null;
|
|
580
|
+
let engineTypes = null;
|
|
581
|
+
let tree = null;
|
|
582
|
+
if (Buffer.byteLength(content, "utf8") > MAX_WITNESS_BYTES) {
|
|
583
|
+
const kib = (Buffer.byteLength(content, "utf8") / 1024).toFixed(0);
|
|
584
|
+
return {
|
|
585
|
+
line: unknownLine(
|
|
586
|
+
`the file is ${kib} KiB, over the declared ${MAX_WITNESS_BYTES / 1024} KiB ceiling for witnessing (measured: p95 rises past the 50 ms hover budget above that size), so no type was inferred`,
|
|
587
|
+
"file-too-large-for-witness",
|
|
588
|
+
null
|
|
589
|
+
),
|
|
590
|
+
engine: null,
|
|
591
|
+
engineCoverage: { language: language ?? "unknown", covered: false, matchedNodeTypes: [] }
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
const parsed = await ASTEngine.parseTree(filePath, content);
|
|
596
|
+
tree = parsed.tree;
|
|
597
|
+
facts = collectFacts(tree.rootNode);
|
|
598
|
+
const engine = new TypeInference.TypeInferenceEngine();
|
|
599
|
+
engineTypes = engine.inferTypesForFile(tree.rootNode, content, filePath, language ?? "unknown");
|
|
600
|
+
} catch {
|
|
601
|
+
tree?.delete?.();
|
|
602
|
+
return {
|
|
603
|
+
line: unknownLine("the file could not be parsed, so no type was witnessed", "parse-failed", null),
|
|
604
|
+
engine: null,
|
|
605
|
+
engineCoverage: { language: language ?? "unknown", covered: false, matchedNodeTypes: [] }
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
const matched = [...facts.nodeTypes].filter(
|
|
610
|
+
(t) => ENGINE_FUNC_TYPES.includes(t) || ENGINE_DECL_TYPES.includes(t)
|
|
611
|
+
);
|
|
612
|
+
const coverage = { language: language ?? "unknown", covered: matched.length > 0, matchedNodeTypes: matched.sort() };
|
|
613
|
+
const candidate = engineTypes.get(`${symbol.name}:return`);
|
|
614
|
+
const answer = candidate === void 0 ? null : { name: candidate.name, confidence: candidate.confidence, kind: candidate.kind, inferredAtCount: candidate.inferredAt.length };
|
|
615
|
+
if (!coverage.covered) {
|
|
616
|
+
return {
|
|
617
|
+
line: unknownLine(
|
|
618
|
+
`the inference engine has no node-type coverage for ${coverage.language}; it returned nothing rather than a type`,
|
|
619
|
+
"language-not-covered",
|
|
620
|
+
answer
|
|
621
|
+
),
|
|
622
|
+
engine: answer,
|
|
623
|
+
engineCoverage: coverage
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (answer === null) {
|
|
627
|
+
return {
|
|
628
|
+
line: unknownLine("the engine returned no entry for this function; no type is asserted", "no-engine-answer", null),
|
|
629
|
+
engine: null,
|
|
630
|
+
engineCoverage: coverage
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const node = locateByStoredRange(facts.callables, symStart, symEnd);
|
|
634
|
+
if (node === null) {
|
|
635
|
+
return {
|
|
636
|
+
line: unknownLine("no callable node in the tree contains this symbol's stored line range", "symbol-not-locatable-in-tree", answer),
|
|
637
|
+
engine: answer,
|
|
638
|
+
engineCoverage: coverage
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const own = facts.returns.filter((r) => {
|
|
642
|
+
const s = r.node.startPosition.row + 1;
|
|
643
|
+
const e = r.node.endPosition.row + 1;
|
|
644
|
+
return s >= node.startPosition.row + 1 && e <= node.endPosition.row + 1;
|
|
645
|
+
});
|
|
646
|
+
const kinds = [...new Set(own.map((r) => r.kind).filter((k) => k !== null))].sort();
|
|
647
|
+
const witness = {
|
|
648
|
+
nodeType: node.type,
|
|
649
|
+
startLine: node.startPosition.row + 1,
|
|
650
|
+
endLine: node.endPosition.row + 1,
|
|
651
|
+
returnExpressionsSeen: own.length,
|
|
652
|
+
returnKinds: kinds,
|
|
653
|
+
corroborated: false
|
|
654
|
+
};
|
|
655
|
+
const isVoidCandidate = answer.name === "void" || answer.kind === TypeInference.TypeKind.Void;
|
|
656
|
+
const hasValuelessReturn = own.length > 0 && own.every((r) => r.kind === "valueless");
|
|
657
|
+
const annotatesVoid = /(^|[^A-Za-z0-9_])(void|Unit)($|[^A-Za-z0-9_])/.test(stripDeclarationHead(node.text));
|
|
658
|
+
if (isVoidCandidate && !(hasValuelessReturn || annotatesVoid)) {
|
|
659
|
+
return {
|
|
660
|
+
line: unknownLine(
|
|
661
|
+
"the engine answered `void` at high confidence, but the tree shows no valueless return and no explicit void annotation \u2014 that answer means the grammar's return form was not recognized, so it is withheld",
|
|
662
|
+
"engine-reported-void-without-valueless-return",
|
|
663
|
+
answer,
|
|
664
|
+
witness
|
|
665
|
+
),
|
|
666
|
+
engine: answer,
|
|
667
|
+
engineCoverage: coverage
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (isVoidCandidate && (hasValuelessReturn || annotatesVoid)) {
|
|
671
|
+
witness.corroborated = true;
|
|
672
|
+
return {
|
|
673
|
+
line: {
|
|
674
|
+
provenance: "inferred",
|
|
675
|
+
text: answer.name,
|
|
676
|
+
confidence: answer.confidence,
|
|
677
|
+
reason: `witnessed in the tree: ${witness.returnExpressionsSeen} valueless return expression(s) at ${witness.nodeType}`,
|
|
678
|
+
withheld: null,
|
|
679
|
+
withheldText: null,
|
|
680
|
+
storedProvenanceUnknown: false,
|
|
681
|
+
stale: false,
|
|
682
|
+
witness
|
|
683
|
+
},
|
|
684
|
+
engine: answer,
|
|
685
|
+
engineCoverage: coverage
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const wanted = kindForCandidate(answer.name);
|
|
689
|
+
if (wanted === null) {
|
|
690
|
+
return {
|
|
691
|
+
line: unknownLine(
|
|
692
|
+
`the engine proposed \`${answer.name}\`, which is outside the narrow proven subset (a bare primitive literal type only)`,
|
|
693
|
+
"no-witness-in-tree",
|
|
694
|
+
answer,
|
|
695
|
+
witness
|
|
696
|
+
),
|
|
697
|
+
engine: answer,
|
|
698
|
+
engineCoverage: coverage
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (kinds.length !== 1 || kinds[0] !== wanted) {
|
|
702
|
+
return {
|
|
703
|
+
line: unknownLine(
|
|
704
|
+
`the engine proposed \`${answer.name}\` but the tree's return expressions read ${kinds.length === 0 ? "no recognizable literal" : kinds.join(" | ")}`,
|
|
705
|
+
"witness-disagrees-with-engine",
|
|
706
|
+
answer,
|
|
707
|
+
witness
|
|
708
|
+
),
|
|
709
|
+
engine: answer,
|
|
710
|
+
engineCoverage: coverage
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
witness.corroborated = true;
|
|
714
|
+
if (answer.confidence < ENGINE_CONFIDENCE_FLOOR) {
|
|
715
|
+
return {
|
|
716
|
+
line: {
|
|
717
|
+
...unknownLine(
|
|
718
|
+
`witnessed in the tree, but the engine's own confidence ${answer.confidence} is below the declared floor ${ENGINE_CONFIDENCE_FLOOR}`,
|
|
719
|
+
"engine-confidence-below-floor",
|
|
720
|
+
answer,
|
|
721
|
+
witness
|
|
722
|
+
),
|
|
723
|
+
witness
|
|
724
|
+
},
|
|
725
|
+
engine: answer,
|
|
726
|
+
engineCoverage: coverage
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
line: {
|
|
731
|
+
provenance: "inferred",
|
|
732
|
+
text: answer.name,
|
|
733
|
+
confidence: answer.confidence,
|
|
734
|
+
reason: `witnessed in the tree: ${witness.returnExpressionsSeen} return expression(s) of kind ${kinds[0]} at ${witness.nodeType}`,
|
|
735
|
+
withheld: null,
|
|
736
|
+
withheldText: null,
|
|
737
|
+
storedProvenanceUnknown: false,
|
|
738
|
+
stale: false,
|
|
739
|
+
witness
|
|
740
|
+
},
|
|
741
|
+
engine: answer,
|
|
742
|
+
engineCoverage: coverage
|
|
743
|
+
};
|
|
744
|
+
} finally {
|
|
745
|
+
tree?.delete?.();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function stripDeclarationHead(text) {
|
|
749
|
+
const i = text.indexOf("{");
|
|
750
|
+
return i === -1 ? text : text.slice(0, i);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/lsp/rich_hover.ts
|
|
754
|
+
import { readFileSync as readFileSync3, statSync } from "fs";
|
|
755
|
+
|
|
756
|
+
// src/lsp/navigation.ts
|
|
757
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
758
|
+
|
|
759
|
+
// src/lsp/launch_languages.ts
|
|
760
|
+
import path from "path";
|
|
761
|
+
var LAUNCH_LANGUAGES = [
|
|
762
|
+
"java",
|
|
763
|
+
"groovy",
|
|
764
|
+
"kotlin",
|
|
765
|
+
"scala",
|
|
766
|
+
"typescript",
|
|
767
|
+
"javascript",
|
|
768
|
+
"python",
|
|
769
|
+
"zig",
|
|
770
|
+
"go",
|
|
771
|
+
"rust",
|
|
772
|
+
"c",
|
|
773
|
+
"cpp",
|
|
774
|
+
"vue",
|
|
775
|
+
"svelte",
|
|
776
|
+
// M29 wave 3 (owner-approved 2026-11-08): the remaining analyzers. Each
|
|
777
|
+
// language joins the launch set only through the wave protocol; the wave
|
|
778
|
+
// execution gate reports which of them can actually serve hover/definition/
|
|
779
|
+
// references today and which are blocked on an analyzer gap.
|
|
780
|
+
"tsx",
|
|
781
|
+
"html",
|
|
782
|
+
"css",
|
|
783
|
+
"scss",
|
|
784
|
+
"sass",
|
|
785
|
+
"json",
|
|
786
|
+
"yaml",
|
|
787
|
+
"toml",
|
|
788
|
+
"markdown",
|
|
789
|
+
"xml",
|
|
790
|
+
"graphql",
|
|
791
|
+
"protobuf",
|
|
792
|
+
"sql",
|
|
793
|
+
"perl",
|
|
794
|
+
"ruby",
|
|
795
|
+
"php",
|
|
796
|
+
"lua",
|
|
797
|
+
"elixir",
|
|
798
|
+
"erlang",
|
|
799
|
+
"haskell",
|
|
800
|
+
"clojure",
|
|
801
|
+
"julia",
|
|
802
|
+
"r",
|
|
803
|
+
"bash",
|
|
804
|
+
"powershell",
|
|
805
|
+
"make",
|
|
806
|
+
"swift",
|
|
807
|
+
"dart",
|
|
808
|
+
"csharp",
|
|
809
|
+
"fsharp",
|
|
810
|
+
"nim",
|
|
811
|
+
"vb",
|
|
812
|
+
"vba",
|
|
813
|
+
"matlab",
|
|
814
|
+
"clean",
|
|
815
|
+
"solidity",
|
|
816
|
+
"hcl",
|
|
817
|
+
"terraform"
|
|
818
|
+
];
|
|
819
|
+
function isLaunchLanguage(language) {
|
|
820
|
+
return language !== null && LAUNCH_LANGUAGES.includes(language);
|
|
821
|
+
}
|
|
822
|
+
function languageForPath(filePath) {
|
|
823
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
824
|
+
const map = {
|
|
825
|
+
".java": "java",
|
|
826
|
+
".groovy": "groovy",
|
|
827
|
+
".gradle": "groovy",
|
|
828
|
+
".kt": "kotlin",
|
|
829
|
+
".kts": "kotlin",
|
|
830
|
+
".scala": "scala",
|
|
831
|
+
".sc": "scala",
|
|
832
|
+
".ts": "typescript",
|
|
833
|
+
".js": "javascript",
|
|
834
|
+
".jsx": "javascript",
|
|
835
|
+
".mjs": "javascript",
|
|
836
|
+
".cjs": "javascript",
|
|
837
|
+
".py": "python",
|
|
838
|
+
".zig": "zig",
|
|
839
|
+
".go": "go",
|
|
840
|
+
".rs": "rust",
|
|
841
|
+
".c": "c",
|
|
842
|
+
".h": "c",
|
|
843
|
+
".cpp": "cpp",
|
|
844
|
+
".cc": "cpp",
|
|
845
|
+
".cxx": "cpp",
|
|
846
|
+
".hpp": "cpp",
|
|
847
|
+
".vue": "vue",
|
|
848
|
+
".svelte": "svelte",
|
|
849
|
+
// M29 wave 3 — extension-level identity for the onboarded languages.
|
|
850
|
+
// Deliberately ABSENT: ".m" (objc and matlab share it, so it resolves to
|
|
851
|
+
// neither until content sniffing lands — a declared absence, not a guess).
|
|
852
|
+
".tsx": "tsx",
|
|
853
|
+
".html": "html",
|
|
854
|
+
".htm": "html",
|
|
855
|
+
".css": "css",
|
|
856
|
+
".scss": "scss",
|
|
857
|
+
".sass": "sass",
|
|
858
|
+
".json": "json",
|
|
859
|
+
".yaml": "yaml",
|
|
860
|
+
".yml": "yaml",
|
|
861
|
+
".toml": "toml",
|
|
862
|
+
".md": "markdown",
|
|
863
|
+
".markdown": "markdown",
|
|
864
|
+
".mdx": "markdown",
|
|
865
|
+
".xml": "xml",
|
|
866
|
+
".graphql": "graphql",
|
|
867
|
+
".gql": "graphql",
|
|
868
|
+
".proto": "protobuf",
|
|
869
|
+
".sql": "sql",
|
|
870
|
+
".pl": "perl",
|
|
871
|
+
".pm": "perl",
|
|
872
|
+
".rb": "ruby",
|
|
873
|
+
".php": "php",
|
|
874
|
+
".lua": "lua",
|
|
875
|
+
".ex": "elixir",
|
|
876
|
+
".exs": "elixir",
|
|
877
|
+
".erl": "erlang",
|
|
878
|
+
".hrl": "erlang",
|
|
879
|
+
".hs": "haskell",
|
|
880
|
+
".clj": "clojure",
|
|
881
|
+
".cljs": "clojure",
|
|
882
|
+
".cljc": "clojure",
|
|
883
|
+
".jl": "julia",
|
|
884
|
+
".r": "r",
|
|
885
|
+
".sh": "bash",
|
|
886
|
+
".bash": "bash",
|
|
887
|
+
".ps1": "powershell",
|
|
888
|
+
".psm1": "powershell",
|
|
889
|
+
".mk": "make",
|
|
890
|
+
".swift": "swift",
|
|
891
|
+
".dart": "dart",
|
|
892
|
+
".cs": "csharp",
|
|
893
|
+
".fs": "fsharp",
|
|
894
|
+
".fsi": "fsharp",
|
|
895
|
+
".fsx": "fsharp",
|
|
896
|
+
".nim": "nim",
|
|
897
|
+
".vb": "vb",
|
|
898
|
+
".bas": "vba",
|
|
899
|
+
".vba": "vba",
|
|
900
|
+
".icl": "clean",
|
|
901
|
+
".dcl": "clean",
|
|
902
|
+
".sol": "solidity",
|
|
903
|
+
".hcl": "hcl",
|
|
904
|
+
".tf": "terraform",
|
|
905
|
+
".tfvars": "terraform"
|
|
906
|
+
};
|
|
907
|
+
return map[ext] ?? null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/lsp/navigation.ts
|
|
911
|
+
var MAX_LOCATIONS = 200;
|
|
912
|
+
var DECLARATION_ENTRY_NOTE = "this is the symbol's own declaration, served because the client asked for it (not a usage)";
|
|
913
|
+
function languageAbsence(filePath) {
|
|
914
|
+
const language = languageForPath(filePath);
|
|
915
|
+
if (isLaunchLanguage(language)) return null;
|
|
916
|
+
return {
|
|
917
|
+
reason: "language_not_launched",
|
|
918
|
+
language,
|
|
919
|
+
detail: language === null ? `file type not in the launch language set (launch: ${LAUNCH_LANGUAGES.join(", ")})` : `${language} is not a launch language (launch: ${LAUNCH_LANGUAGES.join(", ")})`,
|
|
920
|
+
searchedIndex: false
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function notIndexedAbsence(filePath) {
|
|
924
|
+
return {
|
|
925
|
+
reason: "not_indexed",
|
|
926
|
+
language: languageForPath(filePath),
|
|
927
|
+
detail: "no indexed symbol at this position",
|
|
928
|
+
searchedIndex: true
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function findIndexedSymbol(raw, filePath, line, character) {
|
|
932
|
+
let sourceText;
|
|
933
|
+
try {
|
|
934
|
+
sourceText = readFileSync2(filePath, "utf8");
|
|
935
|
+
} catch {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
const mapper = createPositionMapper(sourceText);
|
|
939
|
+
let indexPos;
|
|
940
|
+
try {
|
|
941
|
+
indexPos = mapper.lspToIndex({ line, character });
|
|
942
|
+
} catch (err) {
|
|
943
|
+
if (err instanceof PositionError) return null;
|
|
944
|
+
throw err;
|
|
945
|
+
}
|
|
946
|
+
const rows = raw.prepare(
|
|
947
|
+
`SELECT s.id, s.name, s.qualified_name, s.symbol_type, s.file_id,
|
|
948
|
+
s.byte_start, s.byte_end, s.line_start, s.line_end,
|
|
949
|
+
f.path, f.language
|
|
950
|
+
FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
951
|
+
WHERE f.path = ? AND s.line_start IS NOT NULL AND s.line_end IS NOT NULL
|
|
952
|
+
AND s.line_start <= ? AND s.line_end >= ?`
|
|
953
|
+
).all(filePath, indexPos.line, indexPos.line);
|
|
954
|
+
let best = null;
|
|
955
|
+
let bestSpan = Number.MAX_SAFE_INTEGER;
|
|
956
|
+
for (const row of rows) {
|
|
957
|
+
if (row.byte_start !== null && row.byte_end !== null && row.line_start === indexPos.line && indexPos.column >= row.byte_start && indexPos.column < row.byte_end) {
|
|
958
|
+
return row;
|
|
959
|
+
}
|
|
960
|
+
const span = (row.line_end ?? 0) - (row.line_start ?? 0);
|
|
961
|
+
if (best === null || span < bestSpan) {
|
|
962
|
+
best = row;
|
|
963
|
+
bestSpan = span;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return best;
|
|
967
|
+
}
|
|
968
|
+
function findNamedSymbolAtPosition(raw, filePath, line, character) {
|
|
969
|
+
let sourceText;
|
|
970
|
+
try {
|
|
971
|
+
sourceText = readFileSync2(filePath, "utf8");
|
|
972
|
+
} catch {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
const mapper = createPositionMapper(sourceText);
|
|
976
|
+
let indexPos;
|
|
977
|
+
try {
|
|
978
|
+
indexPos = mapper.lspToIndex({ line, character });
|
|
979
|
+
} catch (err) {
|
|
980
|
+
if (err instanceof PositionError) return null;
|
|
981
|
+
throw err;
|
|
982
|
+
}
|
|
983
|
+
let jsCol;
|
|
984
|
+
try {
|
|
985
|
+
jsCol = mapper.indexToLsp({ line: indexPos.line, column: indexPos.column }).character;
|
|
986
|
+
} catch (err) {
|
|
987
|
+
if (err instanceof PositionError) return null;
|
|
988
|
+
throw err;
|
|
989
|
+
}
|
|
990
|
+
const lineText = mapper.lineTexts[indexPos.line - 1] ?? "";
|
|
991
|
+
if (jsCol > lineText.length) return null;
|
|
992
|
+
const isWordChar3 = (ch) => /[A-Za-z0-9_$]/.test(ch);
|
|
993
|
+
if (jsCol < lineText.length && !isWordChar3(lineText[jsCol])) {
|
|
994
|
+
if (jsCol === 0 || !isWordChar3(lineText[jsCol - 1])) return null;
|
|
995
|
+
jsCol--;
|
|
996
|
+
}
|
|
997
|
+
let start = jsCol;
|
|
998
|
+
let end = jsCol;
|
|
999
|
+
while (start > 0 && isWordChar3(lineText[start - 1])) start--;
|
|
1000
|
+
while (end < lineText.length && isWordChar3(lineText[end])) end++;
|
|
1001
|
+
const identifier = lineText.slice(start, end);
|
|
1002
|
+
if (!identifier) return null;
|
|
1003
|
+
const sameFile = raw.prepare(
|
|
1004
|
+
`SELECT s.id, s.name, s.qualified_name, s.symbol_type, s.file_id,
|
|
1005
|
+
s.byte_start, s.byte_end, s.line_start, s.line_end,
|
|
1006
|
+
f.path, f.language
|
|
1007
|
+
FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
1008
|
+
WHERE f.path = ? AND s.name = ?`
|
|
1009
|
+
).all(filePath, identifier);
|
|
1010
|
+
if (sameFile.length === 1) return sameFile[0];
|
|
1011
|
+
if (sameFile.length > 1) {
|
|
1012
|
+
const hit = sameFile.find(
|
|
1013
|
+
(r) => r.line_start !== null && r.line_end !== null && r.line_start <= indexPos.line && r.line_end >= indexPos.line
|
|
1014
|
+
);
|
|
1015
|
+
if (hit) return hit;
|
|
1016
|
+
}
|
|
1017
|
+
const rows = raw.prepare(
|
|
1018
|
+
`SELECT s.id, s.name, s.qualified_name, s.symbol_type, s.file_id,
|
|
1019
|
+
s.byte_start, s.byte_end, s.line_start, s.line_end,
|
|
1020
|
+
f.path, f.language
|
|
1021
|
+
FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
1022
|
+
WHERE s.name = ?`
|
|
1023
|
+
).all(identifier);
|
|
1024
|
+
if (rows.length === 1) return rows[0];
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
function resolveDefinition(raw, request) {
|
|
1028
|
+
const languageGate = languageAbsence(request.filePath);
|
|
1029
|
+
if (languageGate !== null) {
|
|
1030
|
+
return {
|
|
1031
|
+
found: false,
|
|
1032
|
+
location: null,
|
|
1033
|
+
symbol: null,
|
|
1034
|
+
evidenceTier: "not_indexed",
|
|
1035
|
+
absence: languageGate
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const named = findNamedSymbolAtPosition(raw, request.filePath, request.line, request.character);
|
|
1039
|
+
const symbol = named ?? findIndexedSymbol(raw, request.filePath, request.line, request.character);
|
|
1040
|
+
if (symbol === null) {
|
|
1041
|
+
return {
|
|
1042
|
+
found: false,
|
|
1043
|
+
location: null,
|
|
1044
|
+
symbol: null,
|
|
1045
|
+
evidenceTier: "not_indexed",
|
|
1046
|
+
absence: notIndexedAbsence(request.filePath)
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
const location = symbolLocation(symbol);
|
|
1050
|
+
if (location === null) {
|
|
1051
|
+
return {
|
|
1052
|
+
found: false,
|
|
1053
|
+
location: null,
|
|
1054
|
+
symbol: {
|
|
1055
|
+
id: symbol.id,
|
|
1056
|
+
name: symbol.name,
|
|
1057
|
+
qualifiedName: symbol.qualified_name,
|
|
1058
|
+
symbolType: symbol.symbol_type
|
|
1059
|
+
},
|
|
1060
|
+
evidenceTier: "indexed",
|
|
1061
|
+
absence: {
|
|
1062
|
+
reason: "unresolvable_location",
|
|
1063
|
+
language: symbol.language ?? languageForPath(request.filePath),
|
|
1064
|
+
detail: `symbol ${symbol.name} is indexed at lines ${symbol.line_start}..${symbol.line_end} but its stored byte columns do not convert against the source on disk`,
|
|
1065
|
+
searchedIndex: true
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
found: true,
|
|
1071
|
+
location,
|
|
1072
|
+
symbol: {
|
|
1073
|
+
id: symbol.id,
|
|
1074
|
+
name: symbol.name,
|
|
1075
|
+
qualifiedName: symbol.qualified_name,
|
|
1076
|
+
symbolType: symbol.symbol_type
|
|
1077
|
+
},
|
|
1078
|
+
evidenceTier: "indexed",
|
|
1079
|
+
absence: null
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function symbolLocation(symbol) {
|
|
1083
|
+
if (symbol.byte_start === null || symbol.byte_end === null || symbol.line_start === null || symbol.line_end === null || symbol.path === null) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
let sourceText;
|
|
1087
|
+
try {
|
|
1088
|
+
sourceText = readFileSync2(symbol.path, "utf8");
|
|
1089
|
+
} catch {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
const startCol = resolveRowByteColumns(sourceText, symbol.line_start, symbol.byte_start);
|
|
1093
|
+
const endCol = resolveRowByteColumns(sourceText, symbol.line_end, symbol.byte_end);
|
|
1094
|
+
if (startCol === null || endCol === null) return null;
|
|
1095
|
+
const mapper = createPositionMapper(sourceText);
|
|
1096
|
+
const range = mapper.indexRangeToLsp({
|
|
1097
|
+
start: { line: startCol.line, column: startCol.column },
|
|
1098
|
+
end: { line: endCol.line, column: endCol.column }
|
|
1099
|
+
});
|
|
1100
|
+
return {
|
|
1101
|
+
path: symbol.path,
|
|
1102
|
+
line: range.start.line,
|
|
1103
|
+
character: range.start.character,
|
|
1104
|
+
endLine: range.end.line,
|
|
1105
|
+
endCharacter: range.end.character
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function edgeLocation(path3, byteStart, byteEnd, lineStart, lineEnd, sourceCache) {
|
|
1109
|
+
if (path3 === null || byteStart === null || byteEnd === null || lineStart === null || lineEnd === null) {
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
return edgeLocationDetailed(path3, byteStart, byteEnd, lineStart, lineEnd, sourceCache).location;
|
|
1113
|
+
}
|
|
1114
|
+
function edgeLocationDetailed(path3, byteStart, byteEnd, lineStart, lineEnd, sourceCache) {
|
|
1115
|
+
if (path3 === null || byteStart === null || byteEnd === null || lineStart === null || lineEnd === null) {
|
|
1116
|
+
return {
|
|
1117
|
+
location: null,
|
|
1118
|
+
absence: {
|
|
1119
|
+
reason: "unresolvable_location",
|
|
1120
|
+
language: null,
|
|
1121
|
+
detail: "stored relationship endpoint carries no complete range (path, line, or byte columns are null)",
|
|
1122
|
+
searchedIndex: true
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
let sourceText;
|
|
1127
|
+
let cachedMapper;
|
|
1128
|
+
let cachedLines;
|
|
1129
|
+
const cached = sourceCache?.get(path3);
|
|
1130
|
+
if (cached !== void 0 && "readError" in cached) {
|
|
1131
|
+
return {
|
|
1132
|
+
location: null,
|
|
1133
|
+
absence: {
|
|
1134
|
+
reason: "unresolvable_location",
|
|
1135
|
+
language: languageForPath(path3),
|
|
1136
|
+
detail: `source file could not be read (${cached.readError}); the stored line is omitted rather than emitted unverified`,
|
|
1137
|
+
searchedIndex: true
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
} else if (cached !== void 0) {
|
|
1141
|
+
sourceText = cached.text;
|
|
1142
|
+
cachedLines = cached.lines;
|
|
1143
|
+
cachedMapper = cached.mapper;
|
|
1144
|
+
} else {
|
|
1145
|
+
try {
|
|
1146
|
+
sourceText = readFileSync2(path3, "utf8");
|
|
1147
|
+
const lines2 = sourceText.split("\n");
|
|
1148
|
+
sourceCache?.set(path3, { text: sourceText, lines: lines2 });
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
sourceCache?.set(path3, { readError: error.message });
|
|
1151
|
+
return {
|
|
1152
|
+
location: null,
|
|
1153
|
+
absence: {
|
|
1154
|
+
reason: "unresolvable_location",
|
|
1155
|
+
language: languageForPath(path3),
|
|
1156
|
+
detail: `source file could not be read (${error.message}); the stored line is omitted rather than emitted unverified`,
|
|
1157
|
+
searchedIndex: true
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
const lines = cachedLines ?? sourceText.split("\n");
|
|
1163
|
+
const startCol = resolveRowByteColumns(sourceText, lineStart, byteStart, lines);
|
|
1164
|
+
const endCol = resolveRowByteColumns(sourceText, lineEnd, byteEnd, lines);
|
|
1165
|
+
if (startCol === null || endCol === null) {
|
|
1166
|
+
return {
|
|
1167
|
+
location: null,
|
|
1168
|
+
absence: {
|
|
1169
|
+
reason: "unresolvable_location",
|
|
1170
|
+
language: languageForPath(path3),
|
|
1171
|
+
detail: `stored byte columns (${byteStart}..${byteEnd} on lines ${lineStart}..${lineEnd}) do not convert against this source text`,
|
|
1172
|
+
searchedIndex: true
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
const mapper = cachedMapper ?? createPositionMapper(sourceText);
|
|
1177
|
+
if (cachedMapper === void 0 && cached !== void 0 && sourceCache !== void 0) {
|
|
1178
|
+
sourceCache.set(path3, { text: sourceText, lines, mapper });
|
|
1179
|
+
}
|
|
1180
|
+
let range;
|
|
1181
|
+
try {
|
|
1182
|
+
range = mapper.indexRangeToLsp({
|
|
1183
|
+
start: { line: startCol.line, column: startCol.column },
|
|
1184
|
+
end: { line: endCol.line, column: endCol.column }
|
|
1185
|
+
});
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
return {
|
|
1188
|
+
location: null,
|
|
1189
|
+
absence: {
|
|
1190
|
+
reason: "unresolvable_location",
|
|
1191
|
+
language: languageForPath(path3),
|
|
1192
|
+
detail: `stored range does not convert to an LSP range (${error.message})`,
|
|
1193
|
+
searchedIndex: true
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
location: {
|
|
1199
|
+
path: path3,
|
|
1200
|
+
line: range.start.line,
|
|
1201
|
+
character: range.start.character,
|
|
1202
|
+
endLine: range.end.line,
|
|
1203
|
+
endCharacter: range.end.character
|
|
1204
|
+
},
|
|
1205
|
+
absence: null
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
function resolveReferences(raw, request) {
|
|
1209
|
+
const limit = Math.max(1, Math.min(request.limit ?? MAX_LOCATIONS, MAX_LOCATIONS));
|
|
1210
|
+
const languageGate = languageAbsence(request.filePath);
|
|
1211
|
+
if (languageGate !== null) {
|
|
1212
|
+
return referencesAbsent(languageGate, limit);
|
|
1213
|
+
}
|
|
1214
|
+
const symbol = findIndexedSymbol(raw, request.filePath, request.line, request.character);
|
|
1215
|
+
if (symbol === null) {
|
|
1216
|
+
return referencesAbsent({ ...notIndexedAbsence(request.filePath), totalCount: 0 }, limit);
|
|
1217
|
+
}
|
|
1218
|
+
const edges = raw.prepare(
|
|
1219
|
+
// M62 §2: the stored call-site line is now SELECTED — the surface has to
|
|
1220
|
+
// reach the call site, and the store's line is the only place a call site
|
|
1221
|
+
// is recorded. It arrives under one alias (`stored_call_line`) and its
|
|
1222
|
+
// ONLY use is the hand-off to the address owner below; nothing in this
|
|
1223
|
+
// module reads it for display. The source-side join is UNCHANGED: it is
|
|
1224
|
+
// still the calling symbol's own indexed range, which is what the
|
|
1225
|
+
// `declaration` substitute uses and what the caller declaration line
|
|
1226
|
+
// comes from.
|
|
1227
|
+
`SELECT r.id, r.source_symbol_id, r.target_symbol_id,
|
|
1228
|
+
r.confidence, r.relationship_type,
|
|
1229
|
+
${STORED_LINE_SELECT},
|
|
1230
|
+
sf.path AS source_path, ss.byte_start AS source_byte_start,
|
|
1231
|
+
ss.byte_end AS source_byte_end, ss.line_start AS source_line_start,
|
|
1232
|
+
ss.line_end AS source_line_end,
|
|
1233
|
+
tf.path AS target_path, ts.byte_start AS target_byte_start,
|
|
1234
|
+
ts.byte_end AS target_byte_end, ts.line_start AS target_line_start,
|
|
1235
|
+
ts.line_end AS target_line_end
|
|
1236
|
+
FROM brain_relationship r
|
|
1237
|
+
LEFT JOIN brain_symbol ss ON ss.id = r.source_symbol_id
|
|
1238
|
+
LEFT JOIN brain_file sf ON sf.id = ss.file_id
|
|
1239
|
+
LEFT JOIN brain_symbol ts ON ts.id = r.target_symbol_id
|
|
1240
|
+
LEFT JOIN brain_file tf ON tf.id = ts.file_id
|
|
1241
|
+
WHERE r.target_symbol_id = ?
|
|
1242
|
+
ORDER BY r.confidence DESC, r.id`
|
|
1243
|
+
).all(symbol.id);
|
|
1244
|
+
const locations = [];
|
|
1245
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1246
|
+
let omittedUnresolvable = 0;
|
|
1247
|
+
let omittedUnpositionable = 0;
|
|
1248
|
+
let coalescedRows = 0;
|
|
1249
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
1250
|
+
for (const edge of edges) {
|
|
1251
|
+
if (!isUsageSiteRow(
|
|
1252
|
+
{
|
|
1253
|
+
source_symbol_id: edge.source_symbol_id,
|
|
1254
|
+
target_symbol_id: edge.target_symbol_id,
|
|
1255
|
+
relationship_type: edge.relationship_type
|
|
1256
|
+
},
|
|
1257
|
+
symbol.id
|
|
1258
|
+
)) {
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const address = deriveCallSiteAddress({
|
|
1262
|
+
storedLine: storedLineFromRow(edge),
|
|
1263
|
+
// The address owner's `file` is documented as the calling symbol's
|
|
1264
|
+
// RELATIVE path, for the renderings the graph/hub/MCP surfaces produce.
|
|
1265
|
+
// This surface hands the editor absolute paths (brain_file.path), and it
|
|
1266
|
+
// holds no project root to relativise against, so the field carries the
|
|
1267
|
+
// same absolute string as `NavigationLocation.path`. DECLARED divergence,
|
|
1268
|
+
// not a silent substitution: a renderer printing this address gets an
|
|
1269
|
+
// absolute path, which is correct here and differs from the graph/hub
|
|
1270
|
+
// renderings. Reported as a gap in the owner's field contract.
|
|
1271
|
+
file: edge.source_path ?? "",
|
|
1272
|
+
// The ONLY permitted substitute, and the caller's own declaration line.
|
|
1273
|
+
callerDeclaration: { line: edge.source_line_start }
|
|
1274
|
+
});
|
|
1275
|
+
const placed = referenceLocation(edge, address, sourceCache);
|
|
1276
|
+
if (placed.location === null) {
|
|
1277
|
+
if (placed.omission === "unpositionable") omittedUnpositionable++;
|
|
1278
|
+
else omittedUnresolvable++;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const loc = placed.location;
|
|
1282
|
+
const key = `${loc.path}:${loc.line}:${loc.character}`;
|
|
1283
|
+
if (seen.has(key)) {
|
|
1284
|
+
coalescedRows++;
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
seen.add(key);
|
|
1288
|
+
if (locations.length < limit) {
|
|
1289
|
+
locations.push(loc);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (request.includeDeclaration) {
|
|
1293
|
+
const decl = symbolLocation(symbol);
|
|
1294
|
+
if (decl !== null && symbol.line_start !== null && symbol.path !== null) {
|
|
1295
|
+
const withAddress = {
|
|
1296
|
+
...decl,
|
|
1297
|
+
callSite: {
|
|
1298
|
+
kind: "declaration",
|
|
1299
|
+
file: symbol.path,
|
|
1300
|
+
line: symbol.line_start,
|
|
1301
|
+
note: DECLARATION_ENTRY_NOTE,
|
|
1302
|
+
rowId: `declaration:${symbol.id}`
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
const key = `${withAddress.path}:${withAddress.line}:declaration:${symbol.id}`;
|
|
1306
|
+
if (!seen.has(key)) {
|
|
1307
|
+
seen.add(key);
|
|
1308
|
+
if (locations.length < limit) {
|
|
1309
|
+
locations.push(withAddress);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const totalCount = seen.size;
|
|
1315
|
+
return {
|
|
1316
|
+
locations,
|
|
1317
|
+
truncated: totalCount > locations.length,
|
|
1318
|
+
totalCount,
|
|
1319
|
+
limit,
|
|
1320
|
+
evidenceTier: "indexed",
|
|
1321
|
+
// Locations WERE served, so there is no absence — but the omission counts
|
|
1322
|
+
// are not absence markers and must not be dropped: a caller showing 3 of 5
|
|
1323
|
+
// rows needs to know the other two were dropped on purpose, and WHICH way.
|
|
1324
|
+
absence: null,
|
|
1325
|
+
omittedUnresolvable,
|
|
1326
|
+
omittedUnpositionable,
|
|
1327
|
+
coalescedRows
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function referenceLocation(edge, address, sourceCache) {
|
|
1331
|
+
if (address.kind === "unknown") return { location: null, omission: "unpositionable" };
|
|
1332
|
+
if (edge.source_path === null) return { location: null, omission: "unresolvable" };
|
|
1333
|
+
if (isExactAddress(address)) {
|
|
1334
|
+
const line0 = addressLine0(address);
|
|
1335
|
+
if (line0 === null) return { location: null, omission: "unpositionable" };
|
|
1336
|
+
const sourceText = readSourceCached(edge.source_path, sourceCache);
|
|
1337
|
+
if (sourceText === null) return { location: null, omission: "unresolvable" };
|
|
1338
|
+
if (line0 >= createPositionMapper(sourceText).lineTexts.length) {
|
|
1339
|
+
return { location: null, omission: "unresolvable" };
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
location: {
|
|
1343
|
+
path: edge.source_path,
|
|
1344
|
+
line: line0,
|
|
1345
|
+
character: 0,
|
|
1346
|
+
endLine: line0,
|
|
1347
|
+
endCharacter: 0,
|
|
1348
|
+
callSite: { ...address, rowId: edge.id }
|
|
1349
|
+
},
|
|
1350
|
+
omission: "none"
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
const resolved = edgeLocationDetailed(
|
|
1354
|
+
edge.source_path,
|
|
1355
|
+
edge.source_byte_start,
|
|
1356
|
+
edge.source_byte_end,
|
|
1357
|
+
edge.source_line_start,
|
|
1358
|
+
edge.source_line_end
|
|
1359
|
+
);
|
|
1360
|
+
if (resolved.location === null) return { location: null, omission: "unresolvable" };
|
|
1361
|
+
return {
|
|
1362
|
+
location: { ...resolved.location, callSite: { ...address, rowId: edge.id } },
|
|
1363
|
+
omission: "none"
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function readSourceCached(path3, cache) {
|
|
1367
|
+
const hit = cache.get(path3);
|
|
1368
|
+
if (hit !== void 0) return hit;
|
|
1369
|
+
let text = null;
|
|
1370
|
+
try {
|
|
1371
|
+
text = readFileSync2(path3, "utf8");
|
|
1372
|
+
} catch {
|
|
1373
|
+
text = null;
|
|
1374
|
+
}
|
|
1375
|
+
cache.set(path3, text);
|
|
1376
|
+
return text;
|
|
1377
|
+
}
|
|
1378
|
+
function referencesAbsent(absence, limit) {
|
|
1379
|
+
return {
|
|
1380
|
+
locations: [],
|
|
1381
|
+
truncated: false,
|
|
1382
|
+
totalCount: 0,
|
|
1383
|
+
limit,
|
|
1384
|
+
evidenceTier: "not_indexed",
|
|
1385
|
+
// The count is restated INSIDE the absence, beside `searchedIndex`. A
|
|
1386
|
+
// client that reads the absence must be able to see "0 matches, and the
|
|
1387
|
+
// index was not searched for this language" as one fact; leaving the count
|
|
1388
|
+
// only on the result invites a reader to treat it as a workspace-wide
|
|
1389
|
+
// claim about a language this server never served.
|
|
1390
|
+
absence: { ...absence, totalCount: 0 },
|
|
1391
|
+
omittedUnresolvable: 0,
|
|
1392
|
+
omittedUnpositionable: 0,
|
|
1393
|
+
coalescedRows: 0
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/lsp/enrichment.ts
|
|
1398
|
+
var ENRICHMENT_KEYS = /* @__PURE__ */ new Set([
|
|
1399
|
+
"role",
|
|
1400
|
+
"pattern",
|
|
1401
|
+
"layerFlow",
|
|
1402
|
+
"criticality",
|
|
1403
|
+
"frequency",
|
|
1404
|
+
"isTestCode",
|
|
1405
|
+
"isGeneratedCode",
|
|
1406
|
+
"isFrameworkContract",
|
|
1407
|
+
"isDependencyInjection",
|
|
1408
|
+
"inferredAt"
|
|
1409
|
+
]);
|
|
1410
|
+
function parseEnrichment(metadata) {
|
|
1411
|
+
if (typeof metadata !== "string" || metadata.length === 0) return null;
|
|
1412
|
+
let parsed;
|
|
1413
|
+
try {
|
|
1414
|
+
parsed = JSON.parse(metadata);
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1419
|
+
if (typeof parsed["inferredAt"] !== "number") return null;
|
|
1420
|
+
const out = { inferredAt: parsed["inferredAt"] };
|
|
1421
|
+
for (const key of ENRICHMENT_KEYS) {
|
|
1422
|
+
if (key === "inferredAt") continue;
|
|
1423
|
+
const v = parsed[key];
|
|
1424
|
+
if (v === void 0 || v === null) continue;
|
|
1425
|
+
if (key === "role" || key === "pattern" || key === "criticality") {
|
|
1426
|
+
if (typeof v === "string") out[key] = v;
|
|
1427
|
+
} else if (key === "layerFlow" || key === "frequency") {
|
|
1428
|
+
if (typeof v === "string") out[key] = v;
|
|
1429
|
+
} else if (typeof v === "boolean") {
|
|
1430
|
+
;
|
|
1431
|
+
out[key] = v;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return out;
|
|
1435
|
+
}
|
|
1436
|
+
function enrichmentIsStale(enrichment, rowTimeUpdated) {
|
|
1437
|
+
if (typeof enrichment.inferredAt !== "number") return false;
|
|
1438
|
+
if (typeof rowTimeUpdated !== "number" || rowTimeUpdated <= 0) return false;
|
|
1439
|
+
return enrichment.inferredAt < rowTimeUpdated;
|
|
1440
|
+
}
|
|
1441
|
+
function describeFrequency(frequency) {
|
|
1442
|
+
const m = /^one_of_(\d+)$/.exec(frequency);
|
|
1443
|
+
if (m) {
|
|
1444
|
+
const n = Number(m[1]);
|
|
1445
|
+
return n === 0 ? "no referring rows" : n === 1 ? "1 referring row" : `${n} referring rows`;
|
|
1446
|
+
}
|
|
1447
|
+
return frequency;
|
|
1448
|
+
}
|
|
1449
|
+
function describeEnrichment(e) {
|
|
1450
|
+
const parts = [];
|
|
1451
|
+
if (e.role) parts.push(`role ${e.role}`);
|
|
1452
|
+
if (e.pattern) parts.push(`pattern ${e.pattern}`);
|
|
1453
|
+
if (e.layerFlow) parts.push(`layer ${e.layerFlow}`);
|
|
1454
|
+
if (e.frequency) parts.push(describeFrequency(e.frequency));
|
|
1455
|
+
if (e.criticality) parts.push(`${e.criticality} criticality`);
|
|
1456
|
+
const flags = [];
|
|
1457
|
+
if (e.isTestCode) flags.push("test code");
|
|
1458
|
+
if (e.isGeneratedCode) flags.push("generated");
|
|
1459
|
+
if (e.isFrameworkContract) flags.push("framework contract");
|
|
1460
|
+
if (e.isDependencyInjection) flags.push("dependency injection");
|
|
1461
|
+
if (flags.length > 0) parts.push(flags.join("+"));
|
|
1462
|
+
return parts.join(" \xB7 ");
|
|
1463
|
+
}
|
|
1464
|
+
function hasDisplayableEnrichment(e) {
|
|
1465
|
+
if (!e) return false;
|
|
1466
|
+
return describeEnrichment(e).length > 0;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// src/lsp/rich_hover.ts
|
|
1470
|
+
var MAX_USAGE_LINKS = 3;
|
|
1471
|
+
var LINE_COUNT_CACHE_MAX = 512;
|
|
1472
|
+
var lineCountCache = /* @__PURE__ */ new Map();
|
|
1473
|
+
function cachedLineCount(path3) {
|
|
1474
|
+
let stat;
|
|
1475
|
+
try {
|
|
1476
|
+
const st = statSync(path3);
|
|
1477
|
+
stat = { mtimeMs: st.mtimeMs, size: st.size };
|
|
1478
|
+
} catch {
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
const entry = lineCountCache.get(path3);
|
|
1482
|
+
if (entry === void 0) return null;
|
|
1483
|
+
if (entry.mtimeMs !== stat.mtimeMs || entry.size !== stat.size) {
|
|
1484
|
+
lineCountCache.delete(path3);
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
lineCountCache.delete(path3);
|
|
1488
|
+
lineCountCache.set(path3, entry);
|
|
1489
|
+
return entry.lineCount;
|
|
1490
|
+
}
|
|
1491
|
+
function rememberLineCount(path3, lineCount) {
|
|
1492
|
+
let stat;
|
|
1493
|
+
try {
|
|
1494
|
+
const st = statSync(path3);
|
|
1495
|
+
stat = { mtimeMs: st.mtimeMs, size: st.size };
|
|
1496
|
+
} catch {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
lineCountCache.delete(path3);
|
|
1500
|
+
lineCountCache.set(path3, { ...stat, lineCount });
|
|
1501
|
+
while (lineCountCache.size > LINE_COUNT_CACHE_MAX) {
|
|
1502
|
+
const oldest = lineCountCache.keys().next();
|
|
1503
|
+
if (oldest.done === true) break;
|
|
1504
|
+
lineCountCache.delete(oldest.value);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
var CANONICAL_RELATIONSHIP_KINDS = [
|
|
1508
|
+
"calls",
|
|
1509
|
+
"extends",
|
|
1510
|
+
"implements",
|
|
1511
|
+
"uses",
|
|
1512
|
+
"instantiates",
|
|
1513
|
+
"overrides",
|
|
1514
|
+
"contains",
|
|
1515
|
+
"references",
|
|
1516
|
+
"imports",
|
|
1517
|
+
"exports",
|
|
1518
|
+
"decorates",
|
|
1519
|
+
"renders",
|
|
1520
|
+
"mixes_in",
|
|
1521
|
+
"part_of"
|
|
1522
|
+
];
|
|
1523
|
+
var RELATIONSHIP_KIND_ALIASES = {
|
|
1524
|
+
call: "calls",
|
|
1525
|
+
inherits: "extends",
|
|
1526
|
+
import: "imports",
|
|
1527
|
+
use: "uses",
|
|
1528
|
+
usage: "uses",
|
|
1529
|
+
reference: "references"
|
|
1530
|
+
};
|
|
1531
|
+
function canonicalKind(relationshipType) {
|
|
1532
|
+
const t = relationshipType.trim().toLowerCase();
|
|
1533
|
+
if (CANONICAL_RELATIONSHIP_KINDS.includes(t)) return t;
|
|
1534
|
+
return RELATIONSHIP_KIND_ALIASES[t] ?? null;
|
|
1535
|
+
}
|
|
1536
|
+
var RELATIONSHIP_VERB = {
|
|
1537
|
+
calls: { outgoing: "calls", incoming: "called by" },
|
|
1538
|
+
extends: { outgoing: "extends", incoming: "extended by" },
|
|
1539
|
+
implements: { outgoing: "implements", incoming: "implemented by" },
|
|
1540
|
+
uses: { outgoing: "uses", incoming: "used by" },
|
|
1541
|
+
instantiates: { outgoing: "instantiates", incoming: "instantiated by" },
|
|
1542
|
+
overrides: { outgoing: "overrides", incoming: "overridden by" },
|
|
1543
|
+
// §14 rule 4 + the owner's decision: `contains` reads as a CONTAINMENT verb,
|
|
1544
|
+
// never as a usage or a connection. The words are chosen so that
|
|
1545
|
+
// "Hub contains greet" and "greet — contained by Hub" are statements about
|
|
1546
|
+
// DECLARATION from either end, and so that no reader can take either for
|
|
1547
|
+
// "someone uses this". The negative test that proves it is in the gate.
|
|
1548
|
+
contains: { outgoing: "contains", incoming: "contained by" },
|
|
1549
|
+
references: { outgoing: "references", incoming: "referenced by" },
|
|
1550
|
+
imports: { outgoing: "imports", incoming: "imported by" },
|
|
1551
|
+
// `exports` is the other declaration containment (relations.ts). Same law.
|
|
1552
|
+
exports: { outgoing: "exports", incoming: "exported by" },
|
|
1553
|
+
// ── the four EXTENDED kinds (2026-10-18 vocabulary extension) ─────────────
|
|
1554
|
+
// `decorates`: the EMITTED direction is decorated declaration -> decorator, so
|
|
1555
|
+
// a card for the decorated declaration reads "carries decorator [X]" and a card
|
|
1556
|
+
// for the decorator reads "decorates [Y]". Not "called by": nothing invokes a
|
|
1557
|
+
// decorator at the source level (src/ast/types.ts says so in as many words).
|
|
1558
|
+
decorates: { outgoing: "carries decorator", incoming: "decorates" },
|
|
1559
|
+
// `renders`: rendering component -> rendered component (JSX element creation;
|
|
1560
|
+
// NOT `instantiates` — nothing is `new`ed in the source).
|
|
1561
|
+
renders: { outgoing: "renders", incoming: "rendered by" },
|
|
1562
|
+
// `mixes_in`: class -> mixin composed into its supertype chain (Dart `with`).
|
|
1563
|
+
// A supertype-composition relation, like `extends`/`implements`.
|
|
1564
|
+
mixes_in: { outgoing: "mixes in", incoming: "mixed in by" },
|
|
1565
|
+
// `part_of`: a `part`/`part of` DIRECTIVE. Its emitted direction is the
|
|
1566
|
+
// directive's own and metadata.partRole names which end is the library, so the
|
|
1567
|
+
// two direction words describe the DIRECTIVE, not a symbol-usage claim: a card
|
|
1568
|
+
// reading "part-of directive" or "named by part-of directive" is true whichever
|
|
1569
|
+
// role this row carries. The card never claims a part USES a library (§13.5:
|
|
1570
|
+
// a module-level edge is a directive, not a symbol usage). Rows whose far end
|
|
1571
|
+
// the index could not resolve as a symbol are declared in DECLARED_ABSENCE.
|
|
1572
|
+
part_of: { outgoing: "part-of directive", incoming: "named by part-of directive" }
|
|
1573
|
+
};
|
|
1574
|
+
function verbFor(relationshipType, direction) {
|
|
1575
|
+
const canonical = canonicalKind(relationshipType);
|
|
1576
|
+
if (canonical === null) return null;
|
|
1577
|
+
const entry = RELATIONSHIP_VERB[canonical];
|
|
1578
|
+
return direction === "incoming" ? entry.incoming : entry.outgoing;
|
|
1579
|
+
}
|
|
1580
|
+
var CONTAINMENT_VERB_DECLARES = "declares";
|
|
1581
|
+
var CONTAINMENT_VERB_DECLARED_IN = "declared in";
|
|
1582
|
+
var USAGE_COMMAND = "bb.openUsage";
|
|
1583
|
+
var DEFINITION_COMMAND = "bb.openDefinition";
|
|
1584
|
+
var SHOW_ALL_USAGES_COMMAND = "bb.showAllUsages";
|
|
1585
|
+
var OPEN_GRAPH_COMMAND = "bb.openGraph";
|
|
1586
|
+
var OPEN_IMPACT_COMMAND = "bb.openImpact";
|
|
1587
|
+
var SHOW_CALL_HIERARCHY_COMMAND = "bb.showCallHierarchy";
|
|
1588
|
+
function encodeLocationArgs(path3, line, character) {
|
|
1589
|
+
return encodeURIComponent(JSON.stringify([{ file: path3, line, character }]));
|
|
1590
|
+
}
|
|
1591
|
+
function openUsageUri(site) {
|
|
1592
|
+
return `command:${USAGE_COMMAND}?${encodeLocationArgs(site.path, site.line, site.character)}`;
|
|
1593
|
+
}
|
|
1594
|
+
function openDefinitionUri(path3, line, character) {
|
|
1595
|
+
return `command:${DEFINITION_COMMAND}?${encodeLocationArgs(path3, line, character)}`;
|
|
1596
|
+
}
|
|
1597
|
+
function encodeActionArgs(symbol, path3, line, character) {
|
|
1598
|
+
return encodeURIComponent(JSON.stringify([{ symbol, file: path3, line, character }]));
|
|
1599
|
+
}
|
|
1600
|
+
function openGraphUri(a) {
|
|
1601
|
+
return `command:${OPEN_GRAPH_COMMAND}?${encodeActionArgs(a.symbol, a.file, a.line, a.character)}`;
|
|
1602
|
+
}
|
|
1603
|
+
function openImpactUri(a) {
|
|
1604
|
+
return `command:${OPEN_IMPACT_COMMAND}?${encodeActionArgs(a.symbol, a.file, a.line, a.character)}`;
|
|
1605
|
+
}
|
|
1606
|
+
function showCallHierarchyUri(a) {
|
|
1607
|
+
return `command:${SHOW_CALL_HIERARCHY_COMMAND}?${encodeActionArgs(a.symbol, a.file, a.line, a.character)}`;
|
|
1608
|
+
}
|
|
1609
|
+
function actionsSection(symbol, filePath, line, character) {
|
|
1610
|
+
if (symbol.length === 0 || filePath.length === 0 || line < 0 || character < 0) return "";
|
|
1611
|
+
const a = { symbol, file: filePath, line, character };
|
|
1612
|
+
return `**actions**: [Open in graph](${openGraphUri(a)}) \xB7 [Impact analysis](${openImpactUri(a)}) \xB7 [Call hierarchy](${showCallHierarchyUri(a)})`;
|
|
1613
|
+
}
|
|
1614
|
+
function showAllUsagesUri(a) {
|
|
1615
|
+
return `command:${SHOW_ALL_USAGES_COMMAND}?${encodeURIComponent(JSON.stringify([{ file: a.file, line: a.line, character: a.character, symbol: a.symbol }]))}`;
|
|
1616
|
+
}
|
|
1617
|
+
function siteLabel(site) {
|
|
1618
|
+
const base = site.path.split(/[\\/]/).pop() ?? site.path;
|
|
1619
|
+
const label = `${site.name} \u2014 ${base}:${site.line + 1}`;
|
|
1620
|
+
return addressNoteSuffix(site.callSite) === null ? label : `${label} ${addressNoteSuffix(site.callSite)}`;
|
|
1621
|
+
}
|
|
1622
|
+
function addressNoteSuffix(address) {
|
|
1623
|
+
if (address === void 0 || address === null) return null;
|
|
1624
|
+
if (isExactAddress(address)) return null;
|
|
1625
|
+
return address.kind === "declaration" ? `_(${ADDRESS_NOTES.substitute})_` : `(${address.kind === "unknown" ? address.note : ""})`;
|
|
1626
|
+
}
|
|
1627
|
+
function richSitesForSymbol(raw, symbolId, omissions) {
|
|
1628
|
+
const locationMemo = /* @__PURE__ */ new Map();
|
|
1629
|
+
const edgeSourceCache = /* @__PURE__ */ new Map();
|
|
1630
|
+
const locationOf = (p, bs, be, ls, le) => {
|
|
1631
|
+
const key = `${p}:${bs}:${be}:${ls}:${le}`;
|
|
1632
|
+
const hit = locationMemo.get(key);
|
|
1633
|
+
if (hit !== void 0 || locationMemo.has(key)) return hit ?? null;
|
|
1634
|
+
let loc;
|
|
1635
|
+
try {
|
|
1636
|
+
loc = edgeLocation(p, bs, be, ls, le, edgeSourceCache);
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
if (!(error instanceof PositionError)) throw error;
|
|
1639
|
+
loc = null;
|
|
1640
|
+
}
|
|
1641
|
+
locationMemo.set(key, loc);
|
|
1642
|
+
return loc;
|
|
1643
|
+
};
|
|
1644
|
+
const lineCountOf = (p) => {
|
|
1645
|
+
const cached = edgeSourceCache.get(p);
|
|
1646
|
+
if (cached !== void 0) {
|
|
1647
|
+
if ("readError" in cached) return null;
|
|
1648
|
+
if (cached.mapper === void 0) cached.mapper = createPositionMapper(cached.text);
|
|
1649
|
+
const count = cached.mapper.lineTexts.length;
|
|
1650
|
+
rememberLineCount(p, count);
|
|
1651
|
+
return count;
|
|
1652
|
+
}
|
|
1653
|
+
const hit = cachedLineCount(p);
|
|
1654
|
+
if (hit !== null) return hit;
|
|
1655
|
+
try {
|
|
1656
|
+
const text = readFileSync3(p, "utf8");
|
|
1657
|
+
const lines = text.split("\n");
|
|
1658
|
+
const mapper = createPositionMapper(text);
|
|
1659
|
+
edgeSourceCache.set(p, { text, lines, mapper });
|
|
1660
|
+
rememberLineCount(p, mapper.lineTexts.length);
|
|
1661
|
+
return mapper.lineTexts.length;
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
edgeSourceCache.set(p, { readError: error.message });
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
const rows = raw.prepare(
|
|
1668
|
+
`SELECT r.id, r.relationship_type, r.confidence, r.metadata, r.time_updated,
|
|
1669
|
+
r.source_symbol_id, r.target_symbol_id, ${STORED_LINE_SELECT},
|
|
1670
|
+
ss.name AS src_name, ss.symbol_type AS src_type,
|
|
1671
|
+
ss.byte_start AS src_bs, ss.byte_end AS src_be,
|
|
1672
|
+
ss.line_start AS src_ls, ss.line_end AS src_le, sf.path AS src_path,
|
|
1673
|
+
ss.line_start AS src_decl, sf.relative_path AS src_rel,
|
|
1674
|
+
ts.name AS tgt_name, ts.symbol_type AS tgt_type,
|
|
1675
|
+
ts.byte_start AS tgt_bs, ts.byte_end AS tgt_be,
|
|
1676
|
+
ts.line_start AS tgt_ls, ts.line_end AS tgt_le, tf.path AS tgt_path,
|
|
1677
|
+
ts.line_start AS tgt_decl, tf.relative_path AS tgt_rel
|
|
1678
|
+
FROM brain_relationship r
|
|
1679
|
+
LEFT JOIN brain_symbol ss ON ss.id = r.source_symbol_id
|
|
1680
|
+
LEFT JOIN brain_file sf ON sf.id = ss.file_id
|
|
1681
|
+
LEFT JOIN brain_symbol ts ON ts.id = r.target_symbol_id
|
|
1682
|
+
LEFT JOIN brain_file tf ON tf.id = ts.file_id
|
|
1683
|
+
WHERE r.source_symbol_id = ? OR r.target_symbol_id = ?
|
|
1684
|
+
ORDER BY r.confidence DESC, r.id`
|
|
1685
|
+
).all(symbolId, symbolId);
|
|
1686
|
+
const sites = [];
|
|
1687
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1688
|
+
for (const row of rows) {
|
|
1689
|
+
if (row.source_symbol_id === symbolId && row.target_symbol_id === symbolId) continue;
|
|
1690
|
+
const incoming = row.target_symbol_id === symbolId && row.source_symbol_id !== symbolId;
|
|
1691
|
+
const other = incoming ? { name: row.src_name, type: row.src_type, path: row.src_path, bs: row.src_bs, be: row.src_be, ls: row.src_ls, le: row.src_le, decl: row.src_decl, rel: row.src_rel } : { name: row.tgt_name, type: row.tgt_type, path: row.tgt_path, bs: row.tgt_bs, be: row.tgt_be, ls: row.tgt_ls, le: row.tgt_le, decl: row.tgt_decl, rel: row.tgt_rel };
|
|
1692
|
+
if (!other.name || !other.path) continue;
|
|
1693
|
+
const sourceRel = incoming ? other.rel : row.src_rel;
|
|
1694
|
+
const sourceDeclLine = incoming ? other.decl : row.src_decl;
|
|
1695
|
+
const address = deriveCallSiteAddress({
|
|
1696
|
+
storedLine: storedLineFromRow(row),
|
|
1697
|
+
file: sourceRel ?? "",
|
|
1698
|
+
callerDeclaration: { line: sourceDeclLine }
|
|
1699
|
+
});
|
|
1700
|
+
const callLine0 = addressLine0(address);
|
|
1701
|
+
const declarationRow = isDeclarationRelationship(row.relationship_type);
|
|
1702
|
+
const servesCallSite = callLine0 !== null && incoming && !declarationRow;
|
|
1703
|
+
let loc;
|
|
1704
|
+
if (servesCallSite) {
|
|
1705
|
+
const lineCount = lineCountOf(other.path);
|
|
1706
|
+
if (lineCount === null || callLine0 >= lineCount) {
|
|
1707
|
+
if (omissions !== void 0) omissions.unresolvable++;
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
loc = { path: other.path, line: callLine0, character: 0, endLine: callLine0, endCharacter: 0 };
|
|
1711
|
+
} else {
|
|
1712
|
+
if (other.bs === null || other.be === null || other.ls === null || other.le === null) continue;
|
|
1713
|
+
const resolved = locationOf(other.path, other.bs, other.be, other.ls, other.le);
|
|
1714
|
+
if (resolved === null) continue;
|
|
1715
|
+
loc = resolved;
|
|
1716
|
+
}
|
|
1717
|
+
const opening = servesCallSite ? { path: loc.path, line: callLine0, character: 0, endLine: callLine0, endCharacter: 0 } : { path: loc.path, line: loc.line, character: loc.character, endLine: loc.endLine, endCharacter: loc.endCharacter };
|
|
1718
|
+
const key = `${opening.path}:${opening.line}:${opening.character}:${incoming ? "in" : "out"}`;
|
|
1719
|
+
const existing = sites.find((x) => `${x.path}:${x.line}:${x.character}:${x.reversed ? "in" : "out"}` === key);
|
|
1720
|
+
if (existing !== void 0) {
|
|
1721
|
+
if (!existing.relationshipTypes.includes(row.relationship_type)) {
|
|
1722
|
+
existing.relationshipTypes.push(row.relationship_type);
|
|
1723
|
+
}
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
seen.add(key);
|
|
1727
|
+
const enrichment = parseEnrichment(row.metadata);
|
|
1728
|
+
sites.push({
|
|
1729
|
+
name: other.name,
|
|
1730
|
+
symbolType: other.type,
|
|
1731
|
+
path: opening.path,
|
|
1732
|
+
line: opening.line,
|
|
1733
|
+
character: opening.character,
|
|
1734
|
+
endLine: opening.endLine,
|
|
1735
|
+
endCharacter: opening.endCharacter,
|
|
1736
|
+
relationshipType: row.relationship_type,
|
|
1737
|
+
relationshipTypes: [row.relationship_type],
|
|
1738
|
+
reversed: incoming,
|
|
1739
|
+
confidence: row.confidence,
|
|
1740
|
+
enrichment,
|
|
1741
|
+
enrichmentStale: enrichment === null ? false : enrichmentIsStale(enrichment, row.time_updated),
|
|
1742
|
+
callSite: address
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
return sites;
|
|
1746
|
+
}
|
|
1747
|
+
function connectedGroups(sites) {
|
|
1748
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1749
|
+
for (const site of sites) {
|
|
1750
|
+
for (const kind of siteKinds(site)) {
|
|
1751
|
+
if (isDeclarationRelationship(kind)) continue;
|
|
1752
|
+
const verb = verbFor(kind, site.reversed ? "incoming" : "outgoing");
|
|
1753
|
+
if (verb === null) continue;
|
|
1754
|
+
let g = groups.get(verb);
|
|
1755
|
+
if (!g) {
|
|
1756
|
+
g = { verb, sites: [] };
|
|
1757
|
+
groups.set(verb, g);
|
|
1758
|
+
}
|
|
1759
|
+
g.sites.push(site);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return [...groups.values()];
|
|
1763
|
+
}
|
|
1764
|
+
function siteKinds(site) {
|
|
1765
|
+
const kinds = site.relationshipTypes;
|
|
1766
|
+
return kinds === void 0 || kinds.length === 0 ? [site.relationshipType] : [...kinds];
|
|
1767
|
+
}
|
|
1768
|
+
var MAX_SUMMARY_PER_VERB = 3;
|
|
1769
|
+
function connectedSummarySection(groups) {
|
|
1770
|
+
if (groups.length === 0) return "";
|
|
1771
|
+
const parts = [];
|
|
1772
|
+
for (const group of groups) {
|
|
1773
|
+
const names = group.sites.slice(0, MAX_SUMMARY_PER_VERB);
|
|
1774
|
+
const overflow = group.sites.length - names.length;
|
|
1775
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1776
|
+
const links = names.map((s) => {
|
|
1777
|
+
const dup = seen.has(s.name);
|
|
1778
|
+
seen.add(s.name);
|
|
1779
|
+
const label = dup ? siteLabel(s) : s.name;
|
|
1780
|
+
return `[${label}](${openUsageUri(s)})`;
|
|
1781
|
+
});
|
|
1782
|
+
if (overflow > 0) links.push(`+${overflow}`);
|
|
1783
|
+
let groupText = `${group.verb} ${links.join(", ")}`;
|
|
1784
|
+
if (group.sites.length === 1 && hasDisplayableEnrichment(group.sites[0].enrichment)) {
|
|
1785
|
+
const s = group.sites[0];
|
|
1786
|
+
const desc = describeEnrichment(s.enrichment);
|
|
1787
|
+
const staleNote = s.enrichmentStale ? ", stale \u2014 row changed after enrichment" : "";
|
|
1788
|
+
groupText += ` \xB7 _${desc} (enriched by index${staleNote})_`;
|
|
1789
|
+
}
|
|
1790
|
+
parts.push(groupText);
|
|
1791
|
+
}
|
|
1792
|
+
return `**connected**: ${parts.join("; ")}`;
|
|
1793
|
+
}
|
|
1794
|
+
function usageSitesOf(sites) {
|
|
1795
|
+
return usageSites(sites);
|
|
1796
|
+
}
|
|
1797
|
+
function usagesSection(sites, total, forSymbol, forFile, forLine, forCharacter) {
|
|
1798
|
+
const usage = usageSitesOf(sites);
|
|
1799
|
+
if (usage.length === 0) return "";
|
|
1800
|
+
const lines = [`**usages** \u2014 ${usage.length} stored reference${usage.length === 1 ? "" : "s"} (${USAGE_SITE_FILTER_LABEL})`];
|
|
1801
|
+
const exact = usage.filter((s) => isExactAddress(s.callSite)).length;
|
|
1802
|
+
lines[0] += exact === usage.length ? " \u2014 each opens the stored call site" : exact === 0 ? ` \u2014 the index stores no call-site line for these, so each opens the referencing declaration` : ` \u2014 ${exact} open the stored call site, ${usage.length - exact} open the referencing declaration (no call-site line stored)`;
|
|
1803
|
+
lines[0] += ` _(${USAGE_COUNT_UNIT_LABEL})_`;
|
|
1804
|
+
const shown = usage.slice(0, MAX_USAGE_LINKS);
|
|
1805
|
+
shown.forEach((s, i) => {
|
|
1806
|
+
lines.push(`${i + 1}. [${siteLabel(s)}](${openUsageUri(s)})`);
|
|
1807
|
+
});
|
|
1808
|
+
const rest = Math.max(0, Math.min(total, usage.length) - shown.length);
|
|
1809
|
+
if (rest > 0) {
|
|
1810
|
+
lines.push(`+${rest} more \u2014 [show all usages](${showAllUsagesUri({ file: forFile, line: forLine, character: forCharacter, symbol: forSymbol })})`);
|
|
1811
|
+
}
|
|
1812
|
+
return lines.join("\n");
|
|
1813
|
+
}
|
|
1814
|
+
var MAX_CONTAINMENT_ROWS = 6;
|
|
1815
|
+
function containmentSitesOf(sites) {
|
|
1816
|
+
return sites.filter((s) => siteKinds(s).some((k) => isDeclarationRelationship(k)));
|
|
1817
|
+
}
|
|
1818
|
+
function containmentSection(sites) {
|
|
1819
|
+
const declared = containmentSitesOf(sites);
|
|
1820
|
+
if (declared.length === 0) return "";
|
|
1821
|
+
const members = declared.filter((s) => !s.reversed);
|
|
1822
|
+
const owners = declared.filter((s) => s.reversed);
|
|
1823
|
+
const parts = [];
|
|
1824
|
+
if (members.length > 0) parts.push(`${CONTAINMENT_VERB_DECLARES} ${linkedList(members)}`);
|
|
1825
|
+
if (owners.length > 0) parts.push(`${CONTAINMENT_VERB_DECLARED_IN} ${linkedList(owners)}`);
|
|
1826
|
+
const multi = declared.map((s) => ({ site: s, kinds: siteKinds(s).filter((k) => isDeclarationRelationship(k)) })).filter((x) => x.kinds.length > 1);
|
|
1827
|
+
for (const x of multi) parts.push(`_(also declared by ${x.kinds.join(", ")})_`);
|
|
1828
|
+
const shown = Math.min(declared.length, MAX_CONTAINMENT_ROWS);
|
|
1829
|
+
const rest = declared.length - shown;
|
|
1830
|
+
const header = `**contained** \u2014 declares ${members.length} member${members.length === 1 ? "" : "s"}, declared in ${owners.length} (declaration containment; never a usage)` + (rest > 0 ? ` \u2014 showing ${shown} of ${declared.length}, +${rest} more on the symbol's own list` : "");
|
|
1831
|
+
return [header, ...parts].join("\n");
|
|
1832
|
+
}
|
|
1833
|
+
function linkedList(sites) {
|
|
1834
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1835
|
+
const links = sites.map((s) => {
|
|
1836
|
+
const dup = seen.has(s.name);
|
|
1837
|
+
seen.add(s.name);
|
|
1838
|
+
const label = dup ? siteLabel(s) : s.name;
|
|
1839
|
+
return `[${label}](${openUsageUri(s)})`;
|
|
1840
|
+
});
|
|
1841
|
+
return links.join(", ");
|
|
1842
|
+
}
|
|
1843
|
+
function definitionLink(path3, line, character) {
|
|
1844
|
+
return `_source: bb index ([${path3}:${line + 1}](${openDefinitionUri(path3, line, character)}))_`;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// src/lsp/hover.ts
|
|
1848
|
+
var SYMBOL_ROW_SELECT = `s.id, s.name, s.qualified_name, s.symbol_type, s.line_start, s.line_end,
|
|
1849
|
+
s.byte_start, s.byte_end, s.return_type, s.parameters, s.visibility, s.file_id,
|
|
1850
|
+
f.id AS f_id, f.path, f.relative_path, f.language`;
|
|
1851
|
+
function findSymbolAtPosition(raw, filePath, position, mapper) {
|
|
1852
|
+
let line1;
|
|
1853
|
+
if (mapper !== void 0) {
|
|
1854
|
+
try {
|
|
1855
|
+
line1 = mapper.lspToIndex(position).line;
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
if (error instanceof PositionError) return null;
|
|
1858
|
+
throw error;
|
|
1859
|
+
}
|
|
1860
|
+
} else {
|
|
1861
|
+
let content;
|
|
1862
|
+
try {
|
|
1863
|
+
content = readFileSync4(filePath, "utf8");
|
|
1864
|
+
} catch {
|
|
1865
|
+
return null;
|
|
1866
|
+
}
|
|
1867
|
+
try {
|
|
1868
|
+
line1 = createPositionMapper(content).lspToIndex(position).line;
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
if (error instanceof PositionError) return null;
|
|
1871
|
+
throw error;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
const rows = raw.prepare(
|
|
1875
|
+
`SELECT ${SYMBOL_ROW_SELECT}
|
|
1876
|
+
FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
1877
|
+
WHERE f.path = ? AND s.line_start IS NOT NULL AND s.line_end IS NOT NULL
|
|
1878
|
+
AND s.line_start <= ? AND s.line_end >= ?`
|
|
1879
|
+
).all(filePath, line1, line1);
|
|
1880
|
+
if (rows.length === 0) return null;
|
|
1881
|
+
let best = null;
|
|
1882
|
+
let bestWidth = Number.MAX_SAFE_INTEGER;
|
|
1883
|
+
for (const row of rows) {
|
|
1884
|
+
const width = (row.line_end ?? 0) - (row.line_start ?? 0);
|
|
1885
|
+
if (width < bestWidth) {
|
|
1886
|
+
bestWidth = width;
|
|
1887
|
+
best = {
|
|
1888
|
+
symbol: row,
|
|
1889
|
+
file: { id: row.f_id, path: row.path, relative_path: row.relative_path ?? "", language: row.language }
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return best;
|
|
1894
|
+
}
|
|
1895
|
+
function indexedFileCountFor(raw, fileId) {
|
|
1896
|
+
if (fileId === null || fileId === void 0 || fileId === "") return null;
|
|
1897
|
+
try {
|
|
1898
|
+
const row = raw.prepare("SELECT count(*) AS c FROM brain_file WHERE project_id = (SELECT project_id FROM brain_file WHERE id = ?)").get(fileId);
|
|
1899
|
+
const n = Number(row?.c);
|
|
1900
|
+
return Number.isFinite(n) ? n : null;
|
|
1901
|
+
} catch {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
async function resolveOccurrenceSymbol(raw, filePath, position) {
|
|
1906
|
+
let content;
|
|
1907
|
+
try {
|
|
1908
|
+
content = readFileSync4(filePath, "utf8");
|
|
1909
|
+
} catch {
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
const mapper = createPositionMapper(content);
|
|
1913
|
+
let indexPos;
|
|
1914
|
+
try {
|
|
1915
|
+
indexPos = mapper.lspToIndex({ line: position.line, character: position.character });
|
|
1916
|
+
} catch (e) {
|
|
1917
|
+
if (e instanceof PositionError) return null;
|
|
1918
|
+
throw e;
|
|
1919
|
+
}
|
|
1920
|
+
let parsed;
|
|
1921
|
+
try {
|
|
1922
|
+
parsed = await parseForCursor(filePath, content);
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
if (error instanceof ParseUnavailable) return null;
|
|
1925
|
+
throw error;
|
|
1926
|
+
}
|
|
1927
|
+
const cursorOcc = occurrenceAtPoint(parsed.tree, indexPos.line, indexPos.column);
|
|
1928
|
+
if (!cursorOcc) return null;
|
|
1929
|
+
const sameFile = raw.prepare(`SELECT ${SYMBOL_ROW_SELECT} FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id WHERE f.path = ? AND s.name = ?`).all(filePath, cursorOcc.text);
|
|
1930
|
+
let rows = sameFile;
|
|
1931
|
+
if (sameFile.length === 0) {
|
|
1932
|
+
const workspace = raw.prepare(`SELECT ${SYMBOL_ROW_SELECT} FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id WHERE s.name = ?`).all(cursorOcc.text);
|
|
1933
|
+
rows = workspace;
|
|
1934
|
+
}
|
|
1935
|
+
if (rows.length !== 1) return null;
|
|
1936
|
+
const row = rows[0];
|
|
1937
|
+
return {
|
|
1938
|
+
symbol: row,
|
|
1939
|
+
file: { id: row.f_id, path: row.path, relative_path: row.relative_path ?? "", language: row.language },
|
|
1940
|
+
occurrence: { startLine: cursorOcc.startLine, startColumn: cursorOcc.startColumn, endColumn: cursorOcc.endColumn },
|
|
1941
|
+
// Lane 6: the mapper for THIS file's text, handed back so the containing-
|
|
1942
|
+
// symbol fallback can convert its line through the same owner without a
|
|
1943
|
+
// second read. Two P1 mappers over the same bytes would be two answers to
|
|
1944
|
+
// the same question, which is the drift §5.1 exists to prevent.
|
|
1945
|
+
mapper,
|
|
1946
|
+
// the declaration's own documentation, read from the comment sites
|
|
1947
|
+
// this parse already produced. Attached to the DECLARATION, not to the
|
|
1948
|
+
// cursor: a doc describes the symbol, and the cursor is often a use of it.
|
|
1949
|
+
// Same rule as doc_comment.ts (a comment ending on the line directly above), read from the point that
|
|
1950
|
+
// can satisfy it instead of a whole-file comment walk — equivalence gated line-by-line.
|
|
1951
|
+
docComment: docCommentAt(parsed.tree, row.line_start)
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function evidenceTiersFor(raw, symbolId) {
|
|
1955
|
+
const rows = raw.prepare(
|
|
1956
|
+
"SELECT relationship_type, confidence, metadata FROM brain_relationship WHERE target_symbol_id = ? UNION SELECT relationship_type, confidence, metadata FROM brain_relationship WHERE source_symbol_id = ?"
|
|
1957
|
+
).all(symbolId, symbolId);
|
|
1958
|
+
return orderTiers(rows.map(classifyStoredRow));
|
|
1959
|
+
}
|
|
1960
|
+
function formatHoverMarkdown(data) {
|
|
1961
|
+
const lines = [];
|
|
1962
|
+
lines.push(`\`\`\`
|
|
1963
|
+
${data.signature}
|
|
1964
|
+
\`\`\``);
|
|
1965
|
+
if (data.docComment.length > 0) {
|
|
1966
|
+
lines.push(data.docComment);
|
|
1967
|
+
lines.push("");
|
|
1968
|
+
}
|
|
1969
|
+
lines.push(renderTypeLine(data.typeLine));
|
|
1970
|
+
lines.push(
|
|
1971
|
+
`**${data.name}** \u2014 ${data.symbolType}, ${data.usageCount} usage${data.usageCount === 1 ? "" : "s"}${data.indexedFileCount === null ? " (indexed scope unknown \u2014 this count is bounded by something not measured)" : ` in ${data.indexedFileCount} indexed file${data.indexedFileCount === 1 ? "" : "s"}`} _(${USAGE_SITE_FILTER_LABEL})_`
|
|
1972
|
+
);
|
|
1973
|
+
if (data.evidenceTiers.length > 0) {
|
|
1974
|
+
lines.push(`evidence: ${data.evidenceTiers.join(", ")}`);
|
|
1975
|
+
} else {
|
|
1976
|
+
lines.push("evidence: none (no indexed relationships)");
|
|
1977
|
+
}
|
|
1978
|
+
if (data.connectedSection && data.connectedSection.length > 0) {
|
|
1979
|
+
lines.push(data.connectedSection);
|
|
1980
|
+
}
|
|
1981
|
+
if (data.usagesSection && data.usagesSection.length > 0) {
|
|
1982
|
+
lines.push(data.usagesSection);
|
|
1983
|
+
}
|
|
1984
|
+
if (data.omittedUsageRows.unresolvable > 0) {
|
|
1985
|
+
const n = data.omittedUsageRows.unresolvable;
|
|
1986
|
+
lines.push(
|
|
1987
|
+
`**usages omitted** \u2014 ${n} stored row${n === 1 ? "" : "s"} name a call-site line past the end of the source file, so ${n === 1 ? "it is" : "they are"} not served (\xA715.3: omitted and counted, never clamped); \`textDocument/references\` omits the same ${n === 1 ? "row" : "rows"}.`
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
if (data.unresolvedNameMatches && data.unresolvedNameMatches.total > 0) {
|
|
1991
|
+
const m = data.unresolvedNameMatches;
|
|
1992
|
+
lines.push(
|
|
1993
|
+
`**name matches without identity** \u2014 ${m.total} stored row${m.total === 1 ? "" : "s"} name \`${m.name}\` but resolve to no indexed declaration (${m.external} classified external, ${m.unresolved} unresolved); these are not counted as usages.`
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
if (data.containmentSection && data.containmentSection.length > 0) {
|
|
1997
|
+
lines.push(data.containmentSection);
|
|
1998
|
+
}
|
|
1999
|
+
if (data.actionsSection && data.actionsSection.length > 0) {
|
|
2000
|
+
lines.push(data.actionsSection);
|
|
2001
|
+
}
|
|
2002
|
+
if (data.crossLanguageSection && data.crossLanguageSection.length > 0) {
|
|
2003
|
+
lines.push(data.crossLanguageSection);
|
|
2004
|
+
}
|
|
2005
|
+
if (data.annotationSection && data.annotationSection.length > 0) {
|
|
2006
|
+
lines.push(data.annotationSection);
|
|
2007
|
+
}
|
|
2008
|
+
if (data.stale) {
|
|
2009
|
+
lines.push(`\u26A0\uFE0F **stale** \u2014 file changed since indexing (${data.freshnessStatus})`);
|
|
2010
|
+
}
|
|
2011
|
+
if (data.definition !== null) {
|
|
2012
|
+
lines.push(definitionLink(data.filePath, data.definition.line, data.definition.character));
|
|
2013
|
+
} else {
|
|
2014
|
+
lines.push(`_source: bb index (${data.filePath}:${data.lineStart ?? "?"})_`);
|
|
2015
|
+
}
|
|
2016
|
+
return lines.join("\n\n");
|
|
2017
|
+
}
|
|
2018
|
+
async function hover(raw, request, deps = {}) {
|
|
2019
|
+
const startedAt = (deps.now ?? Date.now)();
|
|
2020
|
+
const language = languageForPath(request.filePath);
|
|
2021
|
+
if (!isLaunchLanguage(language)) {
|
|
2022
|
+
return {
|
|
2023
|
+
contents: null,
|
|
2024
|
+
range: null,
|
|
2025
|
+
data: null,
|
|
2026
|
+
provenance: {
|
|
2027
|
+
source: "bb_index",
|
|
2028
|
+
evidenceClass: "unknown",
|
|
2029
|
+
engine: "bb_lsp hover@m24.l2",
|
|
2030
|
+
limitations: [
|
|
2031
|
+
language === null ? "file type not in the launch language set" : `${language} is not a launch language (launch: ${LAUNCH_LANGUAGES.join(", ")})`
|
|
2032
|
+
],
|
|
2033
|
+
stale: false
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
const occurrence = await resolveOccurrenceSymbol(raw, request.filePath, request.position);
|
|
2038
|
+
const containing = findSymbolAtPosition(raw, request.filePath, request.position, occurrence?.mapper);
|
|
2039
|
+
const found = occurrence ?? containing;
|
|
2040
|
+
if (!found) {
|
|
2041
|
+
return {
|
|
2042
|
+
contents: null,
|
|
2043
|
+
range: null,
|
|
2044
|
+
data: null,
|
|
2045
|
+
provenance: {
|
|
2046
|
+
source: "bb_index",
|
|
2047
|
+
evidenceClass: "unknown",
|
|
2048
|
+
engine: "bb_lsp hover@m24.l2",
|
|
2049
|
+
limitations: ["no indexed symbol at this position (not indexed)"],
|
|
2050
|
+
stale: false
|
|
2051
|
+
}
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
const { symbol, file } = found;
|
|
2055
|
+
const freshness = checkFileFreshness(raw, file.id);
|
|
2056
|
+
const freshnessStatus = freshness?.status ?? "missing";
|
|
2057
|
+
const stale = freshnessStatus !== "fresh";
|
|
2058
|
+
const tiers = evidenceTiersFor(raw, symbol.id);
|
|
2059
|
+
const omittedUsageRows = { unresolvable: 0 };
|
|
2060
|
+
const sites = richSitesForSymbol(raw, symbol.id, omittedUsageRows);
|
|
2061
|
+
const usageSiteRows = usageSitesOf(sites);
|
|
2062
|
+
const usageCount = usageSiteRows.length;
|
|
2063
|
+
const unresolvedNameMatches = countUnresolvedNameMatches(raw, symbol.id);
|
|
2064
|
+
const indexedFileCount = indexedFileCountFor(raw, file.id);
|
|
2065
|
+
const crossSection = crossLanguageSection(factsFromRows(storedCrossLanguageRows(raw, symbol.id), file.language));
|
|
2066
|
+
const annotationRows = raw.prepare(
|
|
2067
|
+
"SELECT symbol_name, symbol_path, author_class, tool_identity, body, time_created, time_updated FROM brain_annotation WHERE project_id = (SELECT project_id FROM brain_file WHERE id = ?) AND (symbol_name = ? OR symbol_name = ?) AND (symbol_path IS NULL OR symbol_path = ? OR symbol_path = ?) ORDER BY time_updated DESC, id"
|
|
2068
|
+
).all(file.id, symbol.name, symbol.qualified_name, file.relative_path, file.path);
|
|
2069
|
+
const annotationSection = annotationHoverSection(annotationRows);
|
|
2070
|
+
let range = null;
|
|
2071
|
+
if (symbol.line_start !== null && symbol.line_end !== null && symbol.byte_start !== null && symbol.byte_end !== null) {
|
|
2072
|
+
try {
|
|
2073
|
+
const sourceText = readFileSync4(file.path, "utf8");
|
|
2074
|
+
const startCol = resolveRowByteColumns(sourceText, symbol.line_start, symbol.byte_start);
|
|
2075
|
+
const endCol = resolveRowByteColumns(sourceText, symbol.line_end, symbol.byte_end);
|
|
2076
|
+
if (startCol === null || endCol === null) {
|
|
2077
|
+
} else {
|
|
2078
|
+
const mapper = createPositionMapper(sourceText);
|
|
2079
|
+
const indexRange = {
|
|
2080
|
+
start: { line: startCol.line, column: startCol.column },
|
|
2081
|
+
end: { line: endCol.line, column: endCol.column }
|
|
2082
|
+
};
|
|
2083
|
+
range = mapper.indexRangeToLsp(indexRange);
|
|
2084
|
+
}
|
|
2085
|
+
} catch (error) {
|
|
2086
|
+
if (!(error instanceof PositionError)) throw error;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
const jumped = occurrence !== null && (containing === null || containing.symbol.id !== symbol.id);
|
|
2090
|
+
let responseRange = range;
|
|
2091
|
+
if (jumped) {
|
|
2092
|
+
try {
|
|
2093
|
+
const sourceText = readFileSync4(request.filePath, "utf8");
|
|
2094
|
+
const startCol = resolveRowByteColumns(sourceText, occurrence.occurrence.startLine, occurrence.occurrence.startColumn);
|
|
2095
|
+
const endCol = resolveRowByteColumns(sourceText, occurrence.occurrence.startLine, occurrence.occurrence.endColumn);
|
|
2096
|
+
if (startCol !== null && endCol !== null) {
|
|
2097
|
+
const cursorMapper = createPositionMapper(sourceText);
|
|
2098
|
+
responseRange = cursorMapper.indexRangeToLsp({
|
|
2099
|
+
start: { line: startCol.line, column: startCol.column },
|
|
2100
|
+
end: { line: endCol.line, column: endCol.column }
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
if (!(error instanceof PositionError)) throw error;
|
|
2105
|
+
responseRange = null;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
let typeLine;
|
|
2109
|
+
try {
|
|
2110
|
+
const typeSource = readFileSync4(file.path, "utf8");
|
|
2111
|
+
typeLine = (await resolveTypeLine(
|
|
2112
|
+
file.path,
|
|
2113
|
+
typeSource,
|
|
2114
|
+
{
|
|
2115
|
+
name: symbol.name,
|
|
2116
|
+
symbol_type: symbol.symbol_type,
|
|
2117
|
+
return_type: symbol.return_type,
|
|
2118
|
+
line_start: symbol.line_start,
|
|
2119
|
+
line_end: symbol.line_end,
|
|
2120
|
+
language: file.language
|
|
2121
|
+
},
|
|
2122
|
+
symbol.return_type
|
|
2123
|
+
)).line;
|
|
2124
|
+
typeLine.stale = stale;
|
|
2125
|
+
} catch {
|
|
2126
|
+
typeLine = {
|
|
2127
|
+
provenance: "unknown",
|
|
2128
|
+
text: UNRESOLVED_TYPE,
|
|
2129
|
+
confidence: null,
|
|
2130
|
+
reason: "the type could not be resolved; the hover renders the declared marker",
|
|
2131
|
+
withheld: "parse-failed",
|
|
2132
|
+
withheldText: null,
|
|
2133
|
+
storedProvenanceUnknown: false,
|
|
2134
|
+
stale,
|
|
2135
|
+
witness: null
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
const data = {
|
|
2139
|
+
name: symbol.name,
|
|
2140
|
+
qualifiedName: symbol.qualified_name,
|
|
2141
|
+
symbolType: symbol.symbol_type,
|
|
2142
|
+
signature: renderSignature(symbol, file.language),
|
|
2143
|
+
// structured data stays structured (see the field's declaration).
|
|
2144
|
+
parameters: (() => {
|
|
2145
|
+
const parsed = parseParameters(symbol.parameters);
|
|
2146
|
+
if (parsed.shape === "empty") return [];
|
|
2147
|
+
if (parsed.shape !== "list") return null;
|
|
2148
|
+
return parsed.entries.map((e) => ({
|
|
2149
|
+
name: e.name,
|
|
2150
|
+
type: e.type ?? null,
|
|
2151
|
+
isOptional: e.isOptional === true,
|
|
2152
|
+
defaultValue: e.defaultValue ?? null,
|
|
2153
|
+
isRest: e.isRest === true
|
|
2154
|
+
}));
|
|
2155
|
+
})(),
|
|
2156
|
+
usageCount,
|
|
2157
|
+
unresolvedNameMatches,
|
|
2158
|
+
omittedUsageRows,
|
|
2159
|
+
indexedFileCount,
|
|
2160
|
+
evidenceTiers: tiers,
|
|
2161
|
+
// rich sections composed from stored relationship rows.
|
|
2162
|
+
connectedSection: connectedSummarySection(connectedGroups(sites)),
|
|
2163
|
+
usagesSection: usagesSection(usageSiteRows, usageCount, symbol.name, file.path, request.position.line, request.position.character),
|
|
2164
|
+
// The declaration-containment rows come from the SAME collected array the
|
|
2165
|
+
// usage list and the connected summary read (one collection, three
|
|
2166
|
+
// renderings), so a containment count can never disagree with a usage count
|
|
2167
|
+
// about the same rows.
|
|
2168
|
+
containmentSection: containmentSection(sites),
|
|
2169
|
+
// LSP definition location (range.start) when resolvable; null otherwise
|
|
2170
|
+
// (the footnote then renders the plain location — still honest).
|
|
2171
|
+
definition: range === null ? null : { line: range.start.line, character: range.start.character },
|
|
2172
|
+
// the actions row targets the same resolved declaration (the
|
|
2173
|
+
// actions carry the server-resolved symbol identity — ).
|
|
2174
|
+
actionsSection: range === null ? "" : actionsSection(symbol.name, file.path, range.start.line, range.start.character),
|
|
2175
|
+
crossLanguageSection: crossSection,
|
|
2176
|
+
annotationSection,
|
|
2177
|
+
stale,
|
|
2178
|
+
freshnessStatus,
|
|
2179
|
+
filePath: file.path,
|
|
2180
|
+
lineStart: symbol.line_start,
|
|
2181
|
+
lineEnd: symbol.line_end,
|
|
2182
|
+
typeLine,
|
|
2183
|
+
docComment: occurrence?.docComment ?? ""
|
|
2184
|
+
};
|
|
2185
|
+
const elapsed = (deps.now ?? Date.now)() - startedAt;
|
|
2186
|
+
void elapsed;
|
|
2187
|
+
return {
|
|
2188
|
+
contents: { value: formatHoverMarkdown(data), kind: "markdown" },
|
|
2189
|
+
range: responseRange,
|
|
2190
|
+
data,
|
|
2191
|
+
provenance: {
|
|
2192
|
+
source: "bb_index",
|
|
2193
|
+
// the primary is the STRONGEST tier the symbol carries, by the
|
|
2194
|
+
// declared rank — a member of `tiers` by construction, so the primary
|
|
2195
|
+
// can never disagree with the tier list it headlines.
|
|
2196
|
+
evidenceClass: stale ? "stale" : primaryTier(tiers),
|
|
2197
|
+
engine: "bb_lsp hover@m24.l2",
|
|
2198
|
+
limitations: [
|
|
2199
|
+
"index-sourced; no compiler-backed types (complement stance)"
|
|
2200
|
+
],
|
|
2201
|
+
stale
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// src/lsp/document_highlight.ts
|
|
2207
|
+
var SAFETY_CACHE_LIMIT = 8;
|
|
2208
|
+
var safetyCache = /* @__PURE__ */ new Map();
|
|
2209
|
+
function cachedSafety(filePath, content) {
|
|
2210
|
+
const hit = safetyCache.get(filePath);
|
|
2211
|
+
if (!hit || hit.content !== content) return null;
|
|
2212
|
+
safetyCache.delete(filePath);
|
|
2213
|
+
safetyCache.set(filePath, hit);
|
|
2214
|
+
return hit.safety;
|
|
2215
|
+
}
|
|
2216
|
+
function rememberSafety(filePath, content, safety) {
|
|
2217
|
+
safetyCache.delete(filePath);
|
|
2218
|
+
safetyCache.set(filePath, { content, safety });
|
|
2219
|
+
while (safetyCache.size > SAFETY_CACHE_LIMIT) {
|
|
2220
|
+
const oldest = safetyCache.keys().next().value;
|
|
2221
|
+
if (oldest === void 0) break;
|
|
2222
|
+
safetyCache.delete(oldest);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
async function documentHighlight(raw, request) {
|
|
2226
|
+
let content;
|
|
2227
|
+
try {
|
|
2228
|
+
content = readFileSync5(request.filePath, "utf8");
|
|
2229
|
+
} catch {
|
|
2230
|
+
return { highlights: [], evidenceTier: "not_indexed", staleness: null, symbol: null };
|
|
2231
|
+
}
|
|
2232
|
+
const mapper = new PositionMapper(content);
|
|
2233
|
+
const absence = (tier) => ({
|
|
2234
|
+
highlights: [],
|
|
2235
|
+
evidenceTier: tier,
|
|
2236
|
+
staleness: null,
|
|
2237
|
+
symbol: null
|
|
2238
|
+
});
|
|
2239
|
+
let safety = cachedSafety(request.filePath, content);
|
|
2240
|
+
if (safety === null) {
|
|
2241
|
+
try {
|
|
2242
|
+
const parsed = await parseForSafety(request.filePath, content);
|
|
2243
|
+
rememberSafety(request.filePath, content, parsed);
|
|
2244
|
+
safety = parsed;
|
|
2245
|
+
} catch {
|
|
2246
|
+
return absence("unsupported_language");
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
let indexPos;
|
|
2250
|
+
try {
|
|
2251
|
+
indexPos = mapper.lspToIndex({ line: request.line, character: request.character });
|
|
2252
|
+
} catch (e) {
|
|
2253
|
+
if (e instanceof PositionError) return absence("not_indexed");
|
|
2254
|
+
throw e;
|
|
2255
|
+
}
|
|
2256
|
+
const cursorOcc = safety.occurrences.find(
|
|
2257
|
+
(o) => o.startLine === indexPos.line && o.startColumn <= indexPos.column && indexPos.column < o.endColumn
|
|
2258
|
+
);
|
|
2259
|
+
const DECL_COLS = `s.id, s.name, s.line_start, s.line_end, s.byte_start, s.byte_end, s.file_id`;
|
|
2260
|
+
if (!cursorOcc) return absence("not_indexed");
|
|
2261
|
+
const sameFile = raw.prepare(
|
|
2262
|
+
`SELECT ${DECL_COLS} FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
2263
|
+
WHERE f.path = ? AND s.name = ?`
|
|
2264
|
+
).all(request.filePath, cursorOcc.text);
|
|
2265
|
+
let symbol = sameFile.length === 1 ? sameFile[0] : null;
|
|
2266
|
+
if (symbol === null && sameFile.length === 0) {
|
|
2267
|
+
const workspace = raw.prepare(
|
|
2268
|
+
`SELECT ${DECL_COLS} FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
2269
|
+
WHERE s.name = ?`
|
|
2270
|
+
).all(cursorOcc.text);
|
|
2271
|
+
if (workspace.length === 1) symbol = workspace[0];
|
|
2272
|
+
}
|
|
2273
|
+
if (symbol === null) {
|
|
2274
|
+
return absence(sameFile.length > 1 ? "ambiguous" : "not_indexed");
|
|
2275
|
+
}
|
|
2276
|
+
const edges = raw.prepare(`SELECT source_symbol_id, target_symbol_id FROM brain_relationship
|
|
2277
|
+
WHERE target_symbol_id = ? OR source_symbol_id = ?`).all(symbol.id, symbol.id);
|
|
2278
|
+
const related = /* @__PURE__ */ new Set([symbol.id]);
|
|
2279
|
+
for (const e of edges) {
|
|
2280
|
+
if (e.target_symbol_id === symbol.id) related.add(e.source_symbol_id);
|
|
2281
|
+
else related.add(e.target_symbol_id);
|
|
2282
|
+
}
|
|
2283
|
+
const fileSymbols = raw.prepare(`SELECT ${DECL_COLS}
|
|
2284
|
+
FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
2285
|
+
WHERE f.path = ? AND s.line_start IS NOT NULL AND s.line_end IS NOT NULL`).all(request.filePath);
|
|
2286
|
+
const highlights = [];
|
|
2287
|
+
for (const occ of safety.occurrences) {
|
|
2288
|
+
if (occ.text !== symbol.name) continue;
|
|
2289
|
+
const occLine = occ.startLine;
|
|
2290
|
+
let inner = null;
|
|
2291
|
+
let innerSpan = Number.MAX_SAFE_INTEGER;
|
|
2292
|
+
for (const s of fileSymbols) {
|
|
2293
|
+
if (s.line_start === null || s.line_end === null) continue;
|
|
2294
|
+
if (occLine < s.line_start || occLine > s.line_end) continue;
|
|
2295
|
+
const span = s.line_end - s.line_start;
|
|
2296
|
+
if (span < innerSpan) {
|
|
2297
|
+
inner = s;
|
|
2298
|
+
innerSpan = span;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
if (inner === null || !related.has(inner.id)) continue;
|
|
2302
|
+
const start = resolveRowByteColumns(content, occ.startLine, occ.startColumn);
|
|
2303
|
+
const end = resolveRowByteColumns(content, occ.endLine, occ.endColumn);
|
|
2304
|
+
if (start === null || end === null) continue;
|
|
2305
|
+
const range = mapper.indexRangeToLsp({
|
|
2306
|
+
start: { line: start.line, column: start.column },
|
|
2307
|
+
end: { line: end.line, column: end.column }
|
|
2308
|
+
});
|
|
2309
|
+
highlights.push({
|
|
2310
|
+
start: range.start,
|
|
2311
|
+
end: range.end,
|
|
2312
|
+
kind: inner.id === symbol.id ? 3 : 2
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
highlights.sort((a, b) => a.start.line - b.start.line || a.start.character - b.start.character);
|
|
2316
|
+
const tiers = evidenceTiersFor(raw, symbol.id);
|
|
2317
|
+
const freshness = checkFileFreshness(raw, symbol.file_id);
|
|
2318
|
+
const freshnessStatus = freshness?.status ?? "missing";
|
|
2319
|
+
return {
|
|
2320
|
+
highlights,
|
|
2321
|
+
evidenceTier: tiers[0] ?? "unknown",
|
|
2322
|
+
staleness: { stale: freshnessStatus !== "fresh", freshnessStatus },
|
|
2323
|
+
symbol: { id: symbol.id, name: symbol.name }
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// src/lsp/server.ts
|
|
2328
|
+
import { fileURLToPath } from "url";
|
|
2329
|
+
|
|
2330
|
+
// src/lsp/symbols.ts
|
|
2331
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
2332
|
+
var MAX_WORKSPACE_SYMBOLS = 200;
|
|
2333
|
+
function declaredAbsenceFor(filePath) {
|
|
2334
|
+
const language = languageForPath(filePath);
|
|
2335
|
+
if (language === null) {
|
|
2336
|
+
return {
|
|
2337
|
+
code: "file_type_unknown",
|
|
2338
|
+
message: `the file's extension maps to no language in the launch map, so bb declares no symbols for it (launch: ${LAUNCH_LANGUAGES.join(", ")})`,
|
|
2339
|
+
language: null,
|
|
2340
|
+
launchLanguages: LAUNCH_LANGUAGES
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
if (!isLaunchLanguage(language)) {
|
|
2344
|
+
return {
|
|
2345
|
+
code: "language_outside_launch_set",
|
|
2346
|
+
message: `${language} is not a launch language, so bb declares no symbols for it (launch: ${LAUNCH_LANGUAGES.join(", ")})`,
|
|
2347
|
+
language,
|
|
2348
|
+
launchLanguages: LAUNCH_LANGUAGES
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
return null;
|
|
2352
|
+
}
|
|
2353
|
+
function absenceWith(code, message, filePath = null) {
|
|
2354
|
+
return {
|
|
2355
|
+
code,
|
|
2356
|
+
message,
|
|
2357
|
+
language: filePath === null ? null : languageForPath(filePath),
|
|
2358
|
+
launchLanguages: LAUNCH_LANGUAGES
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
function markerFor(raw, rows) {
|
|
2362
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2363
|
+
for (const row of rows) {
|
|
2364
|
+
if (!seen.has(row.file_id)) seen.set(row.file_id, row.path);
|
|
2365
|
+
}
|
|
2366
|
+
let fresh = 0;
|
|
2367
|
+
let stale = 0;
|
|
2368
|
+
let missing = 0;
|
|
2369
|
+
const filesUnverified = [];
|
|
2370
|
+
for (const [fileId, path3] of seen) {
|
|
2371
|
+
const status = checkFileFreshness(raw, fileId);
|
|
2372
|
+
if (status === null) {
|
|
2373
|
+
filesUnverified.push(path3);
|
|
2374
|
+
} else if (status.status === "fresh") {
|
|
2375
|
+
fresh++;
|
|
2376
|
+
} else if (status.status === "stale") {
|
|
2377
|
+
stale++;
|
|
2378
|
+
} else {
|
|
2379
|
+
missing++;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return {
|
|
2383
|
+
stale: stale > 0 || missing > 0 || filesUnverified.length > 0,
|
|
2384
|
+
status: missing > 0 ? "missing" : stale > 0 ? "stale" : filesUnverified.length > 0 ? "unknown" : fresh > 0 ? "fresh" : "unknown",
|
|
2385
|
+
filesChecked: seen.size - filesUnverified.length,
|
|
2386
|
+
filesUnverified
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
var WORD_CHAR = /[A-Za-z0-9_$]/;
|
|
2390
|
+
function isWordChar(ch) {
|
|
2391
|
+
return ch !== void 0 && WORD_CHAR.test(ch);
|
|
2392
|
+
}
|
|
2393
|
+
function identifierSpanAt(mapper, start, name) {
|
|
2394
|
+
if (name.length === 0) return null;
|
|
2395
|
+
const text = mapper.lineTexts[start.line];
|
|
2396
|
+
if (text === void 0) return null;
|
|
2397
|
+
const probed = text.slice(start.character, start.character + name.length);
|
|
2398
|
+
if (probed !== name) return null;
|
|
2399
|
+
if (isWordChar(text[start.character - 1])) return null;
|
|
2400
|
+
if (isWordChar(text[start.character + name.length])) return null;
|
|
2401
|
+
return {
|
|
2402
|
+
line: start.line,
|
|
2403
|
+
character: start.character,
|
|
2404
|
+
endLine: start.line,
|
|
2405
|
+
endCharacter: start.character + name.length
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
function symbolRanges(sourceText, row) {
|
|
2409
|
+
if (row.byte_start === null || row.byte_end === null || row.line_start === null || row.line_end === null) {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
const startCol = resolveRowByteColumns(sourceText, row.line_start, row.byte_start);
|
|
2413
|
+
const endCol = resolveRowByteColumns(sourceText, row.line_end, row.byte_end);
|
|
2414
|
+
if (startCol === null || endCol === null) return null;
|
|
2415
|
+
const mapper = createPositionMapper(sourceText);
|
|
2416
|
+
const selection = mapper.indexRangeToLsp({
|
|
2417
|
+
start: { line: startCol.line, column: startCol.column },
|
|
2418
|
+
end: { line: endCol.line, column: endCol.column }
|
|
2419
|
+
});
|
|
2420
|
+
const range = {
|
|
2421
|
+
line: selection.start.line,
|
|
2422
|
+
character: selection.start.character,
|
|
2423
|
+
endLine: selection.end.line,
|
|
2424
|
+
endCharacter: selection.end.character
|
|
2425
|
+
};
|
|
2426
|
+
const identifier = identifierSpanAt(mapper, selection.start, row.name);
|
|
2427
|
+
return {
|
|
2428
|
+
range,
|
|
2429
|
+
selectionRange: identifier ?? range,
|
|
2430
|
+
selectionRangeBasis: identifier === null ? "declaration_span" : "identifier_at_stored_start"
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
function documentSymbols(raw, filePath) {
|
|
2434
|
+
const scope = declaredAbsenceFor(filePath);
|
|
2435
|
+
if (scope !== null) {
|
|
2436
|
+
return {
|
|
2437
|
+
symbols: [],
|
|
2438
|
+
evidenceTier: "not_indexed",
|
|
2439
|
+
notIndexed: true,
|
|
2440
|
+
absence: scope,
|
|
2441
|
+
omitted: [],
|
|
2442
|
+
staleness: {
|
|
2443
|
+
stale: true,
|
|
2444
|
+
status: "unknown",
|
|
2445
|
+
filesChecked: 0,
|
|
2446
|
+
filesUnverified: [filePath]
|
|
2447
|
+
},
|
|
2448
|
+
selectionRangeBasis: "declaration_span"
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
const rows = raw.prepare(
|
|
2452
|
+
`SELECT s.id, s.name, s.qualified_name, s.symbol_type, s.owner_symbol_id,
|
|
2453
|
+
s.file_id, s.byte_start, s.byte_end, s.line_start, s.line_end,
|
|
2454
|
+
s.gravity_score, f.path, f.language
|
|
2455
|
+
FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
2456
|
+
WHERE f.path = ?
|
|
2457
|
+
ORDER BY s.line_start, s.byte_start, s.id`
|
|
2458
|
+
).all(filePath);
|
|
2459
|
+
const staleness = markerFor(raw, rows);
|
|
2460
|
+
if (rows.length === 0) {
|
|
2461
|
+
return {
|
|
2462
|
+
symbols: [],
|
|
2463
|
+
evidenceTier: "not_indexed",
|
|
2464
|
+
notIndexed: true,
|
|
2465
|
+
absence: absenceWith(
|
|
2466
|
+
"not_in_index",
|
|
2467
|
+
"the index stores no symbol rows for this file (bb has no declaration to show)",
|
|
2468
|
+
filePath
|
|
2469
|
+
),
|
|
2470
|
+
omitted: [],
|
|
2471
|
+
staleness,
|
|
2472
|
+
selectionRangeBasis: "declaration_span"
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
let sourceText = null;
|
|
2476
|
+
try {
|
|
2477
|
+
sourceText = readFileSync6(filePath, "utf8");
|
|
2478
|
+
} catch {
|
|
2479
|
+
return {
|
|
2480
|
+
symbols: [],
|
|
2481
|
+
evidenceTier: "not_indexed",
|
|
2482
|
+
notIndexed: true,
|
|
2483
|
+
absence: absenceWith(
|
|
2484
|
+
"source_unreadable",
|
|
2485
|
+
"the file text could not be read, so no stored span can be converted to an LSP position (no symbol is placed)",
|
|
2486
|
+
filePath
|
|
2487
|
+
),
|
|
2488
|
+
omitted: rows.map((row) => ({
|
|
2489
|
+
id: row.id,
|
|
2490
|
+
name: row.name,
|
|
2491
|
+
reason: "source_unreadable"
|
|
2492
|
+
})),
|
|
2493
|
+
staleness,
|
|
2494
|
+
selectionRangeBasis: "declaration_span"
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
const byId = /* @__PURE__ */ new Map();
|
|
2498
|
+
const roots = [];
|
|
2499
|
+
const omitted = [];
|
|
2500
|
+
let identifierPinned = 0;
|
|
2501
|
+
for (const row of rows) {
|
|
2502
|
+
const spans = symbolRanges(sourceText, row);
|
|
2503
|
+
if (spans === null) {
|
|
2504
|
+
omitted.push({
|
|
2505
|
+
id: row.id,
|
|
2506
|
+
name: row.name,
|
|
2507
|
+
reason: row.byte_start === null || row.byte_end === null || row.line_start === null || row.line_end === null ? "coordinates_missing" : "range_unresolvable_from_source"
|
|
2508
|
+
});
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
if (spans.selectionRangeBasis === "identifier_at_stored_start") identifierPinned++;
|
|
2512
|
+
const entry = {
|
|
2513
|
+
id: row.id,
|
|
2514
|
+
name: row.name,
|
|
2515
|
+
qualifiedName: row.qualified_name,
|
|
2516
|
+
symbolType: row.symbol_type,
|
|
2517
|
+
range: spans.range,
|
|
2518
|
+
selectionRange: spans.selectionRange,
|
|
2519
|
+
selectionRangeBasis: spans.selectionRangeBasis,
|
|
2520
|
+
children: []
|
|
2521
|
+
};
|
|
2522
|
+
byId.set(row.id, entry);
|
|
2523
|
+
const parentId = row.owner_symbol_id;
|
|
2524
|
+
const parent = parentId !== null ? byId.get(parentId) : void 0;
|
|
2525
|
+
if (parent !== void 0 && parent.children !== void 0) {
|
|
2526
|
+
parent.children.push(entry);
|
|
2527
|
+
} else {
|
|
2528
|
+
roots.push(entry);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
return {
|
|
2532
|
+
symbols: roots,
|
|
2533
|
+
evidenceTier: "indexed",
|
|
2534
|
+
notIndexed: false,
|
|
2535
|
+
absence: null,
|
|
2536
|
+
omitted,
|
|
2537
|
+
staleness,
|
|
2538
|
+
// The response-level basis is the WEAKEST basis any entry used: a client
|
|
2539
|
+
// that renders one declaration for the whole tree must not claim an
|
|
2540
|
+
// identifier-precision surface when some entries are declaration spans.
|
|
2541
|
+
selectionRangeBasis: identifierPinned === rows.length - omitted.length ? "identifier_at_stored_start" : "declaration_span"
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
function workspaceSymbols(raw, request) {
|
|
2545
|
+
const limit = Math.max(
|
|
2546
|
+
1,
|
|
2547
|
+
Math.min(request.limit ?? MAX_WORKSPACE_SYMBOLS, MAX_WORKSPACE_SYMBOLS)
|
|
2548
|
+
);
|
|
2549
|
+
const query = request.query.trim();
|
|
2550
|
+
const emptyStaleness = {
|
|
2551
|
+
stale: true,
|
|
2552
|
+
status: "unknown",
|
|
2553
|
+
filesChecked: 0,
|
|
2554
|
+
filesUnverified: []
|
|
2555
|
+
};
|
|
2556
|
+
if (query === "") {
|
|
2557
|
+
return {
|
|
2558
|
+
symbols: [],
|
|
2559
|
+
truncated: false,
|
|
2560
|
+
totalCount: 0,
|
|
2561
|
+
limit,
|
|
2562
|
+
evidenceTier: "indexed",
|
|
2563
|
+
absence: absenceWith(
|
|
2564
|
+
"not_in_index",
|
|
2565
|
+
"the query was empty, so no symbol name was searched for"
|
|
2566
|
+
),
|
|
2567
|
+
omitted: [],
|
|
2568
|
+
staleness: emptyStaleness
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
const columns = "SELECT s.id, s.name, s.qualified_name, s.symbol_type, s.owner_symbol_id, s.file_id, s.byte_start, s.byte_end, s.line_start, s.line_end, s.gravity_score, f.path, f.language FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id ";
|
|
2572
|
+
const order = "ORDER BY s.gravity_score DESC, s.name, s.id LIMIT ?";
|
|
2573
|
+
let rows = raw.prepare(columns + "WHERE s.name = ? " + order).all(query, limit);
|
|
2574
|
+
if (rows.length === 0) {
|
|
2575
|
+
rows = raw.prepare(columns + "WHERE s.name LIKE ? " + order).all(`%${query}%`, limit);
|
|
2576
|
+
}
|
|
2577
|
+
const countBase = "SELECT COUNT(*) AS c FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id WHERE s.name = ?";
|
|
2578
|
+
let totalRow = raw.prepare(countBase).get(query);
|
|
2579
|
+
if ((totalRow?.c ?? 0) === 0) {
|
|
2580
|
+
const fuzzyCount = "SELECT COUNT(*) AS c FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id WHERE s.name LIKE ?";
|
|
2581
|
+
totalRow = raw.prepare(fuzzyCount).get(`%${query}%`);
|
|
2582
|
+
}
|
|
2583
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
2584
|
+
const readSource = (path3) => {
|
|
2585
|
+
const hit = sourceCache.get(path3);
|
|
2586
|
+
if (hit !== void 0) return hit;
|
|
2587
|
+
let text = null;
|
|
2588
|
+
try {
|
|
2589
|
+
text = readFileSync6(path3, "utf8");
|
|
2590
|
+
} catch {
|
|
2591
|
+
text = null;
|
|
2592
|
+
}
|
|
2593
|
+
sourceCache.set(path3, text);
|
|
2594
|
+
return text;
|
|
2595
|
+
};
|
|
2596
|
+
const symbols = [];
|
|
2597
|
+
const omitted = [];
|
|
2598
|
+
for (const row of rows) {
|
|
2599
|
+
if (row.line_start === null) {
|
|
2600
|
+
omitted.push({
|
|
2601
|
+
id: row.id,
|
|
2602
|
+
name: row.name,
|
|
2603
|
+
reason: "coordinates_missing"
|
|
2604
|
+
});
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const text = readSource(row.path);
|
|
2608
|
+
if (text === null) {
|
|
2609
|
+
omitted.push({ id: row.id, name: row.name, reason: "source_unreadable" });
|
|
2610
|
+
continue;
|
|
2611
|
+
}
|
|
2612
|
+
let lspLine;
|
|
2613
|
+
try {
|
|
2614
|
+
lspLine = createPositionMapper(text).indexToLsp({
|
|
2615
|
+
line: row.line_start,
|
|
2616
|
+
column: 1
|
|
2617
|
+
}).line;
|
|
2618
|
+
} catch {
|
|
2619
|
+
omitted.push({
|
|
2620
|
+
id: row.id,
|
|
2621
|
+
name: row.name,
|
|
2622
|
+
reason: "line_outside_file"
|
|
2623
|
+
});
|
|
2624
|
+
continue;
|
|
2625
|
+
}
|
|
2626
|
+
symbols.push({
|
|
2627
|
+
id: row.id,
|
|
2628
|
+
name: row.name,
|
|
2629
|
+
qualifiedName: row.qualified_name,
|
|
2630
|
+
symbolType: row.symbol_type,
|
|
2631
|
+
path: row.path,
|
|
2632
|
+
line: lspLine
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
const staleness = markerFor(raw, rows);
|
|
2636
|
+
const totalCount = totalRow?.c ?? rows.length;
|
|
2637
|
+
return {
|
|
2638
|
+
symbols,
|
|
2639
|
+
truncated: totalCount > symbols.length,
|
|
2640
|
+
totalCount,
|
|
2641
|
+
limit,
|
|
2642
|
+
evidenceTier: symbols.length > 0 ? "indexed" : "not_indexed",
|
|
2643
|
+
absence: symbols.length === 0 ? rows.length === 0 ? absenceWith(
|
|
2644
|
+
"not_in_index",
|
|
2645
|
+
`no indexed symbol is named "${query}" (this is an indexed, served language: the answer is "no such symbol", not "no such language")`
|
|
2646
|
+
) : absenceWith(
|
|
2647
|
+
"coordinates_missing",
|
|
2648
|
+
`every match for "${query}" was omitted because its stored position could not be resolved from the file (${omitted.length} omitted) \u2014 no symbol is placed at an invented line`
|
|
2649
|
+
) : null,
|
|
2650
|
+
omitted,
|
|
2651
|
+
staleness
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/lsp/call_hierarchy.ts
|
|
2656
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2657
|
+
var CH_MAX_DEPTH = 5;
|
|
2658
|
+
var CH_FANOUT_BUDGET = 50;
|
|
2659
|
+
var CALL_HIERARCHY_RELATIONSHIP_TYPES = ["calls"];
|
|
2660
|
+
var KIND_MAP = {
|
|
2661
|
+
class: 5,
|
|
2662
|
+
// Class
|
|
2663
|
+
interface: 11,
|
|
2664
|
+
// Interface
|
|
2665
|
+
enum: 13,
|
|
2666
|
+
// Enum
|
|
2667
|
+
function: 12,
|
|
2668
|
+
// Function
|
|
2669
|
+
method: 6,
|
|
2670
|
+
// Method
|
|
2671
|
+
constructor: 9,
|
|
2672
|
+
// Constructor
|
|
2673
|
+
variable: 13,
|
|
2674
|
+
// Variable (LSP has no variable kind; 13 is Enum — see below)
|
|
2675
|
+
field: 8,
|
|
2676
|
+
// Field
|
|
2677
|
+
property: 10,
|
|
2678
|
+
// Property
|
|
2679
|
+
struct: 23,
|
|
2680
|
+
// Struct
|
|
2681
|
+
trait: 24,
|
|
2682
|
+
// Trait (LSP SymbolKind has no trait; closest is Interface)
|
|
2683
|
+
macro: 25
|
|
2684
|
+
// Macro
|
|
2685
|
+
};
|
|
2686
|
+
KIND_MAP["variable"] = 13;
|
|
2687
|
+
var CALLABLE_SYMBOL_TYPES2 = /* @__PURE__ */ new Set([
|
|
2688
|
+
"function",
|
|
2689
|
+
"method",
|
|
2690
|
+
"constructor",
|
|
2691
|
+
"procedure",
|
|
2692
|
+
"subroutine",
|
|
2693
|
+
"getter",
|
|
2694
|
+
"setter"
|
|
2695
|
+
]);
|
|
2696
|
+
var defaultTraverse = traverseGraph;
|
|
2697
|
+
function symbolKindFor(symbolType) {
|
|
2698
|
+
return KIND_MAP[symbolType] ?? 2;
|
|
2699
|
+
}
|
|
2700
|
+
function lspLineWindow(mapper, line, lineEnd) {
|
|
2701
|
+
try {
|
|
2702
|
+
const start = mapper.indexToLsp({ line, column: 1 }).line;
|
|
2703
|
+
const endLine = Number.isInteger(lineEnd) && lineEnd >= line ? lineEnd : line;
|
|
2704
|
+
const end = mapper.indexToLsp({ line: endLine, column: 1 }).line;
|
|
2705
|
+
return { start, end: Math.max(start, end) };
|
|
2706
|
+
} catch {
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
function unresolvableRange() {
|
|
2711
|
+
return { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
|
|
2712
|
+
}
|
|
2713
|
+
function emptyCrossCheck() {
|
|
2714
|
+
return {
|
|
2715
|
+
edges: 0,
|
|
2716
|
+
agree: 0,
|
|
2717
|
+
disagree: 0,
|
|
2718
|
+
sourceOnly: 0,
|
|
2719
|
+
storeOnly: 0,
|
|
2720
|
+
neither: 0,
|
|
2721
|
+
addressMissing: 0,
|
|
2722
|
+
direct: 0,
|
|
2723
|
+
hop: 0,
|
|
2724
|
+
directnessUnstated: 0,
|
|
2725
|
+
comparisonNotRun: 0
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
function crossCheckEdge(edge, sourceLines0, renderedLine0, edgeTargetName) {
|
|
2729
|
+
const address = edge.callSite ?? null;
|
|
2730
|
+
const sourceLines = [...new Set(sourceLines0)].sort((a, b) => a - b);
|
|
2731
|
+
const storedLine0 = address === null ? null : addressLine0(address);
|
|
2732
|
+
const storeNamesCallSite = address !== null && isExactAddress(address);
|
|
2733
|
+
const depth = typeof edge.depth === "number" ? edge.depth : Number.NaN;
|
|
2734
|
+
const direct = depth === 0;
|
|
2735
|
+
let agreement;
|
|
2736
|
+
if (storeNamesCallSite && storedLine0 !== null && sourceLines.includes(storedLine0)) {
|
|
2737
|
+
agreement = "agree";
|
|
2738
|
+
} else if (storeNamesCallSite && storedLine0 !== null && sourceLines.length > 0) {
|
|
2739
|
+
agreement = "disagree";
|
|
2740
|
+
} else if (storeNamesCallSite && storedLine0 !== null) {
|
|
2741
|
+
agreement = "store-only";
|
|
2742
|
+
} else if (sourceLines.length > 0) {
|
|
2743
|
+
agreement = "source-only";
|
|
2744
|
+
} else {
|
|
2745
|
+
agreement = "neither";
|
|
2746
|
+
}
|
|
2747
|
+
return { agreement, sourceLines0: sourceLines, address, renderedLine0, edgeTargetName, direct, depth };
|
|
2748
|
+
}
|
|
2749
|
+
function tallyCrossCheck(census, agreement, addressPresent) {
|
|
2750
|
+
census.edges++;
|
|
2751
|
+
if (!addressPresent) census.addressMissing++;
|
|
2752
|
+
const depth = agreement.depth;
|
|
2753
|
+
if (depth === 0) census.direct++;
|
|
2754
|
+
else if (Number.isInteger(depth) && depth >= 1) census.hop++;
|
|
2755
|
+
else census.directnessUnstated++;
|
|
2756
|
+
switch (agreement.agreement) {
|
|
2757
|
+
case "agree":
|
|
2758
|
+
census.agree++;
|
|
2759
|
+
break;
|
|
2760
|
+
case "disagree":
|
|
2761
|
+
census.disagree++;
|
|
2762
|
+
break;
|
|
2763
|
+
case "source-only":
|
|
2764
|
+
census.sourceOnly++;
|
|
2765
|
+
break;
|
|
2766
|
+
case "store-only":
|
|
2767
|
+
census.storeOnly++;
|
|
2768
|
+
break;
|
|
2769
|
+
case "neither":
|
|
2770
|
+
census.neither++;
|
|
2771
|
+
break;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
function declarationTokenOf(sourceText, mapper, name, declLine1) {
|
|
2775
|
+
const window = lspLineWindow(mapper, declLine1, declLine1);
|
|
2776
|
+
if (window === null) return void 0;
|
|
2777
|
+
const token = nameTokenRange(sourceText, name, { start: window.start, end: window.start });
|
|
2778
|
+
return token === null ? void 0 : token.start;
|
|
2779
|
+
}
|
|
2780
|
+
function unknownStaleness(paths = []) {
|
|
2781
|
+
return { stale: true, status: "unknown", filesChecked: 0, filesUnverified: paths };
|
|
2782
|
+
}
|
|
2783
|
+
function symbolStaleness(raw, fileId, filePath) {
|
|
2784
|
+
const status = checkFileFreshness(raw, fileId);
|
|
2785
|
+
if (status === null) return unknownStaleness([filePath]);
|
|
2786
|
+
return {
|
|
2787
|
+
stale: status.status !== "fresh",
|
|
2788
|
+
status: status.status,
|
|
2789
|
+
filesChecked: 1,
|
|
2790
|
+
filesUnverified: []
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
function expandStaleness(raw, rootFileId, rootFilePath, edgePaths) {
|
|
2794
|
+
const distinct = [...new Set(edgePaths)].filter((p) => p !== "" && p !== rootFilePath);
|
|
2795
|
+
const idByPath = /* @__PURE__ */ new Map();
|
|
2796
|
+
if (distinct.length > 0) {
|
|
2797
|
+
const placeholders = distinct.map(() => "?").join(", ");
|
|
2798
|
+
let rows = [];
|
|
2799
|
+
try {
|
|
2800
|
+
rows = raw.prepare(`SELECT id, path FROM brain_file WHERE path IN (${placeholders})`).all(...distinct);
|
|
2801
|
+
} catch {
|
|
2802
|
+
rows = [];
|
|
2803
|
+
}
|
|
2804
|
+
for (const row of rows) idByPath.set(row.path, row.id);
|
|
2805
|
+
}
|
|
2806
|
+
let fresh = 0;
|
|
2807
|
+
let stale = 0;
|
|
2808
|
+
let missing = 0;
|
|
2809
|
+
const unverified = [];
|
|
2810
|
+
const account = (fileId, filePath) => {
|
|
2811
|
+
if (fileId === null) {
|
|
2812
|
+
unverified.push(filePath);
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
const status = checkFileFreshness(raw, fileId);
|
|
2816
|
+
if (status === null) unverified.push(filePath);
|
|
2817
|
+
else if (status.status === "fresh") fresh++;
|
|
2818
|
+
else if (status.status === "stale") stale++;
|
|
2819
|
+
else missing++;
|
|
2820
|
+
};
|
|
2821
|
+
account(rootFileId, rootFilePath);
|
|
2822
|
+
for (const path3 of distinct) account(idByPath.get(path3) ?? null, path3);
|
|
2823
|
+
return {
|
|
2824
|
+
stale: stale > 0 || missing > 0 || unverified.length > 0,
|
|
2825
|
+
status: missing > 0 ? "missing" : stale > 0 ? "stale" : unverified.length > 0 ? "unknown" : fresh > 0 ? "fresh" : "unknown",
|
|
2826
|
+
filesChecked: 1 + distinct.length - unverified.length,
|
|
2827
|
+
filesUnverified: unverified
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
var WORD_CHAR2 = /[A-Za-z0-9_$]/;
|
|
2831
|
+
function isWordChar2(ch) {
|
|
2832
|
+
return ch !== void 0 && WORD_CHAR2.test(ch);
|
|
2833
|
+
}
|
|
2834
|
+
function codeBeforeComment(line) {
|
|
2835
|
+
const i = line.indexOf("//");
|
|
2836
|
+
return i === -1 ? line : line.slice(0, i);
|
|
2837
|
+
}
|
|
2838
|
+
function receiverBefore(line, at) {
|
|
2839
|
+
let i = at - 1;
|
|
2840
|
+
while (i >= 0 && /\s/.test(line[i])) i--;
|
|
2841
|
+
if (line[i] !== ".") return "";
|
|
2842
|
+
let j = i - 1;
|
|
2843
|
+
while (j >= 0 && /\s/.test(line[j])) j--;
|
|
2844
|
+
const end = j + 1;
|
|
2845
|
+
while (j >= 0 && (isWordChar2(line[j]) || line[j] === ".")) j--;
|
|
2846
|
+
return line.slice(j + 1, end).replace(/\.+$/, "");
|
|
2847
|
+
}
|
|
2848
|
+
function classify(receiver, isConstruct, ownNames) {
|
|
2849
|
+
if (isConstruct) return "construct";
|
|
2850
|
+
if (receiver === "") return "plain";
|
|
2851
|
+
const segments = receiver.split(".").filter((s) => s !== "");
|
|
2852
|
+
const last = segments[segments.length - 1] ?? "";
|
|
2853
|
+
if (last === "this") return "this";
|
|
2854
|
+
if (ownNames.has(last)) return "this";
|
|
2855
|
+
return "receiver";
|
|
2856
|
+
}
|
|
2857
|
+
function resolveInvocations(sourceText, targetName, callerLines0, skip, ownNames = /* @__PURE__ */ new Set(), skipDeclarations = []) {
|
|
2858
|
+
if (targetName.length === 0) return [];
|
|
2859
|
+
const mapper = createPositionMapper(sourceText);
|
|
2860
|
+
const out = [];
|
|
2861
|
+
const last = Math.min(callerLines0.end, mapper.lineTexts.length - 1);
|
|
2862
|
+
const excluded = skip === void 0 ? skipDeclarations : [skip, ...skipDeclarations];
|
|
2863
|
+
for (let line0 = Math.max(0, callerLines0.start); line0 <= last; line0++) {
|
|
2864
|
+
const line = codeBeforeComment(mapper.lineTexts[line0] ?? "");
|
|
2865
|
+
let from = 0;
|
|
2866
|
+
for (; ; ) {
|
|
2867
|
+
const at = line.indexOf(targetName, from);
|
|
2868
|
+
if (at === -1) break;
|
|
2869
|
+
from = at + targetName.length;
|
|
2870
|
+
if (isWordChar2(line[at - 1]) || isWordChar2(line[at + targetName.length])) continue;
|
|
2871
|
+
if (excluded.some((p) => p.line === line0 && p.character === at)) continue;
|
|
2872
|
+
let k = at + targetName.length;
|
|
2873
|
+
while (k < line.length && /\s/.test(line[k])) k++;
|
|
2874
|
+
const isCall = line[k] === "(";
|
|
2875
|
+
const isConstruct = /(^|[^\w$])new\s+$/.test(line.slice(0, at));
|
|
2876
|
+
if (!isCall && !isConstruct) continue;
|
|
2877
|
+
const receiver = receiverBefore(line, at);
|
|
2878
|
+
let range;
|
|
2879
|
+
try {
|
|
2880
|
+
const start = mapper.lspToIndex({ line: line0, character: at });
|
|
2881
|
+
const end = mapper.lspToIndex({ line: line0, character: at + targetName.length });
|
|
2882
|
+
range = { start: mapper.indexToLsp(start), end: mapper.indexToLsp(end) };
|
|
2883
|
+
} catch {
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
out.push({
|
|
2887
|
+
range,
|
|
2888
|
+
kind: classify(receiver, isConstruct, ownNames),
|
|
2889
|
+
receiver
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
return out;
|
|
2894
|
+
}
|
|
2895
|
+
function nameTokenRange(sourceText, name, lines0) {
|
|
2896
|
+
if (name.length === 0) return null;
|
|
2897
|
+
const mapper = createPositionMapper(sourceText);
|
|
2898
|
+
const last = Math.min(lines0.end, mapper.lineTexts.length - 1);
|
|
2899
|
+
for (let line0 = Math.max(0, lines0.start); line0 <= last; line0++) {
|
|
2900
|
+
const line = mapper.lineTexts[line0] ?? "";
|
|
2901
|
+
let from = 0;
|
|
2902
|
+
for (; ; ) {
|
|
2903
|
+
const at = line.indexOf(name, from);
|
|
2904
|
+
if (at === -1) break;
|
|
2905
|
+
from = at + name.length;
|
|
2906
|
+
if (isWordChar2(line[at - 1]) || isWordChar2(line[at + name.length])) continue;
|
|
2907
|
+
try {
|
|
2908
|
+
const start = mapper.lspToIndex({ line: line0, character: at });
|
|
2909
|
+
const end = mapper.lspToIndex({ line: line0, character: at + name.length });
|
|
2910
|
+
return { start: mapper.indexToLsp(start), end: mapper.indexToLsp(end) };
|
|
2911
|
+
} catch {
|
|
2912
|
+
return null;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
return null;
|
|
2917
|
+
}
|
|
2918
|
+
function lineIndent(sourceText, line0) {
|
|
2919
|
+
const line = createPositionMapper(sourceText).lineTexts[line0] ?? "";
|
|
2920
|
+
const m = /^(\s*)/.exec(line);
|
|
2921
|
+
return (m?.[1] ?? "").length;
|
|
2922
|
+
}
|
|
2923
|
+
function lineLength(sourceText, line0) {
|
|
2924
|
+
return (createPositionMapper(sourceText).lineTexts[line0] ?? "").length;
|
|
2925
|
+
}
|
|
2926
|
+
function makeSourceReader() {
|
|
2927
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2928
|
+
return (filePath) => {
|
|
2929
|
+
if (filePath === "") return null;
|
|
2930
|
+
const hit = cache.get(filePath);
|
|
2931
|
+
if (hit !== void 0) return hit;
|
|
2932
|
+
let text = null;
|
|
2933
|
+
try {
|
|
2934
|
+
text = readFileSync7(filePath, "utf8");
|
|
2935
|
+
} catch {
|
|
2936
|
+
text = null;
|
|
2937
|
+
}
|
|
2938
|
+
cache.set(filePath, text);
|
|
2939
|
+
return text;
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
function prepareCallHierarchy(raw, request) {
|
|
2943
|
+
const scope = declaredAbsenceFor(request.filePath);
|
|
2944
|
+
if (scope !== null) {
|
|
2945
|
+
return { item: null, notIndexed: true, absence: scope, staleness: unknownStaleness([request.filePath]) };
|
|
2946
|
+
}
|
|
2947
|
+
const symbol = findIndexedSymbol(raw, request.filePath, request.line, request.character);
|
|
2948
|
+
if (symbol === null) {
|
|
2949
|
+
return {
|
|
2950
|
+
item: null,
|
|
2951
|
+
notIndexed: true,
|
|
2952
|
+
absence: absenceWith(
|
|
2953
|
+
"symbol_not_at_position",
|
|
2954
|
+
"the index stores no symbol covering this position (this is a served language: the answer is 'no symbol here', not 'no call hierarchy')",
|
|
2955
|
+
request.filePath
|
|
2956
|
+
),
|
|
2957
|
+
staleness: unknownStaleness([request.filePath])
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
const staleness = symbolStaleness(raw, symbol.file_id, symbol.path);
|
|
2961
|
+
if (!CALLABLE_SYMBOL_TYPES2.has((symbol.symbol_type ?? "").toLowerCase())) {
|
|
2962
|
+
return {
|
|
2963
|
+
item: null,
|
|
2964
|
+
notIndexed: true,
|
|
2965
|
+
absence: absenceWith(
|
|
2966
|
+
"no_stored_calls",
|
|
2967
|
+
`symbol kind '${symbol.symbol_type}' is not callable \u2014 call hierarchy does not apply (this is NOT 'no callers': the surface has no callable identity for this kind)`,
|
|
2968
|
+
request.filePath
|
|
2969
|
+
),
|
|
2970
|
+
staleness
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
let sourceText;
|
|
2974
|
+
try {
|
|
2975
|
+
sourceText = readFileSync7(symbol.path, "utf8");
|
|
2976
|
+
} catch {
|
|
2977
|
+
return {
|
|
2978
|
+
item: null,
|
|
2979
|
+
notIndexed: true,
|
|
2980
|
+
absence: absenceWith(
|
|
2981
|
+
"source_unreadable",
|
|
2982
|
+
"the file text could not be read, so no stored span can be converted to an LSP position (no item is placed)",
|
|
2983
|
+
symbol.path
|
|
2984
|
+
),
|
|
2985
|
+
staleness
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
if (symbol.byte_start === null || symbol.byte_end === null || symbol.line_start === null || symbol.line_end === null) {
|
|
2989
|
+
return {
|
|
2990
|
+
item: null,
|
|
2991
|
+
notIndexed: true,
|
|
2992
|
+
absence: absenceWith(
|
|
2993
|
+
"coordinates_missing",
|
|
2994
|
+
"the indexed symbol stored no byte/line coordinates, so no position can be derived from it (no item is placed)",
|
|
2995
|
+
symbol.path
|
|
2996
|
+
),
|
|
2997
|
+
staleness
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
const startCol = resolveRowByteColumns(sourceText, symbol.line_start, symbol.byte_start);
|
|
3001
|
+
const endCol = resolveRowByteColumns(sourceText, symbol.line_end, symbol.byte_end);
|
|
3002
|
+
if (startCol === null || endCol === null) {
|
|
3003
|
+
return {
|
|
3004
|
+
item: null,
|
|
3005
|
+
notIndexed: true,
|
|
3006
|
+
absence: absenceWith(
|
|
3007
|
+
"coordinates_missing",
|
|
3008
|
+
"the indexed span could not be resolved against the file's own byte columns (no item is placed)",
|
|
3009
|
+
symbol.path
|
|
3010
|
+
),
|
|
3011
|
+
staleness
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
const mapper = createPositionMapper(sourceText);
|
|
3015
|
+
const range = mapper.indexRangeToLsp({
|
|
3016
|
+
start: { line: startCol.line, column: startCol.column },
|
|
3017
|
+
end: { line: endCol.line, column: endCol.column }
|
|
3018
|
+
});
|
|
3019
|
+
const lspRange = {
|
|
3020
|
+
start: { line: range.start.line, character: range.start.character },
|
|
3021
|
+
end: { line: range.end.line, character: range.end.character }
|
|
3022
|
+
};
|
|
3023
|
+
const nameRange = nameTokenRange(
|
|
3024
|
+
sourceText,
|
|
3025
|
+
symbol.name,
|
|
3026
|
+
{ start: lspRange.start.line, end: lspRange.end.line }
|
|
3027
|
+
) ?? lspRange;
|
|
3028
|
+
const sigRow = raw.prepare("SELECT parameters, return_type, visibility FROM brain_symbol WHERE id = ?").get(symbol.id);
|
|
3029
|
+
const detail = sigRow === void 0 ? symbol.symbol_type : renderSignature(
|
|
3030
|
+
{
|
|
3031
|
+
name: symbol.name,
|
|
3032
|
+
symbol_type: symbol.symbol_type,
|
|
3033
|
+
parameters: sigRow.parameters,
|
|
3034
|
+
return_type: sigRow.return_type,
|
|
3035
|
+
visibility: null
|
|
3036
|
+
},
|
|
3037
|
+
languageForPath(symbol.path)
|
|
3038
|
+
);
|
|
3039
|
+
return {
|
|
3040
|
+
item: {
|
|
3041
|
+
name: symbol.name,
|
|
3042
|
+
kind: symbolKindFor(symbol.symbol_type),
|
|
3043
|
+
uri: `file://${symbol.path}`,
|
|
3044
|
+
range: lspRange,
|
|
3045
|
+
selectionRange: nameRange,
|
|
3046
|
+
detail,
|
|
3047
|
+
positionBasis: "source_resolved",
|
|
3048
|
+
data: { symbolId: symbol.id, isExternal: false }
|
|
3049
|
+
},
|
|
3050
|
+
notIndexed: false,
|
|
3051
|
+
absence: null,
|
|
3052
|
+
staleness
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
function itemFromNode(node, source, detail) {
|
|
3056
|
+
const filePath = node.filePath !== "" ? node.filePath : node.file;
|
|
3057
|
+
const mapper = source === null ? null : createPositionMapper(source);
|
|
3058
|
+
const window = mapper === null ? null : lspLineWindow(mapper, node.line, node.lineEnd);
|
|
3059
|
+
const start0 = window === null ? 0 : window.start;
|
|
3060
|
+
const end0 = window === null ? 0 : window.end;
|
|
3061
|
+
let range = unresolvableRange();
|
|
3062
|
+
let selectionRange = range;
|
|
3063
|
+
if (source !== null && window !== null) {
|
|
3064
|
+
range = {
|
|
3065
|
+
start: { line: start0, character: lineIndent(source, start0) },
|
|
3066
|
+
end: { line: end0, character: lineLength(source, end0) }
|
|
3067
|
+
};
|
|
3068
|
+
selectionRange = nameTokenRange(source, node.name, { start: start0, end: end0 }) ?? range;
|
|
3069
|
+
}
|
|
3070
|
+
const resolved = source !== null && window !== null;
|
|
3071
|
+
return {
|
|
3072
|
+
name: node.name,
|
|
3073
|
+
kind: symbolKindFor(node.symbolType),
|
|
3074
|
+
uri: filePath !== "" ? `file://${filePath}` : "",
|
|
3075
|
+
range,
|
|
3076
|
+
selectionRange,
|
|
3077
|
+
detail: resolved ? detail : `${detail} \xB7 position unresolvable (no readable source for this node)`,
|
|
3078
|
+
positionBasis: resolved ? "source_resolved" : "declared_unresolvable",
|
|
3079
|
+
data: { symbolId: node.id, isExternal: node.isExternal }
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
async function expandCalls(raw, projectId, request, traverse = defaultTraverse, readSource = makeSourceReader()) {
|
|
3083
|
+
const { item } = request;
|
|
3084
|
+
if (item.data.isExternal) {
|
|
3085
|
+
return {
|
|
3086
|
+
calls: [],
|
|
3087
|
+
truncated: false,
|
|
3088
|
+
maxDepthReached: 0,
|
|
3089
|
+
evidenceTier: "not_indexed",
|
|
3090
|
+
callSitesResolved: 0,
|
|
3091
|
+
callSitesApproximate: 0,
|
|
3092
|
+
droppedEdges: [],
|
|
3093
|
+
absence: absenceWith(
|
|
3094
|
+
"external_target",
|
|
3095
|
+
"the expanded item is an external node with no index identity, so there is nothing for bb to traverse (this is NOT 'this symbol has no callers')"
|
|
3096
|
+
),
|
|
3097
|
+
declaredAbsentNodes: [],
|
|
3098
|
+
staleness: unknownStaleness(),
|
|
3099
|
+
callSiteEntries: { resolved: 0, approximate: 0 },
|
|
3100
|
+
addressCrossCheck: emptyCrossCheck(),
|
|
3101
|
+
addressDisagreements: [],
|
|
3102
|
+
relationshipTypes: [...CALL_HIERARCHY_RELATIONSHIP_TYPES]
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
const rootRow = raw.prepare(
|
|
3106
|
+
`SELECT s.id, s.name, s.symbol_type, COALESCE(s.qualified_name,'') AS qualifiedName,
|
|
3107
|
+
COALESCE(s.line_start,0) AS line,
|
|
3108
|
+
COALESCE(s.line_end,0) AS lineEnd,
|
|
3109
|
+
COALESCE(f.relative_path,'') AS relativePath,
|
|
3110
|
+
COALESCE(f.path,'') AS filePath,
|
|
3111
|
+
f.id AS fileId,
|
|
3112
|
+
COALESCE(f.language,'unknown') AS language,
|
|
3113
|
+
COALESCE(s.complexity,0) AS complexity
|
|
3114
|
+
FROM brain_symbol s JOIN brain_file f ON f.id = s.file_id
|
|
3115
|
+
WHERE s.id = ?`
|
|
3116
|
+
).get(item.data.symbolId);
|
|
3117
|
+
if (rootRow === void 0) {
|
|
3118
|
+
return {
|
|
3119
|
+
calls: [],
|
|
3120
|
+
truncated: false,
|
|
3121
|
+
maxDepthReached: 0,
|
|
3122
|
+
evidenceTier: "not_indexed",
|
|
3123
|
+
callSitesResolved: 0,
|
|
3124
|
+
callSitesApproximate: 0,
|
|
3125
|
+
droppedEdges: [],
|
|
3126
|
+
absence: absenceWith(
|
|
3127
|
+
"symbol_not_in_index",
|
|
3128
|
+
`the index stores no symbol with id "${item.data.symbolId}", so there is no hierarchy to traverse (this is NOT 'this symbol has no callers')`
|
|
3129
|
+
),
|
|
3130
|
+
declaredAbsentNodes: [],
|
|
3131
|
+
staleness: unknownStaleness(),
|
|
3132
|
+
callSiteEntries: { resolved: 0, approximate: 0 },
|
|
3133
|
+
addressCrossCheck: emptyCrossCheck(),
|
|
3134
|
+
addressDisagreements: [],
|
|
3135
|
+
relationshipTypes: [...CALL_HIERARCHY_RELATIONSHIP_TYPES]
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
const rootScope = declaredAbsenceFor(rootRow.filePath);
|
|
3139
|
+
if (rootScope !== null) {
|
|
3140
|
+
return {
|
|
3141
|
+
calls: [],
|
|
3142
|
+
truncated: false,
|
|
3143
|
+
maxDepthReached: 0,
|
|
3144
|
+
evidenceTier: "not_indexed",
|
|
3145
|
+
callSitesResolved: 0,
|
|
3146
|
+
callSitesApproximate: 0,
|
|
3147
|
+
droppedEdges: [],
|
|
3148
|
+
absence: rootScope,
|
|
3149
|
+
declaredAbsentNodes: [],
|
|
3150
|
+
staleness: unknownStaleness([rootRow.filePath]),
|
|
3151
|
+
callSiteEntries: { resolved: 0, approximate: 0 },
|
|
3152
|
+
addressCrossCheck: emptyCrossCheck(),
|
|
3153
|
+
addressDisagreements: [],
|
|
3154
|
+
relationshipTypes: [...CALL_HIERARCHY_RELATIONSHIP_TYPES]
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
const ownNames = /* @__PURE__ */ new Set([rootRow.name]);
|
|
3158
|
+
const qualified = rootRow.qualifiedName ?? "";
|
|
3159
|
+
if (qualified.includes(".")) {
|
|
3160
|
+
const ownerSegment = qualified.slice(0, qualified.lastIndexOf(".")).split(".").pop() ?? "";
|
|
3161
|
+
if (ownerSegment.length > 0) ownNames.add(ownerSegment);
|
|
3162
|
+
}
|
|
3163
|
+
const maxDepth = Math.max(1, Math.min(request.maxDepth ?? CH_MAX_DEPTH, 10));
|
|
3164
|
+
const direction = request.direction === "incoming" ? "callers" : "callees";
|
|
3165
|
+
const engineRoot = {
|
|
3166
|
+
id: rootRow.id,
|
|
3167
|
+
name: rootRow.name,
|
|
3168
|
+
symbolType: rootRow.symbol_type,
|
|
3169
|
+
line: rootRow.line,
|
|
3170
|
+
lineEnd: rootRow.lineEnd,
|
|
3171
|
+
relativePath: rootRow.relativePath,
|
|
3172
|
+
filePath: rootRow.filePath,
|
|
3173
|
+
language: rootRow.language,
|
|
3174
|
+
complexity: rootRow.complexity
|
|
3175
|
+
};
|
|
3176
|
+
const result = await traverse(projectId, engineRoot, {
|
|
3177
|
+
direction,
|
|
3178
|
+
maxDepth,
|
|
3179
|
+
callerBudget: CH_FANOUT_BUDGET,
|
|
3180
|
+
calleeBudget: CH_FANOUT_BUDGET,
|
|
3181
|
+
// §15.3: ask for CALL rows only. Without this the engine's default ten-kind
|
|
3182
|
+
// superset reaches a renderer whose `fromRanges` must name a call site.
|
|
3183
|
+
relationshipTypes: [...CALL_HIERARCHY_RELATIONSHIP_TYPES]
|
|
3184
|
+
});
|
|
3185
|
+
const rootSource = readSource(rootRow.filePath);
|
|
3186
|
+
const edges = direction === "callers" ? result.callers : result.callees;
|
|
3187
|
+
const calls = [];
|
|
3188
|
+
const seenFrom = /* @__PURE__ */ new Set();
|
|
3189
|
+
const droppedEdges = [];
|
|
3190
|
+
const declaredAbsentNodes = [];
|
|
3191
|
+
const addressDisagreements = [];
|
|
3192
|
+
const edgePaths = [];
|
|
3193
|
+
let callSitesResolved = 0;
|
|
3194
|
+
let callSitesApproximate = 0;
|
|
3195
|
+
let resolvedEntries = 0;
|
|
3196
|
+
let approximateEntries = 0;
|
|
3197
|
+
const crossCheck = emptyCrossCheck();
|
|
3198
|
+
for (const edge of edges) {
|
|
3199
|
+
const otherId = direction === "callers" ? edge.from : edge.to;
|
|
3200
|
+
if (seenFrom.has(otherId)) continue;
|
|
3201
|
+
const node = result.nodes[otherId];
|
|
3202
|
+
if (node === void 0) continue;
|
|
3203
|
+
const nodePath = node.filePath !== "" ? node.filePath : node.file;
|
|
3204
|
+
if (!node.isExternal && declaredAbsenceFor(nodePath) !== null) {
|
|
3205
|
+
declaredAbsentNodes.push({
|
|
3206
|
+
name: node.name,
|
|
3207
|
+
symbolId: otherId,
|
|
3208
|
+
reason: `the node's own file (${nodePath}) is outside the bb launch set, so bb declares no call sites for it`
|
|
3209
|
+
});
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
edgePaths.push(nodePath);
|
|
3213
|
+
const callerSource = direction === "callers" ? readSource(nodePath) : rootSource;
|
|
3214
|
+
const callerWindow = callerSource === null ? null : lspLineWindow(
|
|
3215
|
+
createPositionMapper(callerSource),
|
|
3216
|
+
direction === "callers" ? node.line : rootRow.line,
|
|
3217
|
+
direction === "callers" ? node.lineEnd : rootRow.lineEnd
|
|
3218
|
+
);
|
|
3219
|
+
const callerLines0 = callerWindow;
|
|
3220
|
+
const invokedName = direction === "callers" ? rootRow.name : node.name;
|
|
3221
|
+
const callerOwnName = direction === "callers" ? node.name : rootRow.name;
|
|
3222
|
+
const callerNamePos = callerSource === null || callerLines0 === null ? void 0 : nameTokenRange(callerSource, callerOwnName, callerLines0)?.start;
|
|
3223
|
+
const scannedPath = direction === "callers" ? nodePath : rootRow.filePath;
|
|
3224
|
+
const targetDeclLine1 = direction === "callers" ? rootRow.line : node.line;
|
|
3225
|
+
const targetDeclPath = direction === "callers" ? rootRow.filePath : nodePath;
|
|
3226
|
+
const targetDeclPos = callerSource === null || scannedPath !== targetDeclPath ? void 0 : declarationTokenOf(callerSource, createPositionMapper(callerSource), invokedName, targetDeclLine1);
|
|
3227
|
+
const invocations = callerSource === null || callerLines0 === null ? [] : resolveInvocations(
|
|
3228
|
+
callerSource,
|
|
3229
|
+
invokedName,
|
|
3230
|
+
callerLines0,
|
|
3231
|
+
callerNamePos,
|
|
3232
|
+
ownNames,
|
|
3233
|
+
targetDeclPos === void 0 ? [] : [targetDeclPos]
|
|
3234
|
+
);
|
|
3235
|
+
const isSelfEdge = otherId === item.data.symbolId;
|
|
3236
|
+
if (isSelfEdge) {
|
|
3237
|
+
const receiverQualified = invocations.length > 0 && invocations.every((i) => i.kind === "receiver");
|
|
3238
|
+
if (receiverQualified) {
|
|
3239
|
+
droppedEdges.push({
|
|
3240
|
+
name: node.name,
|
|
3241
|
+
symbolId: otherId,
|
|
3242
|
+
reason: `receiver-qualified self-name call (${invocations[0].receiver}.${invokedName}) targets another value, not this symbol`
|
|
3243
|
+
});
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
const scanLines0 = invocations.map((i) => i.range.start.line);
|
|
3248
|
+
const edgeTarget = result.nodes[edge.to];
|
|
3249
|
+
const edgeTargetName = edgeTarget?.name ?? "";
|
|
3250
|
+
const edgeSource = result.nodes[edge.from];
|
|
3251
|
+
const likeForLike = edge.depth === 0 && edgeTargetName === invokedName && edgeSource !== void 0;
|
|
3252
|
+
let crossLines0 = scanLines0;
|
|
3253
|
+
let crossScanRan = true;
|
|
3254
|
+
if (!likeForLike) {
|
|
3255
|
+
crossScanRan = false;
|
|
3256
|
+
const hopPath = edgeSource === void 0 ? "" : edgeSource.filePath !== "" ? edgeSource.filePath : edgeSource.file;
|
|
3257
|
+
const hopSource = hopPath === "" ? null : readSource(hopPath);
|
|
3258
|
+
if (hopSource !== null && edgeSource !== void 0 && edgeTargetName !== "") {
|
|
3259
|
+
const hopMapper = createPositionMapper(hopSource);
|
|
3260
|
+
const hopWindow = lspLineWindow(hopMapper, edgeSource.line, edgeSource.lineEnd);
|
|
3261
|
+
if (hopWindow !== null) {
|
|
3262
|
+
const hopTargetPath = edgeTarget === void 0 ? "" : edgeTarget.filePath !== "" ? edgeTarget.filePath : edgeTarget.file;
|
|
3263
|
+
const hopDeclPos = hopTargetPath !== "" && hopPath === hopTargetPath ? declarationTokenOf(hopSource, hopMapper, edgeTargetName, edgeTarget.line) : void 0;
|
|
3264
|
+
crossLines0 = resolveInvocations(
|
|
3265
|
+
hopSource,
|
|
3266
|
+
edgeTargetName,
|
|
3267
|
+
hopWindow,
|
|
3268
|
+
void 0,
|
|
3269
|
+
/* @__PURE__ */ new Set(),
|
|
3270
|
+
hopDeclPos === void 0 ? [] : [hopDeclPos]
|
|
3271
|
+
).map((i) => i.range.start.line);
|
|
3272
|
+
crossScanRan = true;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
const agreement = crossCheckEdge(edge, crossLines0, scanLines0[0] ?? null, edgeTargetName);
|
|
3277
|
+
if (!crossScanRan) agreement.agreement = "neither";
|
|
3278
|
+
tallyCrossCheck(crossCheck, agreement, agreement.address !== null);
|
|
3279
|
+
if (!crossScanRan) crossCheck.comparisonNotRun++;
|
|
3280
|
+
if (!CALL_HIERARCHY_RELATIONSHIP_TYPES.includes(edge.relationshipType)) {
|
|
3281
|
+
droppedEdges.push({
|
|
3282
|
+
name: node.name,
|
|
3283
|
+
symbolId: otherId,
|
|
3284
|
+
reason: `the stored row is a "${edge.relationshipType}" relation, not a call; a call hierarchy renders call sites and this relation names none, so the row is omitted rather than rendered with a range (contract 15.3)`
|
|
3285
|
+
});
|
|
3286
|
+
continue;
|
|
3287
|
+
}
|
|
3288
|
+
if (invocations.length === 0 && callerSource !== null && callerLines0 !== null && !node.isExternal && edge.relationshipType === "calls") {
|
|
3289
|
+
const hop = Number.isInteger(edge.depth) && edge.depth >= 1;
|
|
3290
|
+
const storeLine = agreement.address !== null && isExactAddress(agreement.address) ? agreement.address.line : null;
|
|
3291
|
+
const reason = hop ? `${node.name} does not invoke ${invokedName} in its own source; the engine reports this row at depth ${edge.depth}, so it is a transitive hop (${node.name} reaches ${invokedName} through an intermediate symbol), not a direct ${direction === "callers" ? "caller" : "callee"}` : storeLine !== null ? `${node.name} does not invoke ${invokedName} anywhere in its own source, yet the stored row names call site line ${storeLine} - the store and the caller's own text disagree about this DIRECT row; this surface renders the source-resolved position and declares the difference (contract 15.1), because rendering the stored line would be rendering a position this surface cannot verify (15.3)` : `${node.name} does not invoke ${invokedName} in its own source and the stored row names no call site for it - a stale stored row, not a direct ${direction === "callers" ? "caller" : "callee"}`;
|
|
3292
|
+
droppedEdges.push({ name: node.name, symbolId: otherId, reason });
|
|
3293
|
+
if (agreement.agreement !== "neither") {
|
|
3294
|
+
addressDisagreements.push({
|
|
3295
|
+
name: node.name,
|
|
3296
|
+
symbolId: otherId,
|
|
3297
|
+
reason,
|
|
3298
|
+
agreement
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
seenFrom.add(otherId);
|
|
3304
|
+
const recursiveMarker = isSelfEdge ? " \xB7 recursive" : "";
|
|
3305
|
+
const unplaceable = callerLines0 === null && callerSource !== null;
|
|
3306
|
+
const baseDetail = `${node.symbolType} \xB7 ${edge.relationshipType} \xB7 ${recursiveMarker ? "self" : "1 call site"}${recursiveMarker}`;
|
|
3307
|
+
const detail = unplaceable ? `${baseDetail} \xB7 caller declaration line unresolvable in its own source` : baseDetail;
|
|
3308
|
+
const expanded = itemFromNode(node, direction === "callers" ? callerSource : readSource(nodePath), detail);
|
|
3309
|
+
let fromRanges;
|
|
3310
|
+
if (invocations.length > 0) {
|
|
3311
|
+
fromRanges = invocations.map((i) => i.range);
|
|
3312
|
+
callSitesResolved += invocations.length;
|
|
3313
|
+
resolvedEntries++;
|
|
3314
|
+
} else {
|
|
3315
|
+
fromRanges = [expanded.selectionRange];
|
|
3316
|
+
callSitesApproximate += 1;
|
|
3317
|
+
approximateEntries++;
|
|
3318
|
+
}
|
|
3319
|
+
const withPosition = invocations.length === 0 ? { ...expanded, detail: `${expanded.detail} \xB7 position approximate` } : expanded;
|
|
3320
|
+
if (direction === "callers") {
|
|
3321
|
+
calls.push({ from: withPosition, fromRanges });
|
|
3322
|
+
} else {
|
|
3323
|
+
calls.push({ to: withPosition, fromRanges });
|
|
3324
|
+
}
|
|
3325
|
+
if (agreement.agreement !== "agree" && agreement.agreement !== "neither") {
|
|
3326
|
+
const storeLineText = agreement.address !== null && isExactAddress(agreement.address) ? `${agreement.address.file}:${agreement.address.line}` : agreement.address === null ? "no address at all" : `address kind "${agreement.address.kind}"`;
|
|
3327
|
+
addressDisagreements.push({
|
|
3328
|
+
name: node.name,
|
|
3329
|
+
symbolId: otherId,
|
|
3330
|
+
reason: agreement.agreement === "disagree" ? `the store names call site ${storeLineText} while a like-for-like scan of ${agreement.edgeTargetName} in the row's own source resolved ${JSON.stringify(agreement.sourceLines0.map((l) => l + 1))} (1-based) - the two sources of truth contradict each other for this ${agreement.direct ? "direct" : `depth-${agreement.depth} hop`} row, and this surface renders the source-resolved position` : agreement.agreement === "store-only" ? `the store names call site ${storeLineText} and a like-for-like scan of ${agreement.edgeTargetName} in the row's own source resolves nothing - the invocation is not in the text this surface reads (a macro, a template or a dynamic dispatch), on a ${agreement.direct ? "direct" : `depth-${agreement.depth} hop`} row` : `a like-for-like scan resolved ${JSON.stringify(agreement.sourceLines0.map((l) => l + 1))} (1-based) while the store's address is ${storeLineText} - the store names no call site for this row`,
|
|
3331
|
+
agreement: { ...agreement, renderedLine0: fromRanges[0]?.start.line ?? null }
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
return {
|
|
3336
|
+
calls,
|
|
3337
|
+
truncated: result.truncated,
|
|
3338
|
+
truncationReason: result.truncationReason,
|
|
3339
|
+
maxDepthReached: result.maxDepthReached,
|
|
3340
|
+
evidenceTier: "indexed",
|
|
3341
|
+
callSitesResolved,
|
|
3342
|
+
callSitesApproximate,
|
|
3343
|
+
callSiteEntries: { resolved: resolvedEntries, approximate: approximateEntries },
|
|
3344
|
+
droppedEdges,
|
|
3345
|
+
absence: calls.length === 0 ? absenceWith(
|
|
3346
|
+
// §11.7: the symbol WAS found and served; what is empty is its call
|
|
3347
|
+
// list among the kinds this surface reads. That is a different fact
|
|
3348
|
+
// from "no symbol covers the position" (prepare's code), so it has its
|
|
3349
|
+
// own machine-readable code — a client that branches on the code must
|
|
3350
|
+
// not be told the symbol is missing.
|
|
3351
|
+
"no_stored_calls",
|
|
3352
|
+
// §11.7 + §15.1: the absence names the filter, because an answer
|
|
3353
|
+
// emptied BY the filter is a different claim from an answer emptied
|
|
3354
|
+
// because the STORE holds no row.
|
|
3355
|
+
//
|
|
3356
|
+
// WHAT EACH BRANCH MAY CLAIM, kept strictly to what this answer read.
|
|
3357
|
+
// The surface asks the engine for `calls` alone (measured on the T2
|
|
3358
|
+
// fixture at HEAD: `get/incoming` answers zero caller rows), so no
|
|
3359
|
+
// branch may claim what the engine would have said about another kind:
|
|
3360
|
+
// edges.length === 0 no stored CALL row reached this answer. "none
|
|
3361
|
+
// stored" is scoped to the kinds this surface
|
|
3362
|
+
// read, and the sentence says so.
|
|
3363
|
+
// edges.length > 0 this many stored CALL rows were read and this
|
|
3364
|
+
// many were dropped or declared absent. A
|
|
3365
|
+
// non-call row the surface DID see is named in
|
|
3366
|
+
// `droppedEdges` with its kind, so a reader can
|
|
3367
|
+
// find the rows this answer declined rather
|
|
3368
|
+
// than being told they do not exist.
|
|
3369
|
+
//
|
|
3370
|
+
// DELIBERATELY NOT CLAIMED: that the engine returned no stored edge of
|
|
3371
|
+
// any kind, or that the store holds no relationship row of another
|
|
3372
|
+
// kind. This surface asked for one kind and cannot know either. The
|
|
3373
|
+
// pre-M55 wording of the first branch claimed both ("the engine
|
|
3374
|
+
// returned no stored … edge of any kind"), which a filtered surface is
|
|
3375
|
+
// not entitled to say; the declared filter is what lets a reader bound
|
|
3376
|
+
// the claim.
|
|
3377
|
+
edges.length === 0 ? `no stored ${direction === "callers" ? "caller" : "callee"} CALL edge was read for this symbol (none stored among the relationship kind(s) this surface reads). This surface asked the engine for ${JSON.stringify(CALL_HIERARCHY_RELATIONSHIP_TYPES)} only and declares that filter (\xA715.1), so "none stored" is a statement about ${JSON.stringify(CALL_HIERARCHY_RELATIONSHIP_TYPES)} only \u2014 it is NOT a statement that the store holds no relationship row of any other kind` : `no stored ${direction === "callers" ? "caller" : "callee"} CALL row of this symbol is rendered. This surface reads the ${CALL_HIERARCHY_RELATIONSHIP_TYPES.length} relationship kind(s) ${JSON.stringify(CALL_HIERARCHY_RELATIONSHIP_TYPES)} only and declares that filter on every response (\xA715.1): ${edges.length} stored CALL row(s) were read and ${droppedEdges.length} were dropped or declared absent, each with its reason and kind named \u2014 rows of other kinds may be stored for this symbol and are not ${direction === "callers" ? "callers" : "callees"} here, so this answer is "no stored call rendered", not "no stored relationship"`,
|
|
3378
|
+
rootRow.filePath
|
|
3379
|
+
) : null,
|
|
3380
|
+
declaredAbsentNodes,
|
|
3381
|
+
staleness: expandStaleness(raw, rootRow.fileId ?? null, rootRow.filePath, edgePaths),
|
|
3382
|
+
addressCrossCheck: crossCheck,
|
|
3383
|
+
addressDisagreements,
|
|
3384
|
+
// §15.1: stated on the NORMAL answer too, not only on the absence paths —
|
|
3385
|
+
// a reader of a non-empty caller list needs the same filter declaration as
|
|
3386
|
+
// a reader of an empty one.
|
|
3387
|
+
relationshipTypes: [...CALL_HIERARCHY_RELATIONSHIP_TYPES]
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
// src/lsp/server.ts
|
|
3392
|
+
var STALENESS_SCHEMA_VERSION = 1;
|
|
3393
|
+
function stalenessProviderPayload() {
|
|
3394
|
+
return {
|
|
3395
|
+
schemaVersion: STALENESS_SCHEMA_VERSION,
|
|
3396
|
+
marker: {
|
|
3397
|
+
stale: "boolean",
|
|
3398
|
+
status: ["fresh", "stale", "missing", "unknown"],
|
|
3399
|
+
pendingReindex: "boolean"
|
|
3400
|
+
},
|
|
3401
|
+
/** The declared sync model (contract §4.2): filesystem-side, debounced by
|
|
3402
|
+
* DEBOUNCE_MS in src/lsp/freshness.ts, with a scan-on-read fallback when
|
|
3403
|
+
* the watcher fails. No latency figure is advertised — this gate measures
|
|
3404
|
+
* none, and an unmeasured budget presented as a budget is a §1 claim
|
|
3405
|
+
* violation. */
|
|
3406
|
+
syncModel: {
|
|
3407
|
+
source: "filesystem-watch",
|
|
3408
|
+
debounceMs: 300,
|
|
3409
|
+
fallback: "scan-on-read",
|
|
3410
|
+
clientDocumentSync: false
|
|
3411
|
+
}
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
function impactProviderPayload() {
|
|
3415
|
+
return { schemaVersion: 1 };
|
|
3416
|
+
}
|
|
3417
|
+
function absenceProviderPayload() {
|
|
3418
|
+
return {
|
|
3419
|
+
schemaVersion: ABSENCE_SCHEMA_VERSION,
|
|
3420
|
+
method: ABSENCE_METHOD,
|
|
3421
|
+
direction: "serverToClient",
|
|
3422
|
+
carriers: [ABSENCE_METHOD, "window/logMessage"],
|
|
3423
|
+
/** The reason vocabulary, advertised so a client renders known reasons
|
|
3424
|
+
* correctly instead of string-matching on prose. */
|
|
3425
|
+
reasons: ["language_not_launched", "not_indexed", "unresolvable_location", "unsupported_language", "not_callable"],
|
|
3426
|
+
/** What a client is being told, stated plainly: an EMPTY standard result
|
|
3427
|
+
* on these methods is not evidence that nothing exists. */
|
|
3428
|
+
emptyResultMeans: "absent-or-unserved \u2014 not 'nothing found'; read the bb/absence notice for which"
|
|
3429
|
+
};
|
|
3430
|
+
}
|
|
3431
|
+
function workspaceProviderPayload() {
|
|
3432
|
+
return { workspaceFolders: { supported: false, changeNotifications: false } };
|
|
3433
|
+
}
|
|
3434
|
+
var CAPABILITY_COVERAGE = [
|
|
3435
|
+
{ key: "hoverProvider", methods: ["textDocument/hover"], kind: "handler", value: () => true },
|
|
3436
|
+
{ key: "definitionProvider", methods: ["textDocument/definition"], kind: "handler", value: () => true },
|
|
3437
|
+
{ key: "referencesProvider", methods: ["textDocument/references"], kind: "handler", value: () => true },
|
|
3438
|
+
{
|
|
3439
|
+
key: "documentHighlightProvider",
|
|
3440
|
+
methods: ["textDocument/documentHighlight"],
|
|
3441
|
+
kind: "handler",
|
|
3442
|
+
value: () => true
|
|
3443
|
+
},
|
|
3444
|
+
{ key: "documentSymbolProvider", methods: ["textDocument/documentSymbol"], kind: "handler", value: () => true },
|
|
3445
|
+
{ key: "workspaceSymbolProvider", methods: ["workspace/symbol"], kind: "handler", value: () => true },
|
|
3446
|
+
{
|
|
3447
|
+
key: "callHierarchyProvider",
|
|
3448
|
+
methods: [
|
|
3449
|
+
"textDocument/prepareCallHierarchy",
|
|
3450
|
+
"callHierarchy/incomingCalls",
|
|
3451
|
+
"callHierarchy/outgoingCalls"
|
|
3452
|
+
],
|
|
3453
|
+
kind: "handler",
|
|
3454
|
+
value: () => true
|
|
3455
|
+
},
|
|
3456
|
+
{ key: "graphProvider", methods: ["bb/graph"], kind: "handler", value: () => ({ schemaVersion: 2 }) },
|
|
3457
|
+
{
|
|
3458
|
+
key: "stalenessProvider",
|
|
3459
|
+
methods: ["bb/staleness"],
|
|
3460
|
+
kind: "handler",
|
|
3461
|
+
value: stalenessProviderPayload
|
|
3462
|
+
},
|
|
3463
|
+
{
|
|
3464
|
+
key: "impactProvider",
|
|
3465
|
+
methods: ["bb/impact"],
|
|
3466
|
+
kind: "handler",
|
|
3467
|
+
value: impactProviderPayload
|
|
3468
|
+
},
|
|
3469
|
+
{
|
|
3470
|
+
key: "workspace",
|
|
3471
|
+
methods: [],
|
|
3472
|
+
kind: "static",
|
|
3473
|
+
value: workspaceProviderPayload
|
|
3474
|
+
},
|
|
3475
|
+
{
|
|
3476
|
+
/**
|
|
3477
|
+
* The declared-absence channel. `kind: "static"` because it describes a
|
|
3478
|
+
* SERVER-TO-CLIENT notification, not a request handler: it is offered
|
|
3479
|
+
* whenever the server is running, and there is deliberately no inbound
|
|
3480
|
+
* `bb/absence` handler to advertise (G10 asserts an inbound request gets
|
|
3481
|
+
* MethodNotFound). `direction` is stated so a client cannot build a
|
|
3482
|
+
* request around it.
|
|
3483
|
+
*/
|
|
3484
|
+
key: "absenceProvider",
|
|
3485
|
+
methods: [],
|
|
3486
|
+
kind: "static",
|
|
3487
|
+
value: absenceProviderPayload
|
|
3488
|
+
}
|
|
3489
|
+
];
|
|
3490
|
+
function capabilityKeyForMethod(method) {
|
|
3491
|
+
return CAPABILITY_COVERAGE.find((e) => e.methods.includes(method))?.key;
|
|
3492
|
+
}
|
|
3493
|
+
var ErrorCodes = {
|
|
3494
|
+
ParseError: -32700,
|
|
3495
|
+
InvalidRequest: -32600,
|
|
3496
|
+
MethodNotFound: -32601,
|
|
3497
|
+
InvalidParams: -32602,
|
|
3498
|
+
InternalError: -32603,
|
|
3499
|
+
ServerNotInitialized: -32002
|
|
3500
|
+
};
|
|
3501
|
+
function languageForMessage(params) {
|
|
3502
|
+
const uri = params?.textDocument?.uri;
|
|
3503
|
+
if (typeof uri !== "string" || uri.length === 0) return null;
|
|
3504
|
+
return languageForPath(uriToPath(uri));
|
|
3505
|
+
}
|
|
3506
|
+
var ASTENGINE_UNROUTABLE_PREFIX = "Unsupported file type";
|
|
3507
|
+
function asDocumentUnroutable(error) {
|
|
3508
|
+
if (error instanceof DocumentUnroutable) return error;
|
|
3509
|
+
if (error instanceof Error && typeof error.message === "string" && error.message.startsWith(ASTENGINE_UNROUTABLE_PREFIX)) {
|
|
3510
|
+
return new DocumentUnroutable(error.message);
|
|
3511
|
+
}
|
|
3512
|
+
return null;
|
|
3513
|
+
}
|
|
3514
|
+
function guardUnroutableDocument(method, handler) {
|
|
3515
|
+
return async (params) => {
|
|
3516
|
+
const uri = params?.textDocument?.uri;
|
|
3517
|
+
const hasDocument = typeof uri === "string" && uri.length > 0;
|
|
3518
|
+
try {
|
|
3519
|
+
return await handler(params);
|
|
3520
|
+
} catch (error) {
|
|
3521
|
+
if (asDocumentUnroutable(error) === null) throw error;
|
|
3522
|
+
throw new DeclaredAbsenceThrow({
|
|
3523
|
+
method,
|
|
3524
|
+
reason: "language_not_launched",
|
|
3525
|
+
language: hasDocument ? languageForMessage(params) : null,
|
|
3526
|
+
detail: "this file's type is outside the analyzer's routed set, so it was never routed and the index was not searched",
|
|
3527
|
+
evidenceTier: "unsupported_language",
|
|
3528
|
+
searchedIndex: false,
|
|
3529
|
+
totalCount: null
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
var DeclaredParamsError = class extends Error {
|
|
3535
|
+
code = ErrorCodes.InvalidParams;
|
|
3536
|
+
constructor(message) {
|
|
3537
|
+
super(message);
|
|
3538
|
+
this.name = "DeclaredParamsError";
|
|
3539
|
+
}
|
|
3540
|
+
};
|
|
3541
|
+
var ABSENCE_SCHEMA_VERSION = 1;
|
|
3542
|
+
var ABSENCE_METHOD = "bb/absence";
|
|
3543
|
+
var ABSENCE_LOG_TYPE = 3;
|
|
3544
|
+
var DocumentUnroutable = class extends Error {
|
|
3545
|
+
constructor(message) {
|
|
3546
|
+
super(message);
|
|
3547
|
+
this.name = "DocumentUnroutable";
|
|
3548
|
+
}
|
|
3549
|
+
};
|
|
3550
|
+
function absenceReasonFor(code) {
|
|
3551
|
+
switch (code) {
|
|
3552
|
+
case "language_outside_launch_set":
|
|
3553
|
+
case "file_type_unknown":
|
|
3554
|
+
return "language_not_launched";
|
|
3555
|
+
case "unsupported_language":
|
|
3556
|
+
return "unsupported_language";
|
|
3557
|
+
case "no_stored_calls":
|
|
3558
|
+
return "not_callable";
|
|
3559
|
+
default:
|
|
3560
|
+
return "not_indexed";
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
var DeclaredAbsenceThrow = class extends Error {
|
|
3564
|
+
absence;
|
|
3565
|
+
constructor(absence) {
|
|
3566
|
+
super(absence.detail);
|
|
3567
|
+
this.name = "DeclaredAbsenceThrow";
|
|
3568
|
+
this.absence = absence;
|
|
3569
|
+
}
|
|
3570
|
+
};
|
|
3571
|
+
var JSONRPC = "2.0";
|
|
3572
|
+
function isNotification(msg) {
|
|
3573
|
+
return msg.method !== void 0 && msg.id === void 0;
|
|
3574
|
+
}
|
|
3575
|
+
function isResponse(msg) {
|
|
3576
|
+
return msg.method === void 0 && msg.id !== void 0;
|
|
3577
|
+
}
|
|
3578
|
+
var BbLspServer = class {
|
|
3579
|
+
lifecycleState = "uninitialized";
|
|
3580
|
+
handlers = /* @__PURE__ */ new Map();
|
|
3581
|
+
/** Methods opted into the declared-absence seam (see register). */
|
|
3582
|
+
absentableMethods = /* @__PURE__ */ new Set();
|
|
3583
|
+
/**
|
|
3584
|
+
* Server→client notifications produced by the most recent handleMessage
|
|
3585
|
+
* call, drained by the wire loop. Queueing them here (rather than writing
|
|
3586
|
+
* straight to the stream) keeps handleMessage pure and testable, and is what
|
|
3587
|
+
* lets the verifier assert on EXACTLY what a client would receive.
|
|
3588
|
+
*/
|
|
3589
|
+
outboundNotifications = [];
|
|
3590
|
+
protocolEvents = [];
|
|
3591
|
+
hooks;
|
|
3592
|
+
stderrLogPath = process.env.BB_LSP_STDERR_LOG ?? null;
|
|
3593
|
+
context;
|
|
3594
|
+
constructor(root, hooks = {}) {
|
|
3595
|
+
const resolved = path2.resolve(root);
|
|
3596
|
+
let canonical = resolved;
|
|
3597
|
+
try {
|
|
3598
|
+
canonical = realpathSync.native(resolved);
|
|
3599
|
+
} catch {
|
|
3600
|
+
}
|
|
3601
|
+
this.context = {
|
|
3602
|
+
root: canonical,
|
|
3603
|
+
projectId: getProjectId(canonical),
|
|
3604
|
+
serverInfo: { name: "bb_lsp", version: BACKBENCHER_BRAIN_VERSION }
|
|
3605
|
+
};
|
|
3606
|
+
this.hooks = hooks;
|
|
3607
|
+
}
|
|
3608
|
+
/** Register a feature handler. Capabilities derive from this table.
|
|
3609
|
+
*
|
|
3610
|
+
* `absentable: true` opts the method into the declared-absence seam: a
|
|
3611
|
+
* component fault of a DECLARED type (PositionError, DeclaredAbsenceThrow,
|
|
3612
|
+
* DocumentUnroutable) then answers as declared absence with a reason the
|
|
3613
|
+
* client can read, instead of -32603. The opt-in is per method and
|
|
3614
|
+
* deliberate: a blanket catch over all handlers would let a genuine bug
|
|
3615
|
+
* render as "nothing here", and G10 gates exactly that regression. */
|
|
3616
|
+
register(method, handler, opts = {}) {
|
|
3617
|
+
this.handlers.set(method, opts.absentable ? guardUnroutableDocument(method, handler) : handler);
|
|
3618
|
+
if (opts.absentable) this.absentableMethods.add(method);
|
|
3619
|
+
}
|
|
3620
|
+
/** The LSP methods this instance can actually serve — the honest successor
|
|
3621
|
+
* to the deleted `IMPLEMENTED_METHODS` export, which was a frozen empty
|
|
3622
|
+
* array while twelve handlers were registered. Handler set is per-instance
|
|
3623
|
+
* (it is whatever `register`/`wireFeatures` put there), so the truth is an
|
|
3624
|
+
* instance read, not a module constant. */
|
|
3625
|
+
implementedMethods() {
|
|
3626
|
+
return [...this.handlers.keys()].sort();
|
|
3627
|
+
}
|
|
3628
|
+
/** Capabilities advertised in the initialize result — derived from the
|
|
3629
|
+
* registered handler set so advertisement can never outrun reality, and
|
|
3630
|
+
* cross-checked against CAPABILITY_COVERAGE by script/m24/verify-server.ts
|
|
3631
|
+
* (G6) in both directions.
|
|
3632
|
+
*
|
|
3633
|
+
* The three additions below are the ones a client could not previously
|
|
3634
|
+
* discover:
|
|
3635
|
+
* - stalenessProvider / impactProvider: bb/staleness and bb/impact were
|
|
3636
|
+
* registered but had NO capability key, so a client could only reach
|
|
3637
|
+
* them blind. LSP 3.18 has no standard `impactProvider`, and the
|
|
3638
|
+
* `bb/` namespace is private honesty plumbing rather than a standard
|
|
3639
|
+
* provider, so these ride top-level keys exactly as bb/graph already
|
|
3640
|
+
* did (`graphProvider`) — with the schemaVersion the handler ENFORCES
|
|
3641
|
+
* on inbound requests, so a client can detect a version mismatch before
|
|
3642
|
+
* it sends one. The G6 table makes their absence a gate failure.
|
|
3643
|
+
* - workspace: the single-root limitation is a server CONSTANT, not a
|
|
3644
|
+
* handler-derived fact, so it is declared unconditionally. Advertising
|
|
3645
|
+
* `workspaceFolders.supported: false` is how a client learns it from the
|
|
3646
|
+
* wire (contract §11.6) instead of inferring it from a hang.
|
|
3647
|
+
* What is deliberately NOT here: `textDocumentSync`. This server registers
|
|
3648
|
+
* no `textDocument/did*` handler, so advertising a sync kind would tell the
|
|
3649
|
+
* client to send notifications into a void — a capability we do not
|
|
3650
|
+
* implement, which contract §2 calls a gate failure. The sync model is
|
|
3651
|
+
* filesystem-side (see the file header) and is advertised, truthfully, as
|
|
3652
|
+
* `stalenessProvider.syncModel`. G8 pins the absence so a future edit that
|
|
3653
|
+
* advertises a sync kind without a did* handler fails the gate. */
|
|
3654
|
+
capabilities() {
|
|
3655
|
+
const caps = {};
|
|
3656
|
+
if (this.handlers.has("textDocument/hover")) caps.hoverProvider = true;
|
|
3657
|
+
if (this.handlers.has("textDocument/definition")) caps.definitionProvider = true;
|
|
3658
|
+
if (this.handlers.has("textDocument/references")) caps.referencesProvider = true;
|
|
3659
|
+
if (this.handlers.has("textDocument/documentHighlight")) caps.documentHighlightProvider = true;
|
|
3660
|
+
if (this.handlers.has("textDocument/documentSymbol")) caps.documentSymbolProvider = true;
|
|
3661
|
+
if (this.handlers.has("workspace/symbol")) caps.workspaceSymbolProvider = true;
|
|
3662
|
+
if (this.handlers.has("textDocument/prepareCallHierarchy") && this.handlers.has("callHierarchy/incomingCalls") && this.handlers.has("callHierarchy/outgoingCalls")) {
|
|
3663
|
+
caps.callHierarchyProvider = true;
|
|
3664
|
+
}
|
|
3665
|
+
if (this.handlers.has("bb/graph")) caps.graphProvider = { schemaVersion: 2 };
|
|
3666
|
+
if (this.handlers.has("bb/staleness")) caps.stalenessProvider = stalenessProviderPayload();
|
|
3667
|
+
if (this.handlers.has("bb/impact")) caps.impactProvider = impactProviderPayload();
|
|
3668
|
+
caps.absenceProvider = absenceProviderPayload();
|
|
3669
|
+
caps.workspace = workspaceProviderPayload();
|
|
3670
|
+
return caps;
|
|
3671
|
+
}
|
|
3672
|
+
state() {
|
|
3673
|
+
return this.lifecycleState;
|
|
3674
|
+
}
|
|
3675
|
+
/** The server→client notifications queued by the last handleMessage call.
|
|
3676
|
+
* Read-only: a test may inspect them, and the wire loop still drains the
|
|
3677
|
+
* same queue, so what the test asserts on is byte-for-byte what a client
|
|
3678
|
+
* receives. */
|
|
3679
|
+
notifications() {
|
|
3680
|
+
return [...this.outboundNotifications];
|
|
3681
|
+
}
|
|
3682
|
+
/** Take the queued notifications, leaving the queue empty. */
|
|
3683
|
+
drainNotifications() {
|
|
3684
|
+
const queued = this.outboundNotifications;
|
|
3685
|
+
this.outboundNotifications = [];
|
|
3686
|
+
return queued;
|
|
3687
|
+
}
|
|
3688
|
+
/** Queue a server→client notification. Every notification this server emits
|
|
3689
|
+
* goes through here so the carriers cannot drift: `bb/absence` for the
|
|
3690
|
+
* machine-readable record and `window/logMessage` for the human line are
|
|
3691
|
+
* emitted as ONE unit from `emitAbsence`, never separately. */
|
|
3692
|
+
notify(method, params) {
|
|
3693
|
+
this.outboundNotifications.push({ jsonrpc: JSONRPC, method, params });
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Announce a declared absence on BOTH carriers, from one call, so they cannot
|
|
3697
|
+
* disagree:
|
|
3698
|
+
* - `bb/absence` (versioned, machine-readable; what a client should read)
|
|
3699
|
+
* - `window/logMessage` (the protocol's own channel; reaches a client that
|
|
3700
|
+
* has never heard of bb/*)
|
|
3701
|
+
* Emitted ONLY on the absence path — never on a served request (G9's negative
|
|
3702
|
+
* case gates that; logging every successful hover would be noise the user
|
|
3703
|
+
* learns to ignore, which is how a warning channel dies).
|
|
3704
|
+
*/
|
|
3705
|
+
emitAbsence(absence) {
|
|
3706
|
+
this.notify(ABSENCE_METHOD, { schemaVersion: ABSENCE_SCHEMA_VERSION, ...absence });
|
|
3707
|
+
this.notify("window/logMessage", {
|
|
3708
|
+
type: ABSENCE_LOG_TYPE,
|
|
3709
|
+
// Named by method so the log line is actionable on its own: a user who
|
|
3710
|
+
// only sees this in an output panel learns WHICH request was not
|
|
3711
|
+
// answered, and why.
|
|
3712
|
+
message: `bb ${absence.method}: declared absence (${absence.reason}) \u2014 ${absence.detail}`
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
events() {
|
|
3716
|
+
return this.protocolEvents;
|
|
3717
|
+
}
|
|
3718
|
+
record(direction, method, errorCode) {
|
|
3719
|
+
const event = { direction, method, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3720
|
+
if (errorCode !== void 0) event.errorCode = errorCode;
|
|
3721
|
+
this.protocolEvents.push(event);
|
|
3722
|
+
this.hooks.onEvent?.(event);
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Handle one decoded JSON-RPC message. Returns the response message to send
|
|
3726
|
+
* (null for notifications and responses, which produce no reply).
|
|
3727
|
+
*/
|
|
3728
|
+
async handleMessage(message) {
|
|
3729
|
+
if (!message || typeof message !== "object") {
|
|
3730
|
+
return this.errorResponse(null, ErrorCodes.InvalidRequest, "invalid message");
|
|
3731
|
+
}
|
|
3732
|
+
if (isResponse(message)) return null;
|
|
3733
|
+
const method = message.method;
|
|
3734
|
+
if (typeof method !== "string") {
|
|
3735
|
+
return this.errorResponse(message.id ?? null, ErrorCodes.InvalidRequest, "missing method");
|
|
3736
|
+
}
|
|
3737
|
+
if (this.lifecycleState === "uninitialized" && method !== "initialize" && method !== "exit") {
|
|
3738
|
+
this.record("in", method, ErrorCodes.ServerNotInitialized);
|
|
3739
|
+
if (isNotification(message)) return null;
|
|
3740
|
+
return this.errorResponse(message.id, ErrorCodes.ServerNotInitialized, "server not initialized");
|
|
3741
|
+
}
|
|
3742
|
+
if (this.lifecycleState === "shutdown" && method !== "exit") {
|
|
3743
|
+
this.record("in", method, ErrorCodes.InvalidRequest);
|
|
3744
|
+
if (isNotification(message)) return null;
|
|
3745
|
+
return this.errorResponse(message.id, ErrorCodes.InvalidRequest, "invalid request after shutdown");
|
|
3746
|
+
}
|
|
3747
|
+
if (method === "exit") {
|
|
3748
|
+
this.record("in", "exit");
|
|
3749
|
+
return null;
|
|
3750
|
+
}
|
|
3751
|
+
if (method === "initialize") {
|
|
3752
|
+
this.record("in", "initialize");
|
|
3753
|
+
this.lifecycleState = "initialized";
|
|
3754
|
+
this.record("out", "initialize");
|
|
3755
|
+
return {
|
|
3756
|
+
jsonrpc: JSONRPC,
|
|
3757
|
+
id: message.id,
|
|
3758
|
+
result: {
|
|
3759
|
+
capabilities: this.capabilities(),
|
|
3760
|
+
serverInfo: this.context.serverInfo,
|
|
3761
|
+
protocolVersion: "3.18"
|
|
3762
|
+
}
|
|
3763
|
+
};
|
|
3764
|
+
}
|
|
3765
|
+
if (method === "initialized") {
|
|
3766
|
+
this.record("in", "initialized");
|
|
3767
|
+
return null;
|
|
3768
|
+
}
|
|
3769
|
+
if (method === "shutdown") {
|
|
3770
|
+
this.record("in", "shutdown");
|
|
3771
|
+
this.lifecycleState = "shutdown";
|
|
3772
|
+
this.record("out", "shutdown");
|
|
3773
|
+
return { jsonrpc: JSONRPC, id: message.id, result: null };
|
|
3774
|
+
}
|
|
3775
|
+
const handler = this.handlers.get(method);
|
|
3776
|
+
if (handler) {
|
|
3777
|
+
this.record("in", method);
|
|
3778
|
+
this.outboundNotifications = [];
|
|
3779
|
+
try {
|
|
3780
|
+
const result = await handler(message.params);
|
|
3781
|
+
this.record("out", method);
|
|
3782
|
+
return { jsonrpc: JSONRPC, id: message.id, result };
|
|
3783
|
+
} catch (error) {
|
|
3784
|
+
if (error instanceof DeclaredParamsError) {
|
|
3785
|
+
this.record("out", method, ErrorCodes.InvalidParams);
|
|
3786
|
+
return this.errorResponse(message.id, ErrorCodes.InvalidParams, error.message);
|
|
3787
|
+
}
|
|
3788
|
+
const absentable = this.absentableMethods.has(method);
|
|
3789
|
+
if (absentable && error instanceof DeclaredAbsenceThrow) {
|
|
3790
|
+
this.emitAbsence(error.absence);
|
|
3791
|
+
this.record("out", method);
|
|
3792
|
+
return { jsonrpc: JSONRPC, id: message.id, result: null };
|
|
3793
|
+
}
|
|
3794
|
+
if (absentable && error instanceof PositionError) {
|
|
3795
|
+
this.emitAbsence({
|
|
3796
|
+
method,
|
|
3797
|
+
reason: "unresolvable_location",
|
|
3798
|
+
language: languageForMessage(message.params),
|
|
3799
|
+
detail: `${error.message} \u2014 the position does not resolve, so there is no answer to give (contract \xA713.10)`,
|
|
3800
|
+
evidenceTier: null,
|
|
3801
|
+
searchedIndex: false,
|
|
3802
|
+
totalCount: null
|
|
3803
|
+
});
|
|
3804
|
+
this.record("out", method);
|
|
3805
|
+
return { jsonrpc: JSONRPC, id: message.id, result: null };
|
|
3806
|
+
}
|
|
3807
|
+
if (absentable && error instanceof DocumentUnroutable) {
|
|
3808
|
+
this.emitAbsence({
|
|
3809
|
+
method,
|
|
3810
|
+
reason: "language_not_launched",
|
|
3811
|
+
language: null,
|
|
3812
|
+
detail: `${error.message} \u2014 this file type is outside the analyzer's routed set, so the index was never searched`,
|
|
3813
|
+
evidenceTier: null,
|
|
3814
|
+
searchedIndex: false,
|
|
3815
|
+
totalCount: null
|
|
3816
|
+
});
|
|
3817
|
+
this.record("out", method);
|
|
3818
|
+
return { jsonrpc: JSONRPC, id: message.id, result: null };
|
|
3819
|
+
}
|
|
3820
|
+
this.record("out", method, ErrorCodes.InternalError);
|
|
3821
|
+
return this.errorResponse(message.id, ErrorCodes.InternalError, error?.message ?? String(error));
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
this.record("in", method, ErrorCodes.MethodNotFound);
|
|
3825
|
+
if (isNotification(message)) return null;
|
|
3826
|
+
return this.errorResponse(message.id, ErrorCodes.MethodNotFound, `method not implemented: ${method}`);
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3829
|
+
* Wire loop: read Content-Length-framed JSON-RPC messages from stdin, feed
|
|
3830
|
+
* each to handleMessage, and write responses to stdout. Resolves when the
|
|
3831
|
+
* exit notification arrives (or stdin closes).
|
|
3832
|
+
*/
|
|
3833
|
+
async run(input = process.stdin, output = process.stdout) {
|
|
3834
|
+
const onEpipe = (err) => {
|
|
3835
|
+
if (err?.code === "EPIPE") {
|
|
3836
|
+
try {
|
|
3837
|
+
output.end();
|
|
3838
|
+
} catch {
|
|
3839
|
+
}
|
|
3840
|
+
process.exit(0);
|
|
3841
|
+
}
|
|
3842
|
+
};
|
|
3843
|
+
process.on("error", onEpipe);
|
|
3844
|
+
if (output === process.stdout) process.stdout.on("error", onEpipe);
|
|
3845
|
+
if (input === process.stdin) process.stdin.on("error", onEpipe);
|
|
3846
|
+
const writeMessage = (message) => {
|
|
3847
|
+
if (!message) return;
|
|
3848
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
3849
|
+
output.write(`Content-Length: ${body.length}\r
|
|
3850
|
+
\r
|
|
3851
|
+
`);
|
|
3852
|
+
output.write(body);
|
|
3853
|
+
};
|
|
3854
|
+
const writeNotification = (notification) => {
|
|
3855
|
+
const body = Buffer.from(JSON.stringify(notification), "utf8");
|
|
3856
|
+
output.write(`Content-Length: ${body.length}\r
|
|
3857
|
+
\r
|
|
3858
|
+
`);
|
|
3859
|
+
output.write(body);
|
|
3860
|
+
};
|
|
3861
|
+
const drainNotifications = () => {
|
|
3862
|
+
for (const notification of this.drainNotifications()) writeNotification(notification);
|
|
3863
|
+
};
|
|
3864
|
+
return new Promise((resolve) => {
|
|
3865
|
+
let finished = false;
|
|
3866
|
+
const finish = () => {
|
|
3867
|
+
if (!finished) {
|
|
3868
|
+
finished = true;
|
|
3869
|
+
resolve();
|
|
3870
|
+
}
|
|
3871
|
+
};
|
|
3872
|
+
let chunks = [];
|
|
3873
|
+
let buffered = 0;
|
|
3874
|
+
let contentLength = -1;
|
|
3875
|
+
const tryExtract = () => {
|
|
3876
|
+
while (true) {
|
|
3877
|
+
if (contentLength < 0) {
|
|
3878
|
+
const combined = Buffer.concat(chunks);
|
|
3879
|
+
const idx = combined.indexOf("\r\n\r\n");
|
|
3880
|
+
if (idx === -1) return;
|
|
3881
|
+
const headerText = combined.subarray(0, idx).toString("utf8");
|
|
3882
|
+
const match = /Content-Length:\s*(\d+)\s*(?:\r\n.*)?$/is.exec(headerText);
|
|
3883
|
+
if (!match) {
|
|
3884
|
+
writeMessage(this.errorResponse(null, ErrorCodes.ParseError, "missing Content-Length header"));
|
|
3885
|
+
chunks = [];
|
|
3886
|
+
buffered = 0;
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
contentLength = parseInt(match[1], 10);
|
|
3890
|
+
const rest = combined.subarray(idx + 4);
|
|
3891
|
+
chunks = [rest];
|
|
3892
|
+
buffered = rest.length;
|
|
3893
|
+
}
|
|
3894
|
+
if (contentLength >= 0 && buffered >= contentLength) {
|
|
3895
|
+
const combined = Buffer.concat(chunks);
|
|
3896
|
+
const body = combined.subarray(0, contentLength);
|
|
3897
|
+
chunks = [combined.subarray(contentLength)];
|
|
3898
|
+
buffered = chunks[0].length;
|
|
3899
|
+
contentLength = -1;
|
|
3900
|
+
void this.dispatch(body.toString("utf8"), writeMessage, finish, drainNotifications);
|
|
3901
|
+
if (finished) return;
|
|
3902
|
+
continue;
|
|
3903
|
+
}
|
|
3904
|
+
return;
|
|
3905
|
+
}
|
|
3906
|
+
};
|
|
3907
|
+
input.on("data", (chunk) => {
|
|
3908
|
+
chunks.push(chunk);
|
|
3909
|
+
buffered += chunk.length;
|
|
3910
|
+
tryExtract();
|
|
3911
|
+
});
|
|
3912
|
+
input.on("end", finish);
|
|
3913
|
+
input.on("error", finish);
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
async dispatch(body, write, finish, drainNotifications) {
|
|
3917
|
+
let message;
|
|
3918
|
+
try {
|
|
3919
|
+
message = JSON.parse(body);
|
|
3920
|
+
} catch {
|
|
3921
|
+
write(this.errorResponse(null, ErrorCodes.ParseError, "parse error"));
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
if (message && isNotification(message) && message.method === "exit") {
|
|
3925
|
+
this.record("in", "exit");
|
|
3926
|
+
finish();
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
const response = await this.handleMessage(message);
|
|
3930
|
+
if (this.stderrLogPath) {
|
|
3931
|
+
const req = message;
|
|
3932
|
+
const err = response?.error;
|
|
3933
|
+
try {
|
|
3934
|
+
appendFileSync(this.stderrLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${req.method ?? "?"} id=${String(req.id)} params=${JSON.stringify(req.params ?? null)?.slice(0, 220)} -> ${err ? "ERROR " + String(err.message).slice(0, 200) : "ok"}
|
|
3935
|
+
`);
|
|
3936
|
+
} catch {
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
write(response);
|
|
3940
|
+
drainNotifications();
|
|
3941
|
+
}
|
|
3942
|
+
errorResponse(id, code, message) {
|
|
3943
|
+
return {
|
|
3944
|
+
jsonrpc: JSONRPC,
|
|
3945
|
+
id: id ?? null,
|
|
3946
|
+
error: { code, message }
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
};
|
|
3950
|
+
function createServer(root, hooks = {}) {
|
|
3951
|
+
return new BbLspServer(root, hooks);
|
|
3952
|
+
}
|
|
3953
|
+
function uriToPath(uri) {
|
|
3954
|
+
if (uri.startsWith("file://")) {
|
|
3955
|
+
try {
|
|
3956
|
+
return fileURLToPath(uri);
|
|
3957
|
+
} catch {
|
|
3958
|
+
return uri.slice("file://".length);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return uri;
|
|
3962
|
+
}
|
|
3963
|
+
function wireFeatures(server) {
|
|
3964
|
+
const projectId = server.context.projectId;
|
|
3965
|
+
let monitor = null;
|
|
3966
|
+
async function freshnessMonitor() {
|
|
3967
|
+
if (monitor !== null) return monitor;
|
|
3968
|
+
const { createFreshnessMonitor } = await import("../freshness-3K6VU5OT.js");
|
|
3969
|
+
const { reindexFilesParallel } = await import("../parallel-indexer-4PW3AFXP.js");
|
|
3970
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
3971
|
+
monitor = createFreshnessMonitor({
|
|
3972
|
+
projectId,
|
|
3973
|
+
projectPath: server.context.root,
|
|
3974
|
+
raw,
|
|
3975
|
+
fileIdForPath: (p) => {
|
|
3976
|
+
try {
|
|
3977
|
+
const row = raw.prepare("SELECT id FROM brain_file WHERE path = ?").get(p);
|
|
3978
|
+
return row?.id ?? null;
|
|
3979
|
+
} catch {
|
|
3980
|
+
return null;
|
|
3981
|
+
}
|
|
3982
|
+
},
|
|
3983
|
+
// LSP-safety-critical: the indexer's DEFAULT logger writes progress to
|
|
3984
|
+
// process.stdout — protocol poison on a stdio server (progress lines
|
|
3985
|
+
// corrupt JSON-RPC framing; the editor client dies with "Header must
|
|
3986
|
+
// provide a Content-Length property", found live by E5). All server-side
|
|
3987
|
+
// indexer output must go to stderr; worker forks already do the same.
|
|
3988
|
+
reindex: (changes) => reindexFilesParallel(projectId, server.context.root, changes, {
|
|
3989
|
+
logger: { log: (...args) => console.error(...args), write: (s) => process.stderr.write(s) }
|
|
3990
|
+
}),
|
|
3991
|
+
log: (msg, meta) => console.error(`[bb_lsp] ${msg}`, meta ?? "")
|
|
3992
|
+
});
|
|
3993
|
+
monitor.start();
|
|
3994
|
+
return monitor;
|
|
3995
|
+
}
|
|
3996
|
+
server.register("bb/staleness", async (params) => {
|
|
3997
|
+
const mon = await freshnessMonitor();
|
|
3998
|
+
const filePath = uriToPath(params?.uri ?? "");
|
|
3999
|
+
let fileId = null;
|
|
4000
|
+
try {
|
|
4001
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4002
|
+
const row = raw.prepare("SELECT id FROM brain_file WHERE path = ?").get(filePath);
|
|
4003
|
+
fileId = row?.id ?? null;
|
|
4004
|
+
} catch {
|
|
4005
|
+
}
|
|
4006
|
+
if (fileId === null) {
|
|
4007
|
+
return { stale: true, status: "missing", pendingReindex: false };
|
|
4008
|
+
}
|
|
4009
|
+
return mon.marker(fileId);
|
|
4010
|
+
});
|
|
4011
|
+
server.register("bb/graph", async (params) => {
|
|
4012
|
+
const { handleGraphRequest } = await import("../graph_request-MM4REP6H.js");
|
|
4013
|
+
let raw = null;
|
|
4014
|
+
try {
|
|
4015
|
+
raw = await BrainDatabase.getRawInstance(projectId);
|
|
4016
|
+
} catch {
|
|
4017
|
+
}
|
|
4018
|
+
return handleGraphRequest(raw, projectId, params);
|
|
4019
|
+
});
|
|
4020
|
+
server.register("bb/impact", async (params) => {
|
|
4021
|
+
const { handleImpactRequest } = await import("../impact_request-WDE47IZ5.js");
|
|
4022
|
+
let raw = null;
|
|
4023
|
+
try {
|
|
4024
|
+
raw = await BrainDatabase.getRawInstance(projectId);
|
|
4025
|
+
} catch {
|
|
4026
|
+
}
|
|
4027
|
+
return handleImpactRequest(raw, projectId, params);
|
|
4028
|
+
});
|
|
4029
|
+
server.register("textDocument/hover", async (params) => {
|
|
4030
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4031
|
+
const r = await hover(raw, {
|
|
4032
|
+
filePath: uriToPath(params?.textDocument?.uri ?? ""),
|
|
4033
|
+
position: params?.position ?? { line: 0, character: 0 }
|
|
4034
|
+
});
|
|
4035
|
+
if (r.contents === null || r.range === null || r.data === null) {
|
|
4036
|
+
const limits = r.provenance?.limitations ?? [];
|
|
4037
|
+
const notLaunched = limits.some((l) => l.includes("launch language set"));
|
|
4038
|
+
throw new DeclaredAbsenceThrow({
|
|
4039
|
+
method: "textDocument/hover",
|
|
4040
|
+
reason: notLaunched ? "language_not_launched" : "not_indexed",
|
|
4041
|
+
language: languageForMessage(params),
|
|
4042
|
+
// Human prose, never a JSON blob (§15.4): the component's own
|
|
4043
|
+
// limitation sentences, joined.
|
|
4044
|
+
detail: limits.length > 0 ? limits.join("; ") : "no indexed symbol at this position (the index was searched and stores nothing here)",
|
|
4045
|
+
evidenceTier: r.provenance?.evidenceClass ?? null,
|
|
4046
|
+
// A launch-language miss means the index WAS asked. A non-launch file
|
|
4047
|
+
// means it never was — the distinction that lets a client label the
|
|
4048
|
+
// difference instead of guessing.
|
|
4049
|
+
searchedIndex: !notLaunched,
|
|
4050
|
+
totalCount: null
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
return {
|
|
4054
|
+
contents: r.contents,
|
|
4055
|
+
range: { start: r.range.start, end: r.range.end },
|
|
4056
|
+
data: { provenance: r.provenance, symbol: r.data }
|
|
4057
|
+
};
|
|
4058
|
+
}, { absentable: true });
|
|
4059
|
+
server.register("textDocument/definition", async (params) => {
|
|
4060
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4061
|
+
const r = await resolveDefinition(raw, {
|
|
4062
|
+
filePath: uriToPath(params?.textDocument?.uri ?? ""),
|
|
4063
|
+
line: params?.position?.line ?? 0,
|
|
4064
|
+
character: params?.position?.character ?? 0
|
|
4065
|
+
});
|
|
4066
|
+
if (!r.found || r.location === null) {
|
|
4067
|
+
throw new DeclaredAbsenceThrow(
|
|
4068
|
+
navigationAbsence("textDocument/definition", params, r.evidenceTier, {
|
|
4069
|
+
limit: r.symbol?.qualifiedName ?? r.symbol?.name ?? null
|
|
4070
|
+
})
|
|
4071
|
+
);
|
|
4072
|
+
}
|
|
4073
|
+
return navLocationToWire(r.location);
|
|
4074
|
+
}, { absentable: true });
|
|
4075
|
+
server.register("textDocument/documentHighlight", async (params) => {
|
|
4076
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4077
|
+
const r = await documentHighlight(raw, {
|
|
4078
|
+
filePath: uriToPath(params?.textDocument?.uri ?? ""),
|
|
4079
|
+
line: params?.position?.line ?? 0,
|
|
4080
|
+
character: params?.position?.character ?? 0
|
|
4081
|
+
});
|
|
4082
|
+
if (r.highlights.length === 0) {
|
|
4083
|
+
const tier = r.evidenceTier;
|
|
4084
|
+
const notLaunched = tier === "unsupported_language";
|
|
4085
|
+
throw new DeclaredAbsenceThrow({
|
|
4086
|
+
method: "textDocument/documentHighlight",
|
|
4087
|
+
reason: notLaunched ? "language_not_launched" : "not_indexed",
|
|
4088
|
+
language: languageForMessage(params),
|
|
4089
|
+
detail: notLaunched ? "this file's type is outside the analyzer's routed set, so it was never routed and the index was not searched" : "the index was searched and stores no occurrence of this symbol at the requested position",
|
|
4090
|
+
evidenceTier: tier,
|
|
4091
|
+
searchedIndex: !notLaunched,
|
|
4092
|
+
totalCount: 0
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
return r.highlights.map((h) => ({
|
|
4096
|
+
range: { start: h.start, end: h.end },
|
|
4097
|
+
kind: h.kind,
|
|
4098
|
+
data: { evidenceTier: r.evidenceTier, staleness: r.staleness, symbol: r.symbol }
|
|
4099
|
+
}));
|
|
4100
|
+
}, { absentable: true });
|
|
4101
|
+
server.register("textDocument/references", async (params) => {
|
|
4102
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4103
|
+
const r = await resolveReferences(raw, {
|
|
4104
|
+
filePath: uriToPath(params?.textDocument?.uri ?? ""),
|
|
4105
|
+
line: params?.position?.line ?? 0,
|
|
4106
|
+
character: params?.position?.character ?? 0,
|
|
4107
|
+
includeDeclaration: params?.context?.includeDeclaration ?? false
|
|
4108
|
+
});
|
|
4109
|
+
if (r.locations.length === 0) {
|
|
4110
|
+
throw new DeclaredAbsenceThrow(navigationAbsence("textDocument/references", params, r.evidenceTier));
|
|
4111
|
+
}
|
|
4112
|
+
return r.locations.map(navLocationToWire);
|
|
4113
|
+
}, { absentable: true });
|
|
4114
|
+
server.register("textDocument/documentSymbol", async (params) => {
|
|
4115
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4116
|
+
const r = documentSymbols(raw, uriToPath(params?.textDocument?.uri ?? ""));
|
|
4117
|
+
const tier = r.evidenceTier;
|
|
4118
|
+
const notIndexed = r.notIndexed;
|
|
4119
|
+
const toWire = (e) => ({
|
|
4120
|
+
name: e.name,
|
|
4121
|
+
detail: e.qualifiedName ?? void 0,
|
|
4122
|
+
kind: symbolKindFor(e.symbolType),
|
|
4123
|
+
range: symbolRangeToWire(e.range),
|
|
4124
|
+
selectionRange: symbolRangeToWire(e.selectionRange),
|
|
4125
|
+
children: e.children?.map(toWire),
|
|
4126
|
+
data: { id: e.id, evidenceTier: tier, notIndexed }
|
|
4127
|
+
});
|
|
4128
|
+
return r.symbols.map(toWire);
|
|
4129
|
+
});
|
|
4130
|
+
server.register("workspace/symbol", async (params) => {
|
|
4131
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4132
|
+
const r = workspaceSymbols(raw, {
|
|
4133
|
+
query: params?.query ?? "",
|
|
4134
|
+
limit: params?.limit
|
|
4135
|
+
});
|
|
4136
|
+
return r.symbols.map((s) => ({
|
|
4137
|
+
name: s.name,
|
|
4138
|
+
kind: symbolKindFor(s.symbolType),
|
|
4139
|
+
containerName: s.qualifiedName ?? void 0,
|
|
4140
|
+
location: {
|
|
4141
|
+
uri: `file://${s.path}`,
|
|
4142
|
+
range: {
|
|
4143
|
+
start: { line: s.line, character: 0 },
|
|
4144
|
+
end: { line: s.line, character: 0 }
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
}));
|
|
4148
|
+
});
|
|
4149
|
+
server.register("textDocument/prepareCallHierarchy", async (params) => {
|
|
4150
|
+
const p = requireObject(params, "params");
|
|
4151
|
+
const doc = requireObject(p.textDocument, "params.textDocument");
|
|
4152
|
+
if (typeof doc.uri !== "string" || doc.uri.length === 0) {
|
|
4153
|
+
throw new DeclaredParamsError("bb/callHierarchy: params.textDocument.uri must be a non-empty string");
|
|
4154
|
+
}
|
|
4155
|
+
const pos = requireObject(p.position, "params.position");
|
|
4156
|
+
if (typeof pos.line !== "number" || typeof pos.character !== "number") {
|
|
4157
|
+
throw new DeclaredParamsError("bb/callHierarchy: params.position must carry numeric line and character");
|
|
4158
|
+
}
|
|
4159
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4160
|
+
const r = await prepareCallHierarchy(raw, {
|
|
4161
|
+
filePath: uriToPath(doc.uri),
|
|
4162
|
+
line: pos.line,
|
|
4163
|
+
character: pos.character
|
|
4164
|
+
});
|
|
4165
|
+
if (r.item === null && r.absence) {
|
|
4166
|
+
const code = r.absence.code;
|
|
4167
|
+
throw new DeclaredAbsenceThrow({
|
|
4168
|
+
method: "textDocument/prepareCallHierarchy",
|
|
4169
|
+
reason: absenceReasonFor(code),
|
|
4170
|
+
language: r.absence.language ?? null,
|
|
4171
|
+
detail: r.absence.message,
|
|
4172
|
+
evidenceTier: null,
|
|
4173
|
+
searchedIndex: code !== "language_outside_launch_set" && code !== "file_type_unknown",
|
|
4174
|
+
totalCount: null
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
4177
|
+
return r.item === null ? null : [r.item];
|
|
4178
|
+
}, { absentable: true });
|
|
4179
|
+
server.register("callHierarchy/incomingCalls", async (params) => {
|
|
4180
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4181
|
+
const r = await expandCalls(raw, projectId, {
|
|
4182
|
+
item: requireCallHierarchyItem(params),
|
|
4183
|
+
direction: "incoming"
|
|
4184
|
+
});
|
|
4185
|
+
return r.calls;
|
|
4186
|
+
});
|
|
4187
|
+
server.register("callHierarchy/outgoingCalls", async (params) => {
|
|
4188
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
4189
|
+
const r = await expandCalls(raw, projectId, {
|
|
4190
|
+
item: requireCallHierarchyItem(params),
|
|
4191
|
+
direction: "outgoing"
|
|
4192
|
+
});
|
|
4193
|
+
return r.calls;
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
function requireObject(value, what, shape) {
|
|
4197
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4198
|
+
throw new DeclaredParamsError(
|
|
4199
|
+
`bb/callHierarchy: ${what} must be an object${shape === void 0 ? "" : ` carrying { ${shape} }`}`
|
|
4200
|
+
);
|
|
4201
|
+
}
|
|
4202
|
+
return value;
|
|
4203
|
+
}
|
|
4204
|
+
function requireCallHierarchyItem(params) {
|
|
4205
|
+
const p = requireObject(params, "params");
|
|
4206
|
+
const item = requireObject(p.item, "params.item", "symbolId: string, isExternal: boolean");
|
|
4207
|
+
const data = requireObject(item.data, "params.item.data", "symbolId: string, isExternal: boolean");
|
|
4208
|
+
if (typeof data.symbolId !== "string" || data.symbolId.length === 0) {
|
|
4209
|
+
throw new DeclaredParamsError("bb/callHierarchy: params.item.data.symbolId must be a non-empty string");
|
|
4210
|
+
}
|
|
4211
|
+
if (typeof data.isExternal !== "boolean") {
|
|
4212
|
+
throw new DeclaredParamsError("bb/callHierarchy: params.item.data.isExternal must be a boolean");
|
|
4213
|
+
}
|
|
4214
|
+
return item;
|
|
4215
|
+
}
|
|
4216
|
+
function navigationAbsence(method, params, evidenceTier, extra = {}) {
|
|
4217
|
+
const language = languageForMessage(params);
|
|
4218
|
+
const notLaunched = language === null || !isLaunchLanguage(language);
|
|
4219
|
+
const what = extra.limit ? ` (asked for ${extra.limit})` : "";
|
|
4220
|
+
const which = language === null ? "this file's type is not routed by any analyzer" : `${language} is outside the launch-language set`;
|
|
4221
|
+
return {
|
|
4222
|
+
method,
|
|
4223
|
+
reason: notLaunched ? "language_not_launched" : "not_indexed",
|
|
4224
|
+
language,
|
|
4225
|
+
detail: notLaunched ? `${which}, so the index was never searched for this position${what}` : `the index was searched and stores no matching symbol at this position${what}`,
|
|
4226
|
+
evidenceTier,
|
|
4227
|
+
searchedIndex: !notLaunched,
|
|
4228
|
+
totalCount: null
|
|
4229
|
+
};
|
|
4230
|
+
}
|
|
4231
|
+
function navLocationToWire(loc) {
|
|
4232
|
+
return {
|
|
4233
|
+
uri: `file://${loc.path}`,
|
|
4234
|
+
range: {
|
|
4235
|
+
start: { line: loc.line, character: loc.character },
|
|
4236
|
+
end: { line: loc.endLine, character: loc.endCharacter }
|
|
4237
|
+
}
|
|
4238
|
+
};
|
|
4239
|
+
}
|
|
4240
|
+
function symbolRangeToWire(r) {
|
|
4241
|
+
return {
|
|
4242
|
+
start: { line: r.line, character: r.character },
|
|
4243
|
+
end: { line: r.endLine, character: r.endCharacter }
|
|
4244
|
+
};
|
|
4245
|
+
}
|
|
4246
|
+
var isMain = typeof globalThis.Bun !== "undefined" ? import.meta.main === true : process.argv[1] !== void 0 && // Windows argv[1] uses backslashes (…\dist\lsp\server.js) — normalize
|
|
4247
|
+
// so the entry check is separator-independent (found by the E7 win32
|
|
4248
|
+
// CI leg: the server silently exited 0 without starting, 2026-09-05).
|
|
4249
|
+
(process.argv[1].replace(/\\/g, "/").endsWith("lsp/server.js") || process.argv[1].replace(/\\/g, "/").endsWith("lsp/server.ts"));
|
|
4250
|
+
if (isMain) {
|
|
4251
|
+
const dirIdx = process.argv.indexOf("--dir");
|
|
4252
|
+
const root = dirIdx !== -1 ? process.argv[dirIdx + 1] : process.cwd();
|
|
4253
|
+
if (process.env.BB_LSP_STDERR_LOG) {
|
|
4254
|
+
const trace = (what, err) => {
|
|
4255
|
+
try {
|
|
4256
|
+
appendFileSync(process.env.BB_LSP_STDERR_LOG, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${what}: ${err?.stack ?? String(err)}
|
|
4257
|
+
`);
|
|
4258
|
+
} catch {
|
|
4259
|
+
}
|
|
4260
|
+
};
|
|
4261
|
+
process.on("uncaughtException", (e) => trace("uncaughtException", e));
|
|
4262
|
+
process.on("unhandledRejection", (e) => trace("unhandledRejection", e));
|
|
4263
|
+
}
|
|
4264
|
+
const server = createServer(root);
|
|
4265
|
+
wireFeatures(server);
|
|
4266
|
+
server.run().catch((error) => {
|
|
4267
|
+
console.error("bb_lsp failed:", error?.message ?? String(error));
|
|
4268
|
+
if (process.env.BB_LSP_STDERR_LOG) {
|
|
4269
|
+
try {
|
|
4270
|
+
appendFileSync(process.env.BB_LSP_STDERR_LOG, `[${(/* @__PURE__ */ new Date()).toISOString()}] run-failed: ${error?.stack ?? String(error)}
|
|
4271
|
+
`);
|
|
4272
|
+
} catch {
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
process.exit(1);
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
export {
|
|
4279
|
+
ABSENCE_LOG_TYPE,
|
|
4280
|
+
ABSENCE_METHOD,
|
|
4281
|
+
ABSENCE_SCHEMA_VERSION,
|
|
4282
|
+
BbLspServer,
|
|
4283
|
+
CAPABILITY_COVERAGE,
|
|
4284
|
+
DeclaredAbsenceThrow,
|
|
4285
|
+
DeclaredParamsError,
|
|
4286
|
+
DocumentUnroutable,
|
|
4287
|
+
ErrorCodes,
|
|
4288
|
+
LAUNCH_LANGUAGES,
|
|
4289
|
+
STALENESS_SCHEMA_VERSION,
|
|
4290
|
+
absenceProviderPayload,
|
|
4291
|
+
asDocumentUnroutable,
|
|
4292
|
+
capabilityKeyForMethod,
|
|
4293
|
+
createServer,
|
|
4294
|
+
impactProviderPayload,
|
|
4295
|
+
isLaunchLanguage,
|
|
4296
|
+
languageForPath,
|
|
4297
|
+
stalenessProviderPayload,
|
|
4298
|
+
uriToPath,
|
|
4299
|
+
wireFeatures,
|
|
4300
|
+
workspaceProviderPayload
|
|
4301
|
+
};
|