@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,1479 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reindexFilesParallel
|
|
3
|
+
} from "./chunk-PJTNGD3Z.js";
|
|
4
|
+
import {
|
|
5
|
+
assertFreshFiles,
|
|
6
|
+
extractIdentifierOccurrences,
|
|
7
|
+
parseForSafety
|
|
8
|
+
} from "./chunk-YI6FF2ZR.js";
|
|
9
|
+
import {
|
|
10
|
+
checkSymbolFreshness,
|
|
11
|
+
fileUnchangedSince
|
|
12
|
+
} from "./chunk-QLK6ED4Q.js";
|
|
13
|
+
import {
|
|
14
|
+
BrainDatabase
|
|
15
|
+
} from "./chunk-KLK3PBTO.js";
|
|
16
|
+
import {
|
|
17
|
+
ASTEngine
|
|
18
|
+
} from "./chunk-V44PGPA6.js";
|
|
19
|
+
import {
|
|
20
|
+
bbDataDir
|
|
21
|
+
} from "./chunk-2BSKD7O3.js";
|
|
22
|
+
import {
|
|
23
|
+
EXTENSION_LANGUAGE
|
|
24
|
+
} from "./chunk-KCDECRJY.js";
|
|
25
|
+
|
|
26
|
+
// src/intelligence/edit/edit-engine.ts
|
|
27
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync, rmSync, writeFileSync as writeFileSync2, statSync, chmodSync } from "fs";
|
|
28
|
+
import { createHash } from "crypto";
|
|
29
|
+
import path2 from "path";
|
|
30
|
+
import { randomUUID } from "crypto";
|
|
31
|
+
|
|
32
|
+
// src/ast/brain-surgical-patcher.ts
|
|
33
|
+
import * as fs from "fs";
|
|
34
|
+
import { execFile } from "child_process";
|
|
35
|
+
import * as path from "path";
|
|
36
|
+
var FORMAT_TIMEOUT_MS = 1500;
|
|
37
|
+
var BrainSurgicalPatcher = class _BrainSurgicalPatcher {
|
|
38
|
+
static async patch(absolutePath, startIndex, endIndex, newContent, isDelete = false, format = true) {
|
|
39
|
+
if (!fs.existsSync(absolutePath)) {
|
|
40
|
+
throw new Error(`[BrainSurgicalPatcher] File not found: ${absolutePath}`);
|
|
41
|
+
}
|
|
42
|
+
if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) {
|
|
43
|
+
throw new Error(`[BrainSurgicalPatcher] Invalid bounds: [${startIndex}, ${endIndex}]`);
|
|
44
|
+
}
|
|
45
|
+
const originalContent = fs.readFileSync(absolutePath, "utf8");
|
|
46
|
+
if (endIndex > originalContent.length) {
|
|
47
|
+
throw new Error(`[BrainSurgicalPatcher] endIndex (${endIndex}) exceeds file length (${originalContent.length})`);
|
|
48
|
+
}
|
|
49
|
+
let actualStart = startIndex;
|
|
50
|
+
let actualEnd = endIndex;
|
|
51
|
+
let finalContent = newContent;
|
|
52
|
+
if (isDelete) {
|
|
53
|
+
while (actualStart > 0) {
|
|
54
|
+
const char = originalContent[actualStart - 1];
|
|
55
|
+
if (char === " " || char === " ") {
|
|
56
|
+
actualStart--;
|
|
57
|
+
} else {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const isAtLineStart = actualStart === 0 || originalContent[actualStart - 1] === "\n";
|
|
62
|
+
if (isAtLineStart) {
|
|
63
|
+
if (actualEnd < originalContent.length && originalContent[actualEnd] === "\r") {
|
|
64
|
+
actualEnd++;
|
|
65
|
+
}
|
|
66
|
+
if (actualEnd < originalContent.length && originalContent[actualEnd] === "\n") {
|
|
67
|
+
actualEnd++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
let baseIndent = "";
|
|
72
|
+
let tempStart = startIndex;
|
|
73
|
+
while (tempStart > 0) {
|
|
74
|
+
const char = originalContent[tempStart - 1];
|
|
75
|
+
if (char === "\n") break;
|
|
76
|
+
if (char === " " || char === " ") {
|
|
77
|
+
baseIndent = char + baseIndent;
|
|
78
|
+
} else {
|
|
79
|
+
baseIndent = "";
|
|
80
|
+
}
|
|
81
|
+
tempStart--;
|
|
82
|
+
}
|
|
83
|
+
if (baseIndent.length > 0 && finalContent.includes("\n")) {
|
|
84
|
+
const lines = finalContent.split("\n");
|
|
85
|
+
const secondLine = lines.length > 1 ? lines[1] : "";
|
|
86
|
+
const hasOwnIndent = secondLine.startsWith(" ") || secondLine.startsWith(" ");
|
|
87
|
+
if (!hasOwnIndent && secondLine.trim().length > 0) {
|
|
88
|
+
for (let i = 1; i < lines.length; i++) {
|
|
89
|
+
if (lines[i].trim().length > 0) {
|
|
90
|
+
lines[i] = baseIndent + lines[i];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finalContent = lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const before = originalContent.substring(0, actualStart);
|
|
98
|
+
const after = originalContent.substring(actualEnd);
|
|
99
|
+
const patchedContent = before + finalContent + after;
|
|
100
|
+
fs.writeFileSync(absolutePath, patchedContent, "utf8");
|
|
101
|
+
if (format) {
|
|
102
|
+
void _BrainSurgicalPatcher.tryAutoFormat(absolutePath);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* D5: the extension → formatter mapping, as data rather than a built shell string.
|
|
108
|
+
*
|
|
109
|
+
* The invocation is argv, never a command line that interpolates the path:
|
|
110
|
+
* `exec(\`npx prettier --write "${absolutePath}"\`)`. Two consequences, both
|
|
111
|
+
* measured before the rewrite (script/m71/probe-format-truth.ts):
|
|
112
|
+
*
|
|
113
|
+
* 1. `npx` without a local install is an INSTALL attempt. On a machine with no
|
|
114
|
+
* prettier it hit the network and was killed by the 1500ms timeout —
|
|
115
|
+
* 1573ms of wall time per edited file, on a tool whose performance claim is
|
|
116
|
+
* "only affected files are written", with the result saying
|
|
117
|
+
* `formatted: true` regardless.
|
|
118
|
+
* 2. A filename is not trusted input. `resolveSafeEditPath` validates
|
|
119
|
+
* containment, not characters, so a file NAMED `inj$(touch pwned).ts` is a
|
|
120
|
+
* legal in-project path — and the shell expanded the substitution. The
|
|
121
|
+
* marker file appeared; that is command execution triggered by the name of
|
|
122
|
+
* a file being formatted, through the public edit path.
|
|
123
|
+
*
|
|
124
|
+
* So the binary is resolved explicitly (nearest node_modules/.bin walking up from
|
|
125
|
+
* the file, then PATH) and invoked with execFile: an argv array, no shell, and
|
|
126
|
+
* nothing in the path can be expanded.
|
|
127
|
+
*/
|
|
128
|
+
static FORMATTERS = [
|
|
129
|
+
{ ext: [".ts", ".tsx", ".js", ".jsx", ".json", ".html", ".css", ".md"], candidates: [{ name: "prettier", args: ["--write"] }] },
|
|
130
|
+
{ ext: [".rs"], candidates: [{ name: "rustfmt", args: [] }] },
|
|
131
|
+
{ ext: [".go"], candidates: [{ name: "gofmt", args: ["-w"] }] },
|
|
132
|
+
{ ext: [".c", ".cpp", ".h", ".hpp"], candidates: [{ name: "clang-format", args: ["-i"] }] },
|
|
133
|
+
{ ext: [".py"], candidates: [{ name: "black", args: [] }, { name: "ruff", args: ["format"] }] }
|
|
134
|
+
];
|
|
135
|
+
/** Executable `<name>` in the nearest node_modules/.bin at or above `startDir`. */
|
|
136
|
+
static resolveInNodeModules(startDir, name) {
|
|
137
|
+
let dir = startDir;
|
|
138
|
+
for (; ; ) {
|
|
139
|
+
const candidate = path.join(dir, "node_modules", ".bin", name);
|
|
140
|
+
try {
|
|
141
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
142
|
+
return candidate;
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
const parent = path.dirname(dir);
|
|
146
|
+
if (parent === dir) return null;
|
|
147
|
+
dir = parent;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Executable `<name>` on PATH. */
|
|
151
|
+
static resolveOnPath(name) {
|
|
152
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
153
|
+
if (!dir) continue;
|
|
154
|
+
const candidate = path.join(dir, name);
|
|
155
|
+
try {
|
|
156
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
157
|
+
return candidate;
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
static async tryAutoFormat(absolutePath) {
|
|
164
|
+
const ext = path.extname(absolutePath);
|
|
165
|
+
const mapping = _BrainSurgicalPatcher.FORMATTERS.find((m) => m.ext.includes(ext));
|
|
166
|
+
if (!mapping) return { formatter: null, ran: false, ok: false, reason: `no formatter mapped for ${ext}` };
|
|
167
|
+
const names = mapping.candidates.map((candidate) => candidate.name);
|
|
168
|
+
let endOfLine = [];
|
|
169
|
+
try {
|
|
170
|
+
const head = fs.readFileSync(absolutePath).subarray(0, 4096).toString("utf8");
|
|
171
|
+
const crlf = (head.match(/\r\n/g) ?? []).length;
|
|
172
|
+
const lf = (head.match(/(?<!\r)\n/g) ?? []).length;
|
|
173
|
+
if (crlf > lf || crlf > 0 && lf === 0) endOfLine = ["--end-of-line", "crlf"];
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
let firstFailure = null;
|
|
177
|
+
for (const candidate of mapping.candidates) {
|
|
178
|
+
const bin = _BrainSurgicalPatcher.resolveInNodeModules(path.dirname(absolutePath), candidate.name) ?? _BrainSurgicalPatcher.resolveOnPath(candidate.name);
|
|
179
|
+
if (!bin) continue;
|
|
180
|
+
const args = [...candidate.args, ...candidate.name === "prettier" ? endOfLine : [], absolutePath];
|
|
181
|
+
const outcome = await new Promise((resolve) => {
|
|
182
|
+
execFile(bin, args, { cwd: path.dirname(absolutePath), timeout: FORMAT_TIMEOUT_MS, windowsHide: true }, (error, _stdout, stderr) => {
|
|
183
|
+
if (!error) return resolve({ formatter: candidate.name, ran: true, ok: true });
|
|
184
|
+
if (error.killed || error.signal) {
|
|
185
|
+
return resolve({ formatter: candidate.name, ran: true, ok: false, reason: `${candidate.name} timed out after ${FORMAT_TIMEOUT_MS}ms` });
|
|
186
|
+
}
|
|
187
|
+
const code = error.code;
|
|
188
|
+
const detail = String(stderr ?? "").split("\n").map((line) => line.trim()).filter(Boolean)[0] ?? error.message;
|
|
189
|
+
const how = typeof code === "number" ? `exited ${code}` : code === "ENOENT" ? "could not be started" : "failed";
|
|
190
|
+
resolve({ formatter: candidate.name, ran: true, ok: false, reason: `${candidate.name} ${how}: ${detail.slice(0, 160)}` });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
if (outcome.ok) return outcome;
|
|
194
|
+
if (!firstFailure) firstFailure = outcome;
|
|
195
|
+
}
|
|
196
|
+
return firstFailure ?? {
|
|
197
|
+
formatter: names[0] ?? null,
|
|
198
|
+
ran: false,
|
|
199
|
+
ok: false,
|
|
200
|
+
reason: `no formatter found: tried ${names.join(", ")} in node_modules/.bin (walking up from the file) and on PATH`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/ast/import-pruner.ts
|
|
206
|
+
var PRUNABLE_LANGUAGES = Object.freeze(["typescript", "javascript", "tsx", "jsx"]);
|
|
207
|
+
var ImportPruner = class _ImportPruner {
|
|
208
|
+
/**
|
|
209
|
+
* Remove import bindings that the `delete` made unreferenced, and report what
|
|
210
|
+
* was and was not attempted so the delete result can declare it.
|
|
211
|
+
*
|
|
212
|
+
* A file this module cannot parse, or whose language is outside
|
|
213
|
+
* PRUNABLE_LANGUAGES, is returned EXACTLY as given: a leftover import never
|
|
214
|
+
* breaks a build, and an import removed on a guess does. When pruning is not
|
|
215
|
+
* attempted the report says so and names the language; the edit is correct
|
|
216
|
+
* either way, and the caller is never left to assume cleanup happened.
|
|
217
|
+
*/
|
|
218
|
+
static async pruneWithReport(fileContent, filePath) {
|
|
219
|
+
const language = ASTEngine.detectLanguage(filePath);
|
|
220
|
+
if (!language) {
|
|
221
|
+
return { attempted: false, language: null, removedBindings: [], reason: "the file's language is not recognised, so no import syntax could be read", content: fileContent };
|
|
222
|
+
}
|
|
223
|
+
if (!PRUNABLE_LANGUAGES.includes(language)) {
|
|
224
|
+
return {
|
|
225
|
+
attempted: false,
|
|
226
|
+
language,
|
|
227
|
+
removedBindings: [],
|
|
228
|
+
reason: `import pruning is implemented for TypeScript/JavaScript only; ${language} imports were left unchanged rather than pattern-matched`,
|
|
229
|
+
content: fileContent
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
let tree = null;
|
|
233
|
+
try {
|
|
234
|
+
const parsed = await ASTEngine.parseTree(filePath, fileContent);
|
|
235
|
+
tree = parsed.tree;
|
|
236
|
+
const occurrences = extractIdentifierOccurrences(tree, fileContent);
|
|
237
|
+
const statements = _ImportPruner.importStatements(tree);
|
|
238
|
+
if (statements.length === 0) {
|
|
239
|
+
return { attempted: true, language, removedBindings: [], content: fileContent };
|
|
240
|
+
}
|
|
241
|
+
const isInsideImport = (index) => statements.some((s) => index >= s.start && index < s.end);
|
|
242
|
+
const usedOutsideImports = new Set(occurrences.filter((o) => !isInsideImport(o.startIndex)).map((o) => o.text));
|
|
243
|
+
const removedBindings = _ImportPruner.unusedBindings(statements, usedOutsideImports);
|
|
244
|
+
return { attempted: true, language, removedBindings, content: _ImportPruner.rewrite(fileContent, statements, usedOutsideImports) };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return {
|
|
247
|
+
attempted: false,
|
|
248
|
+
language,
|
|
249
|
+
removedBindings: [],
|
|
250
|
+
reason: `the file could not be parsed for import pruning (${error?.message ?? String(error)}), so its imports were left unchanged`,
|
|
251
|
+
content: fileContent
|
|
252
|
+
};
|
|
253
|
+
} finally {
|
|
254
|
+
try {
|
|
255
|
+
if (tree && typeof tree.delete === "function") tree.delete();
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Remove import bindings that the `delete` made unreferenced. */
|
|
261
|
+
static async prune(fileContent, filePath) {
|
|
262
|
+
return (await _ImportPruner.pruneWithReport(fileContent, filePath)).content ?? fileContent;
|
|
263
|
+
}
|
|
264
|
+
/** Local names of the bindings no live occurrence refers to. */
|
|
265
|
+
static unusedBindings(statements, used) {
|
|
266
|
+
const out = [];
|
|
267
|
+
for (const statement of statements) {
|
|
268
|
+
for (const binding of statement.bindings) if (!used.has(binding.localName)) out.push(binding.localName);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
/** Every top-level import statement with its bindings, in source order. */
|
|
273
|
+
static importStatements(tree) {
|
|
274
|
+
const out = [];
|
|
275
|
+
const children = tree?.rootNode?.children ?? [];
|
|
276
|
+
for (const node of children) {
|
|
277
|
+
if (node?.type !== "import_statement") continue;
|
|
278
|
+
const clause = (node.children ?? []).find((c) => c.type === "import_clause");
|
|
279
|
+
const bindings = [];
|
|
280
|
+
if (clause) {
|
|
281
|
+
for (const child of clause.children ?? []) {
|
|
282
|
+
if (child.type === "identifier") {
|
|
283
|
+
bindings.push({ localName: child.text, start: child.startIndex, end: child.endIndex, kind: "default" });
|
|
284
|
+
} else if (child.type === "named_imports") {
|
|
285
|
+
for (const spec of child.children ?? []) {
|
|
286
|
+
if (spec.type !== "import_specifier") continue;
|
|
287
|
+
const identifiers = (spec.children ?? []).filter((c) => c.type === "identifier");
|
|
288
|
+
const local = identifiers.length > 1 ? identifiers[identifiers.length - 1] : identifiers[0];
|
|
289
|
+
if (!local) continue;
|
|
290
|
+
bindings.push({ localName: local.text, start: spec.startIndex, end: spec.endIndex, kind: "named" });
|
|
291
|
+
}
|
|
292
|
+
} else if (child.type === "namespace_import") {
|
|
293
|
+
const id = (child.children ?? []).find((c) => c.type === "identifier");
|
|
294
|
+
if (id) bindings.push({ localName: id.text, start: child.startIndex, end: child.endIndex, kind: "namespace" });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
out.push({ start: node.startIndex, end: node.endIndex, bindings });
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
/** Apply statement/specifier removals bottom-up so byte offsets stay valid. */
|
|
303
|
+
static rewrite(content, statements, used) {
|
|
304
|
+
const removals = [];
|
|
305
|
+
for (const statement of statements) {
|
|
306
|
+
if (statement.bindings.length === 0) continue;
|
|
307
|
+
const unused = statement.bindings.filter((binding) => !used.has(binding.localName));
|
|
308
|
+
if (unused.length === 0) continue;
|
|
309
|
+
if (unused.length === statement.bindings.length) {
|
|
310
|
+
removals.push({ start: statement.start, end: _ImportPruner.throughTrailingNewline(content, statement.end) });
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
for (const binding of unused) {
|
|
314
|
+
if (binding.kind !== "named") continue;
|
|
315
|
+
const range = _ImportPruner.specifierRemoval(content, binding.start, binding.end);
|
|
316
|
+
if (range) removals.push(range);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
let result = content;
|
|
320
|
+
for (const range of removals.sort((a, b) => b.start - a.start)) {
|
|
321
|
+
result = result.slice(0, range.start) + result.slice(range.end);
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
/** End offset widened over a trailing newline so a removed statement leaves no blank line. */
|
|
326
|
+
static throughTrailingNewline(content, end) {
|
|
327
|
+
let cursor = end;
|
|
328
|
+
if (content[cursor] === "\r") cursor++;
|
|
329
|
+
if (content[cursor] === "\n") cursor++;
|
|
330
|
+
return cursor;
|
|
331
|
+
}
|
|
332
|
+
/** The range to delete for one named specifier, eating one adjacent comma. */
|
|
333
|
+
static specifierRemoval(content, start, end) {
|
|
334
|
+
let from = start;
|
|
335
|
+
const to = end;
|
|
336
|
+
let cursor = to;
|
|
337
|
+
while (cursor < content.length && (content[cursor] === " " || content[cursor] === " ")) cursor++;
|
|
338
|
+
if (content[cursor] === ",") {
|
|
339
|
+
cursor++;
|
|
340
|
+
while (cursor < content.length && (content[cursor] === " " || content[cursor] === " ")) cursor++;
|
|
341
|
+
return { start: from, end: cursor };
|
|
342
|
+
}
|
|
343
|
+
let back = from;
|
|
344
|
+
while (back > 0 && (content[back - 1] === " " || content[back - 1] === " ")) back--;
|
|
345
|
+
if (back > 0 && content[back - 1] === ",") {
|
|
346
|
+
back--;
|
|
347
|
+
while (back > 0 && (content[back - 1] === " " || content[back - 1] === " ")) back--;
|
|
348
|
+
return { start: back, end: to };
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// src/intelligence/edit/edit-engine.ts
|
|
355
|
+
function inside(root, candidate) {
|
|
356
|
+
const relative = path2.relative(root, candidate);
|
|
357
|
+
return relative === "" || !relative.startsWith(".." + path2.sep) && relative !== ".." && !path2.isAbsolute(relative);
|
|
358
|
+
}
|
|
359
|
+
function nearestExisting(candidate) {
|
|
360
|
+
let cursor = candidate;
|
|
361
|
+
while (!existsSync2(cursor)) {
|
|
362
|
+
const parent = path2.dirname(cursor);
|
|
363
|
+
if (parent === cursor) break;
|
|
364
|
+
cursor = parent;
|
|
365
|
+
}
|
|
366
|
+
return cursor;
|
|
367
|
+
}
|
|
368
|
+
function resolveSafeEditPath(projectPath, requested) {
|
|
369
|
+
const root = realpathSync(projectPath);
|
|
370
|
+
const raw = path2.isAbsolute(requested) ? requested : path2.resolve(root, requested);
|
|
371
|
+
const existing = nearestExisting(raw);
|
|
372
|
+
const realExisting = realpathSync(existing);
|
|
373
|
+
if (!inside(root, realExisting)) {
|
|
374
|
+
const viaSymlink = realExisting !== existing;
|
|
375
|
+
throw new Error(
|
|
376
|
+
`path escapes project root${viaSymlink ? " through symlink" : ""}: ${requested}${raw === existing ? "" : ` (via ${existing})`}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (raw !== existing) {
|
|
380
|
+
const tail = path2.relative(existing, raw);
|
|
381
|
+
const target = tail ? path2.join(realExisting, tail) : realExisting;
|
|
382
|
+
if (!inside(root, target)) throw new Error(`path escapes project root through symlink: ${requested}`);
|
|
383
|
+
assertWritableTarget(root, target, requested);
|
|
384
|
+
return target;
|
|
385
|
+
}
|
|
386
|
+
assertWritableTarget(root, realExisting, requested);
|
|
387
|
+
return realExisting;
|
|
388
|
+
}
|
|
389
|
+
var PROTECTED_SEGMENTS = /* @__PURE__ */ new Set([".git", ".hg", ".svn", "node_modules", ".bb-data"]);
|
|
390
|
+
function assertWritableTarget(root, target, requested) {
|
|
391
|
+
const relative = path2.relative(root, target);
|
|
392
|
+
const protectedSegment = relative.split(path2.sep).find((segment) => PROTECTED_SEGMENTS.has(segment));
|
|
393
|
+
if (protectedSegment) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
`refused: ${requested} is inside a protected path (${protectedSegment}/) \u2014 bb_edit does not write version-control, dependency, or tool-state directories; choose a source path instead`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
let dataDir = null;
|
|
399
|
+
try {
|
|
400
|
+
dataDir = bbDataDir();
|
|
401
|
+
} catch {
|
|
402
|
+
dataDir = null;
|
|
403
|
+
}
|
|
404
|
+
if (dataDir && inside(dataDir, target)) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`refused: ${requested} is inside the index data directory (${dataDir}) \u2014 bb_edit never edits index state as source; choose a source path instead`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function replaceRange(text, start, end, replacement) {
|
|
411
|
+
if (start < 0 || end < start || end > text.length) throw new Error(`invalid edit bounds: ${start}-${end}`);
|
|
412
|
+
return text.slice(0, start) + replacement + text.slice(end);
|
|
413
|
+
}
|
|
414
|
+
function dominantNewline(text) {
|
|
415
|
+
let crlf = 0;
|
|
416
|
+
let lf = 0;
|
|
417
|
+
for (let i = 0; i < text.length; i++) {
|
|
418
|
+
if (text[i] !== "\n") continue;
|
|
419
|
+
if (i > 0 && text[i - 1] === "\r") crlf++;
|
|
420
|
+
else lf++;
|
|
421
|
+
}
|
|
422
|
+
return crlf > lf ? "\r\n" : "\n";
|
|
423
|
+
}
|
|
424
|
+
function withNewline(text, nl) {
|
|
425
|
+
return nl === "\n" ? text : text.replace(/\r?\n/g, nl);
|
|
426
|
+
}
|
|
427
|
+
function hasLoneLf(text) {
|
|
428
|
+
for (let i = 0; i < text.length; i++) {
|
|
429
|
+
if (text[i] !== "\n") continue;
|
|
430
|
+
if (i === 0 || text[i - 1] !== "\r") return true;
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
function snapshotOf(file) {
|
|
435
|
+
if (!existsSync2(file)) return { existed: false, bytes: null, mode: null, hash: null };
|
|
436
|
+
const bytes = readFileSync2(file);
|
|
437
|
+
let mode = null;
|
|
438
|
+
try {
|
|
439
|
+
mode = statSync(file).mode & 511;
|
|
440
|
+
} catch {
|
|
441
|
+
mode = null;
|
|
442
|
+
}
|
|
443
|
+
return { existed: true, bytes, mode, hash: createHash("sha256").update(bytes).digest("hex") };
|
|
444
|
+
}
|
|
445
|
+
function fileHash(file) {
|
|
446
|
+
try {
|
|
447
|
+
if (!existsSync2(file)) return null;
|
|
448
|
+
return createHash("sha256").update(readFileSync2(file)).digest("hex");
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function preImageMismatches(projectPath, planned, current) {
|
|
454
|
+
const moved = [];
|
|
455
|
+
for (const path0 of Object.keys(planned)) {
|
|
456
|
+
const want = planned[path0];
|
|
457
|
+
const got = current.get(path0) ?? null;
|
|
458
|
+
if (want === got) continue;
|
|
459
|
+
const label = relativeTo(projectPath, path0);
|
|
460
|
+
moved.push(`${label} (planned ${want ? want.slice(0, 12) : "no file at that path"}, now ${got ? got.slice(0, 12) : "gone"})`);
|
|
461
|
+
}
|
|
462
|
+
return moved;
|
|
463
|
+
}
|
|
464
|
+
function writeBytesPreservingMode(file, bytes, mode) {
|
|
465
|
+
writeFileSync2(file, bytes);
|
|
466
|
+
if (mode !== null) {
|
|
467
|
+
try {
|
|
468
|
+
chmodSync(file, mode);
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
var WRITE_GUARD = {
|
|
474
|
+
/** A replacement that removes fewer lines than this is never questioned. */
|
|
475
|
+
minRemovedLines: 5,
|
|
476
|
+
/** …and it must also leave no more than this share of the pre-image's lines. */
|
|
477
|
+
maxRemainingRatio: 0.5
|
|
478
|
+
};
|
|
479
|
+
function lineCount(text) {
|
|
480
|
+
if (text.length === 0) return 0;
|
|
481
|
+
const end = text.endsWith("\n") ? text.length - 1 : text.length;
|
|
482
|
+
return text.slice(0, end).split("\n").length;
|
|
483
|
+
}
|
|
484
|
+
function writeGuardMessage(relPath, target, overwrite) {
|
|
485
|
+
if (!target.existed) return null;
|
|
486
|
+
if (overwrite === false) {
|
|
487
|
+
return `write refused: ${relPath} already exists and overwrite is false (create-only) \u2014 pass overwrite: true to replace it, or use update/insert to change it in place.`;
|
|
488
|
+
}
|
|
489
|
+
if (overwrite === true) return null;
|
|
490
|
+
const beforeLines = lineCount(target.before);
|
|
491
|
+
const afterLines = lineCount(target.after);
|
|
492
|
+
const removed = beforeLines - afterLines;
|
|
493
|
+
if (removed < WRITE_GUARD.minRemovedLines) return null;
|
|
494
|
+
if (afterLines > beforeLines * WRITE_GUARD.maxRemainingRatio) return null;
|
|
495
|
+
const percent = Math.round(removed / beforeLines * 100);
|
|
496
|
+
return `write refused: it would replace ${relPath} (${beforeLines} lines) with ${afterLines} line(s), removing ${removed} lines (${percent}%) \u2014 a write that removes at least half of a file's lines (and at least ${WRITE_GUARD.minRemovedLines}) is treated as a probable mistake. Pass overwrite: true to replace it anyway, or use update/insert to change part of the file.`;
|
|
497
|
+
}
|
|
498
|
+
function findSymbolBody(parsed, start, end) {
|
|
499
|
+
const candidates = parsed.bodyRanges.filter((b) => b.startIndex >= start && b.endIndex <= end);
|
|
500
|
+
if (candidates.length === 0) return null;
|
|
501
|
+
candidates.sort((a, b) => a.startIndex - b.startIndex || b.endIndex - a.endIndex);
|
|
502
|
+
return candidates[0];
|
|
503
|
+
}
|
|
504
|
+
function lineIndent(text, pos) {
|
|
505
|
+
let lineStart = pos;
|
|
506
|
+
while (lineStart > 0 && text[lineStart - 1] !== "\n") lineStart--;
|
|
507
|
+
return text.slice(lineStart, pos);
|
|
508
|
+
}
|
|
509
|
+
function indentLines(text, indent, includeFirst) {
|
|
510
|
+
return text.split("\n").map((line, i) => i === 0 && !includeFirst || line.length === 0 ? line : indent + line).join("\n");
|
|
511
|
+
}
|
|
512
|
+
function lineLeadingWhitespace(text, pos) {
|
|
513
|
+
let lineStart = pos;
|
|
514
|
+
while (lineStart > 0 && text[lineStart - 1] !== "\n") lineStart--;
|
|
515
|
+
let i = lineStart;
|
|
516
|
+
while (i < pos && (text[i] === " " || text[i] === " ")) i++;
|
|
517
|
+
return text.slice(lineStart, i);
|
|
518
|
+
}
|
|
519
|
+
function indentUnit(text) {
|
|
520
|
+
const lines = text.split("\n").slice(0, 200);
|
|
521
|
+
let min = "";
|
|
522
|
+
for (const line of lines) {
|
|
523
|
+
const m = /^[\t ]+/.exec(line);
|
|
524
|
+
if (!m) continue;
|
|
525
|
+
if (m[0].includes(" ") && !m[0].includes(" ")) return " ";
|
|
526
|
+
if (min === "" || m[0].length < min.length) min = m[0];
|
|
527
|
+
}
|
|
528
|
+
if (min.length >= 4) return " ";
|
|
529
|
+
return " ";
|
|
530
|
+
}
|
|
531
|
+
function computeInnerIndent(text, body) {
|
|
532
|
+
const open = body.startIndex;
|
|
533
|
+
const end = body.endIndex;
|
|
534
|
+
let i = open + 1;
|
|
535
|
+
while (i < end && (text[i] === " " || text[i] === " " || text[i] === "\r" || text[i] === "\n")) i++;
|
|
536
|
+
if (i >= end) return lineLeadingWhitespace(text, open) + indentUnit(text);
|
|
537
|
+
if (!text.slice(open, i).includes("\n")) return lineLeadingWhitespace(text, open) + indentUnit(text);
|
|
538
|
+
return lineLeadingWhitespace(text, i);
|
|
539
|
+
}
|
|
540
|
+
var FALSE_POSITIVE_BINDING_TYPES = /* @__PURE__ */ new Set([
|
|
541
|
+
"field_expression",
|
|
542
|
+
// Rust, C/C++, Scala, Zig — t.includes("field") matches
|
|
543
|
+
"field_access"
|
|
544
|
+
// Java — t.includes("field") matches
|
|
545
|
+
]);
|
|
546
|
+
function isBindingParentType(type) {
|
|
547
|
+
const t = type.toLowerCase();
|
|
548
|
+
if (FALSE_POSITIVE_BINDING_TYPES.has(t)) return false;
|
|
549
|
+
return t.includes("declarator") || // variable_declarator, function_declarator (C/C++)
|
|
550
|
+
t.includes("declaration") || // function/class/method/variable/field_declaration
|
|
551
|
+
t.includes("definition") || // function/class/method/field_definition
|
|
552
|
+
t.includes("parameter") || // formal/required/typed/default parameter
|
|
553
|
+
t.includes("field") || // field_declaration / field_definition / record_field
|
|
554
|
+
t.includes("property") || // property_signature / property_declaration
|
|
555
|
+
t.includes("signature") || // function_signature (Dart), method/property_signature (TS)
|
|
556
|
+
t === "method" || // Ruby def name
|
|
557
|
+
t === "class" || // Ruby class/module name (bare 'class' node)
|
|
558
|
+
t === "bind" || // Haskell top-level binding (function/pattern)
|
|
559
|
+
t.endsWith("_item") || // Rust function/struct/enum/trait/impl/mod/type/const/static_item
|
|
560
|
+
t.endsWith("specifier") || // C/C++ class/struct/union/enum_specifier
|
|
561
|
+
t.endsWith("_spec") || // Go type_spec / import_spec
|
|
562
|
+
t === "type_name" || // F# type name container
|
|
563
|
+
t === "assignment" || // Python `x = …`
|
|
564
|
+
t === "pair" || // JS/TS object key { target: … }
|
|
565
|
+
t.includes("import") || // import_specifier
|
|
566
|
+
t === "catch_clause" || // catch (target)
|
|
567
|
+
t.includes("for_in") || // for (target in …)
|
|
568
|
+
t.includes("for_of") || // for (target of …)
|
|
569
|
+
t.includes("shorthand");
|
|
570
|
+
}
|
|
571
|
+
var VALUE_SIDE_BINDING_PARENTS = /* @__PURE__ */ new Set([
|
|
572
|
+
"assignment",
|
|
573
|
+
"augmented_assignment",
|
|
574
|
+
"variable_declarator",
|
|
575
|
+
"pair",
|
|
576
|
+
"var_spec",
|
|
577
|
+
"init_declarator",
|
|
578
|
+
"field_declaration"
|
|
579
|
+
]);
|
|
580
|
+
function isReferenceSite(o, _relType) {
|
|
581
|
+
if (o.isMemberName) return false;
|
|
582
|
+
if (isBindingParentType(o.parentType)) {
|
|
583
|
+
if (VALUE_SIDE_BINDING_PARENTS.has(o.parentType) && (o.parentField === "value" || o.parentField === "right" || o.parentField === "initializer")) return true;
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
function findDeclarationName(occurrences, symbolName, start, end) {
|
|
589
|
+
let best;
|
|
590
|
+
for (const o of occurrences) {
|
|
591
|
+
if (o.text !== symbolName) continue;
|
|
592
|
+
if (o.startIndex < start || o.endIndex > end) continue;
|
|
593
|
+
if (!isBindingParentType(o.parentType)) continue;
|
|
594
|
+
if (!best || o.startIndex < best.startIndex) best = o;
|
|
595
|
+
}
|
|
596
|
+
return best ? best.startIndex : void 0;
|
|
597
|
+
}
|
|
598
|
+
async function staleDeclarationFile(projectId, match) {
|
|
599
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
600
|
+
const freshness = checkSymbolFreshness(raw, match.id);
|
|
601
|
+
if (freshness?.status === "fresh") return null;
|
|
602
|
+
const row = raw.prepare("SELECT file_hash FROM brain_file WHERE path=? AND project_id=?").get(match.fileId, projectId);
|
|
603
|
+
if (!row?.file_hash || !fileUnchangedSince(match.fileId, row.file_hash)) return match.fileId;
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
function matchesOf(found) {
|
|
607
|
+
return (found?.matches ?? []).filter((m) => m.byteStart != null && m.byteEnd != null);
|
|
608
|
+
}
|
|
609
|
+
async function indexedFileCount(projectId) {
|
|
610
|
+
try {
|
|
611
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
612
|
+
return Number(raw.prepare("SELECT COUNT(*) c FROM brain_file WHERE project_id=?").get(projectId).c ?? 0);
|
|
613
|
+
} catch {
|
|
614
|
+
return -1;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async function uniqueSymbol(projectId, name, filePath, lineStart, allowStaleIndex = false, projectPath, warnings) {
|
|
618
|
+
const found = await ASTEngine.findSymbol(projectId, name, filePath, lineStart);
|
|
619
|
+
let matches = matchesOf(found);
|
|
620
|
+
if (matches.length === 0) {
|
|
621
|
+
if (await indexedFileCount(projectId) === 0) {
|
|
622
|
+
const project = projectPath ? `"${projectPath}"` : "<project>";
|
|
623
|
+
throw new Error(
|
|
624
|
+
`symbol ${name} could not be resolved: this project has no index (0 files indexed), so the name cannot be found in it \u2014 index it first: bb index ${project}. bb_edit write mode does not need an index and works now; update, insert, delete and refactor resolve their target through the index.`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
throw new Error(`symbol ${name} not found or has no byte bounds`);
|
|
628
|
+
}
|
|
629
|
+
if (matches.length > 1) throw new Error(`symbol ${name} is ambiguous; provide filePath and lineStart`);
|
|
630
|
+
let match = matches[0];
|
|
631
|
+
if (allowStaleIndex) return match;
|
|
632
|
+
const staleFile = await staleDeclarationFile(projectId, match);
|
|
633
|
+
if (!staleFile) return match;
|
|
634
|
+
if (!projectPath) throw new Error(`stale index bounds for ${name}; reindex before editing`);
|
|
635
|
+
try {
|
|
636
|
+
await reindexFilesParallel(projectId, realpathSync(projectPath), [{ path: staleFile, type: "modified" }]);
|
|
637
|
+
} catch (e) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`stale index bounds for ${name}: ${staleFile} changed after indexing and refreshing it failed (${e?.message ?? String(e)}); run bb_index for that file, then retry`
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
const reFound = await ASTEngine.findSymbol(projectId, name, filePath, lineStart);
|
|
643
|
+
matches = matchesOf(reFound);
|
|
644
|
+
if (matches.length === 0) throw new Error(`symbol ${name} not found after refreshing ${staleFile} \u2014 it was renamed or removed since it was indexed`);
|
|
645
|
+
if (matches.length > 1) throw new Error(`symbol ${name} is ambiguous; provide filePath and lineStart`);
|
|
646
|
+
match = matches[0];
|
|
647
|
+
if (await staleDeclarationFile(projectId, match)) {
|
|
648
|
+
throw new Error(`stale index bounds for ${name} persist after refreshing ${staleFile}; reindex before editing`);
|
|
649
|
+
}
|
|
650
|
+
warnings?.push(`refreshed stale index rows for ${path2.basename(match.fileId)} before editing (the file changed after it was indexed)`);
|
|
651
|
+
return match;
|
|
652
|
+
}
|
|
653
|
+
function basenameNoExt(fileId) {
|
|
654
|
+
return path2.basename(fileId, path2.extname(fileId));
|
|
655
|
+
}
|
|
656
|
+
var IMPORT_SPECIFIER_PARENTS = /* @__PURE__ */ new Set([
|
|
657
|
+
"import_specifier",
|
|
658
|
+
"import_clause",
|
|
659
|
+
"export_specifier",
|
|
660
|
+
"dotted_name",
|
|
661
|
+
"scoped_identifier"
|
|
662
|
+
]);
|
|
663
|
+
var MODULE_EXTENSIONS = Object.freeze(
|
|
664
|
+
Array.from(new Set(Object.keys(EXTENSION_LANGUAGE).filter((k) => k !== k.toUpperCase()).map((k) => `.${k}`)))
|
|
665
|
+
);
|
|
666
|
+
function resolveImportToFile(importerRelPath, spec, declRelPath) {
|
|
667
|
+
if (!spec) return false;
|
|
668
|
+
const importerDir = path2.posix.dirname(importerRelPath.replace(/\\/g, "/"));
|
|
669
|
+
let base;
|
|
670
|
+
if (spec.startsWith("./") || spec.startsWith("../") || spec === "." || spec === "..") {
|
|
671
|
+
base = path2.posix.normalize(path2.posix.join(importerDir, spec));
|
|
672
|
+
} else if (spec.startsWith("/")) {
|
|
673
|
+
base = path2.posix.normalize(spec.slice(1));
|
|
674
|
+
} else return false;
|
|
675
|
+
const declExt = path2.posix.extname(declRelPath);
|
|
676
|
+
const declStem = declRelPath.slice(0, declRelPath.length - declExt.length);
|
|
677
|
+
if (base === declRelPath || base === declStem) return true;
|
|
678
|
+
if (path2.posix.dirname(declStem) === base) {
|
|
679
|
+
const asIndex = path2.posix.basename(declStem);
|
|
680
|
+
if (asIndex === "index" || MODULE_EXTENSIONS.some((e) => asIndex === `index${e}`)) return true;
|
|
681
|
+
}
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
function projectRelativePath(raw, projectId, fileId, projectRoot) {
|
|
685
|
+
const row = raw.prepare("SELECT relative_path AS relPath FROM brain_file WHERE project_id = ? AND (id = ? OR path = ?)").get(projectId, fileId, fileId);
|
|
686
|
+
if (row?.relPath) return row.relPath.replace(/\\/g, "/");
|
|
687
|
+
if (projectRoot && path2.isAbsolute(fileId)) {
|
|
688
|
+
const rel = path2.relative(projectRoot, fileId);
|
|
689
|
+
if (rel && !rel.startsWith("..")) return rel.split(path2.sep).join("/");
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
function relativeTo(root, file) {
|
|
694
|
+
const rel = path2.relative(root, file);
|
|
695
|
+
return rel && !rel.startsWith("..") ? rel : path2.basename(file);
|
|
696
|
+
}
|
|
697
|
+
async function validateFile(filePath, content) {
|
|
698
|
+
const result = await ASTEngine.parseFile(filePath, content);
|
|
699
|
+
const errors = (result.errors ?? []).filter((error) => /syntax error/i.test(error.message ?? ""));
|
|
700
|
+
if (errors.length > 0) throw new Error(`validation failed for ${filePath}: ${errors[0].message}`);
|
|
701
|
+
}
|
|
702
|
+
var MEMBER_NAME_PARENT_TYPES = /* @__PURE__ */ new Set([
|
|
703
|
+
"pair",
|
|
704
|
+
"pair_pattern",
|
|
705
|
+
"method_definition",
|
|
706
|
+
"public_field_definition",
|
|
707
|
+
"property_signature"
|
|
708
|
+
]);
|
|
709
|
+
var PARAMETER_BINDING_PARENTS = /* @__PURE__ */ new Set([
|
|
710
|
+
"required_parameter",
|
|
711
|
+
"optional_parameter",
|
|
712
|
+
"formal_parameters",
|
|
713
|
+
"formal_parameter",
|
|
714
|
+
"spread_parameter",
|
|
715
|
+
"parameter",
|
|
716
|
+
"parameter_declaration",
|
|
717
|
+
"parameters",
|
|
718
|
+
"method_parameters",
|
|
719
|
+
"lambda_parameters",
|
|
720
|
+
"typed_parameter",
|
|
721
|
+
"default_parameter"
|
|
722
|
+
]);
|
|
723
|
+
function collectShadowScopes(parsed, symbolName, declStart) {
|
|
724
|
+
const out = [];
|
|
725
|
+
for (const o of parsed.occurrences) {
|
|
726
|
+
if (o.text !== symbolName) continue;
|
|
727
|
+
if (!isBindingParentType(o.parentType)) continue;
|
|
728
|
+
if (o.startIndex === declStart) continue;
|
|
729
|
+
if (PARAMETER_BINDING_PARENTS.has(o.parentType)) {
|
|
730
|
+
const following = parsed.bodyRanges.filter((b) => b.startIndex > o.endIndex).sort((a, b) => a.startIndex - b.startIndex)[0];
|
|
731
|
+
if (following) out.push({ start: following.startIndex, end: following.endIndex });
|
|
732
|
+
const container = parsed.declaredNames.filter((d) => d.declarationStart <= o.startIndex && o.endIndex <= d.declarationEnd).sort((a, b) => a.declarationEnd - a.declarationStart - (b.declarationEnd - b.declarationStart))[0];
|
|
733
|
+
if (container) out.push({ start: container.declarationStart, end: container.declarationEnd });
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const body = parsed.bodyRanges.filter((b) => b.startIndex <= o.startIndex && o.endIndex <= b.endIndex).sort((a, b) => b.endIndex - b.startIndex - (a.endIndex - a.startIndex))[0];
|
|
737
|
+
if (!body) continue;
|
|
738
|
+
if (body.startIndex <= declStart && declStart <= body.endIndex) continue;
|
|
739
|
+
if (MEMBER_NAME_PARENT_TYPES.has(o.parentType) && !DECLARATION_WRAPPER_BODY_TYPES.has(body.nodeType)) continue;
|
|
740
|
+
out.push({ start: body.startIndex, end: body.endIndex });
|
|
741
|
+
}
|
|
742
|
+
return out;
|
|
743
|
+
}
|
|
744
|
+
var DECLARATION_WRAPPER_BODY_TYPES = /* @__PURE__ */ new Set([
|
|
745
|
+
"class_body",
|
|
746
|
+
"interface_body",
|
|
747
|
+
"enum_body",
|
|
748
|
+
"object_body",
|
|
749
|
+
"impl_body",
|
|
750
|
+
"trait_body",
|
|
751
|
+
"type_body",
|
|
752
|
+
"class_definition",
|
|
753
|
+
"declaration_list",
|
|
754
|
+
// C# class/interface/enum body
|
|
755
|
+
"field_declaration_list",
|
|
756
|
+
// Rust struct / Go struct body
|
|
757
|
+
"enum_variant_list"
|
|
758
|
+
// Rust enum body
|
|
759
|
+
]);
|
|
760
|
+
function insideStatementBody(parsed, declared) {
|
|
761
|
+
return parsed.bodyRanges.some(
|
|
762
|
+
(b) => !DECLARATION_WRAPPER_BODY_TYPES.has(b.nodeType) && b.startIndex <= declared.declarationStart && declared.declarationEnd <= b.endIndex
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
function declaresSymbolAtTopLevel(parsed, symbolName) {
|
|
766
|
+
return parsed.declaredNames.some((d) => d.text === symbolName && !insideStatementBody(parsed, d));
|
|
767
|
+
}
|
|
768
|
+
function legacySelfDeclarationPattern(symbolName) {
|
|
769
|
+
const escaped = symbolName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
770
|
+
return new RegExp(
|
|
771
|
+
`(?:function|class|interface|type|enum|struct|impl|def|fn|func|method|const|let|var|val)\\s+${escaped}\\b|\\b(?:public|private|protected|internal|static|abstract|override|readonly|final|sealed|export|declare|async)\\s+(?:async\\s+)?${escaped}\\b`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
var METHOD_FRAGMENT_WRAPPED = (content) => `class __bb_fragment__ {
|
|
775
|
+
${content}
|
|
776
|
+
}
|
|
777
|
+
`;
|
|
778
|
+
async function contentDeclaresSymbol(filePath, content, symbolName, symbolType) {
|
|
779
|
+
let parsed;
|
|
780
|
+
try {
|
|
781
|
+
parsed = await parseForSafety(filePath, content);
|
|
782
|
+
} catch {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
if (declaresSymbolAtTopLevel(parsed, symbolName)) return true;
|
|
786
|
+
if (parsed.declaredNames.length === 0 && legacySelfDeclarationPattern(symbolName).test(content)) return true;
|
|
787
|
+
const grammarSawTheName = parsed.declaredNames.some((d) => d.text === symbolName);
|
|
788
|
+
if (!grammarSawTheName && (parsed.hasSyntaxError || /method|constructor/i.test(symbolType ?? ""))) {
|
|
789
|
+
try {
|
|
790
|
+
const wrapped = await parseForSafety(filePath, METHOD_FRAGMENT_WRAPPED(content));
|
|
791
|
+
if (wrapped.declaredNames.some(
|
|
792
|
+
(d) => d.text === symbolName && /function|method/i.test(d.declarationType) && !insideStatementBody(wrapped, d)
|
|
793
|
+
)) {
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
function declarationRangeOf(parsed, symbolName, spanStart, spanEnd) {
|
|
802
|
+
const hits = parsed.declaredNames.filter(
|
|
803
|
+
(d) => d.text === symbolName && d.startIndex >= spanStart && d.endIndex <= spanEnd
|
|
804
|
+
);
|
|
805
|
+
if (hits.length === 0) return null;
|
|
806
|
+
const decl = hits.reduce((a, b) => b.declarationStart < a.declarationStart ? b : a);
|
|
807
|
+
const start = Math.min(spanStart, decl.declarationStart);
|
|
808
|
+
const end = Math.max(spanEnd, decl.declarationEnd);
|
|
809
|
+
if (start === spanStart && end === spanEnd) return null;
|
|
810
|
+
if (parsed.content.slice(start, spanStart).includes("\n") || parsed.content.slice(spanEnd, end).includes("\n")) return null;
|
|
811
|
+
return { start, end };
|
|
812
|
+
}
|
|
813
|
+
var BATCH_ENTRY_MODES = ["write", "update", "insert", "delete", "refactor"];
|
|
814
|
+
var BATCH_WIDE_OPTIONS = ["dryRun", "format", "edits", "files"];
|
|
815
|
+
var MAX_BATCH_EDITS = 200;
|
|
816
|
+
function expandBatchRequest(request) {
|
|
817
|
+
const specs = request.edits;
|
|
818
|
+
if (!Array.isArray(specs) || specs.length === 0) {
|
|
819
|
+
throw new Error("batch requires a non-empty `edits` array \u2014 one entry per edit, each with its own mode (write|update|insert|delete|refactor)");
|
|
820
|
+
}
|
|
821
|
+
const files = request.files;
|
|
822
|
+
if (files !== void 0) {
|
|
823
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
824
|
+
throw new Error("batch: `files` must be a non-empty array of project-relative paths \u2014 omit it to let each entry name its own filePath");
|
|
825
|
+
}
|
|
826
|
+
const seen = /* @__PURE__ */ new Map();
|
|
827
|
+
for (const [i, file] of files.entries()) {
|
|
828
|
+
if (typeof file !== "string" || file.trim() === "") throw new Error(`batch: files[${i}] must be a non-empty project-relative path`);
|
|
829
|
+
const prior = seen.get(file);
|
|
830
|
+
if (prior !== void 0) {
|
|
831
|
+
throw new Error(`batch: files[${i}] repeats ${file} (already listed as files[${prior}]) \u2014 \`files\` fans every entry out over the list, and two entries on one file would be planned from the same pre-edit bytes`);
|
|
832
|
+
}
|
|
833
|
+
seen.set(file, i);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const batch = [];
|
|
837
|
+
for (const [i, raw] of specs.entries()) {
|
|
838
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
839
|
+
throw new Error(`batch entry ${i} must be an object carrying its own mode (write|update|insert|delete|refactor)`);
|
|
840
|
+
}
|
|
841
|
+
const spec = raw;
|
|
842
|
+
if (String(spec.mode) === "batch") {
|
|
843
|
+
throw new Error(`batch entry ${i} must not itself be a batch \u2014 the entries are the five single-file modes: ${BATCH_ENTRY_MODES.join(" | ")}`);
|
|
844
|
+
}
|
|
845
|
+
if (!BATCH_ENTRY_MODES.includes(String(spec.mode))) {
|
|
846
|
+
throw new Error(`batch entry ${i} requires a mode of ${BATCH_ENTRY_MODES.join("|")} (got ${JSON.stringify(spec.mode ?? null)}) \u2014 an entry with no mode would otherwise pass every per-mode check silently`);
|
|
847
|
+
}
|
|
848
|
+
const mode = spec.mode;
|
|
849
|
+
const wide = BATCH_WIDE_OPTIONS.filter((option) => spec[option] !== void 0);
|
|
850
|
+
if (wide.length > 0) {
|
|
851
|
+
throw new Error(`batch entry ${i} must not set ${wide.join(", ")} \u2014 ${wide.length === 1 ? "that option belongs" : "those options belong"} to the batch as a whole, because a batch previews, formats and reindexes ONCE; set ${wide.join(", ")} on the batch request itself`);
|
|
852
|
+
}
|
|
853
|
+
if (files !== void 0) {
|
|
854
|
+
if (typeof spec.filePath === "string") {
|
|
855
|
+
throw new Error(`batch entry ${i} sets filePath (${spec.filePath}) and the batch sets \`files\` \u2014 \`files\` supplies the target of every entry, so an entry must not also name one`);
|
|
856
|
+
}
|
|
857
|
+
for (const file of files) batch.push({ entry: i, mode, request: { ...spec, filePath: file } });
|
|
858
|
+
} else {
|
|
859
|
+
batch.push({ entry: i, mode, request: { ...spec } });
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (batch.length > MAX_BATCH_EDITS) {
|
|
863
|
+
throw new Error(
|
|
864
|
+
`batch: ${batch.length} planned edits exceeds the limit of ${MAX_BATCH_EDITS} (${specs.length} entr${specs.length === 1 ? "y" : "ies"}${files ? ` \xD7 ${files.length} file(s)` : ""}) \u2014 split the batch, or lower the entry count and the fan-out list. The limit bounds one plan, one formatting pass, one validation pass and one reindex, which all hold every target at once.`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
return batch;
|
|
868
|
+
}
|
|
869
|
+
function readPreImage(file, preImages, preImageBytes) {
|
|
870
|
+
if (!existsSync2(file)) {
|
|
871
|
+
preImages.set(file, null);
|
|
872
|
+
preImageBytes.set(file, null);
|
|
873
|
+
return { existed: false, text: "" };
|
|
874
|
+
}
|
|
875
|
+
const bytes = readFileSync2(file);
|
|
876
|
+
preImages.set(file, createHash("sha256").update(bytes).digest("hex"));
|
|
877
|
+
preImageBytes.set(file, bytes.length);
|
|
878
|
+
return { existed: true, text: bytes.toString("utf8") };
|
|
879
|
+
}
|
|
880
|
+
async function planEdit(projectPath, projectId, request) {
|
|
881
|
+
const preImages = /* @__PURE__ */ new Map();
|
|
882
|
+
const preImageBytes = /* @__PURE__ */ new Map();
|
|
883
|
+
const plan = await planEditInto(projectPath, projectId, request, preImages, preImageBytes);
|
|
884
|
+
return {
|
|
885
|
+
...plan,
|
|
886
|
+
preImages: Object.fromEntries(preImages),
|
|
887
|
+
preImageBytes: Object.fromEntries(preImageBytes),
|
|
888
|
+
owners: Object.fromEntries(
|
|
889
|
+
plan.changes.map((change) => [change.path, { entry: 0, mode: request.mode }])
|
|
890
|
+
)
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
async function planBatch(projectPath, projectId, request) {
|
|
894
|
+
const entries = expandBatchRequest(request);
|
|
895
|
+
const preImages = /* @__PURE__ */ new Map();
|
|
896
|
+
const preImageBytes = /* @__PURE__ */ new Map();
|
|
897
|
+
const changes = [];
|
|
898
|
+
const warnings = [];
|
|
899
|
+
const owners = {};
|
|
900
|
+
const batchRows = /* @__PURE__ */ new Map();
|
|
901
|
+
let renamedFiles = 0;
|
|
902
|
+
let renamedSites = 0;
|
|
903
|
+
let sawSummary = false;
|
|
904
|
+
for (const { entry, mode, request: concrete } of entries) {
|
|
905
|
+
let planned;
|
|
906
|
+
try {
|
|
907
|
+
planned = await planEditInto(projectPath, projectId, concrete, preImages, preImageBytes);
|
|
908
|
+
} catch (e) {
|
|
909
|
+
const target = typeof concrete.filePath === "string" ? ` ${concrete.filePath}` : "";
|
|
910
|
+
throw new Error(`batch entry ${entry} (${mode}${target}): ${e?.message ?? String(e)}`);
|
|
911
|
+
}
|
|
912
|
+
for (const change of planned.changes) {
|
|
913
|
+
const prior = owners[change.path];
|
|
914
|
+
if (prior) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`bb_edit wrote nothing: batch entries ${prior.entry} and ${entry} both resolve to ${relativeTo(projectPath, change.path)} \u2014 each entry is planned from the file's pre-edit bytes, so applying both would discard one of them. Edit that file in one entry, or make a second bb_edit call after this one.`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
owners[change.path] = { entry, mode };
|
|
920
|
+
changes.push(change);
|
|
921
|
+
}
|
|
922
|
+
for (const warning of planned.warnings) warnings.push(`batch entry ${entry}: ${warning}`);
|
|
923
|
+
if (planned.summary) {
|
|
924
|
+
sawSummary = true;
|
|
925
|
+
renamedFiles += planned.summary.renamedFiles;
|
|
926
|
+
renamedSites += planned.summary.renamedSites;
|
|
927
|
+
}
|
|
928
|
+
const row = batchRows.get(entry) ?? { entry, mode, files: [] };
|
|
929
|
+
row.files.push(...planned.changes.map((change) => relativeTo(projectPath, change.path)));
|
|
930
|
+
if (planned.importPruning) row.importPruning = planned.importPruning;
|
|
931
|
+
batchRows.set(entry, row);
|
|
932
|
+
}
|
|
933
|
+
return {
|
|
934
|
+
changes,
|
|
935
|
+
warnings,
|
|
936
|
+
summary: sawSummary ? { renamedFiles, renamedSites } : void 0,
|
|
937
|
+
preImages: Object.fromEntries(preImages),
|
|
938
|
+
preImageBytes: Object.fromEntries(preImageBytes),
|
|
939
|
+
owners,
|
|
940
|
+
batch: [...batchRows.values()]
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
async function planEditInto(projectPath, projectId, request, preImages, preImageBytes) {
|
|
944
|
+
const warnings = [];
|
|
945
|
+
if (request.mode === "write") {
|
|
946
|
+
if (!request.filePath) throw new Error("write requires filePath");
|
|
947
|
+
const target = resolveSafeEditPath(projectPath, request.filePath);
|
|
948
|
+
const pre = readPreImage(target, preImages, preImageBytes);
|
|
949
|
+
const after = String(request.content ?? request.newContent ?? "");
|
|
950
|
+
const refusal = writeGuardMessage(relativeTo(projectPath, target), { existed: pre.existed, before: pre.text, after }, request.overwrite);
|
|
951
|
+
if (refusal) throw new Error(refusal);
|
|
952
|
+
return { changes: [{ path: target, before: pre.text, after }], warnings };
|
|
953
|
+
}
|
|
954
|
+
const symbolName = request.mode === "insert" ? request.targetSymbolName : request.symbolName;
|
|
955
|
+
if (!symbolName) throw new Error(`${request.mode} requires ${request.mode === "insert" ? "targetSymbolName" : "symbolName"}`);
|
|
956
|
+
const match = await uniqueSymbol(projectId, symbolName, request.filePath, request.lineStart, request.allowStaleIndex === true, projectPath, warnings);
|
|
957
|
+
resolveSafeEditPath(projectPath, match.fileId);
|
|
958
|
+
const original = readPreImage(match.fileId, preImages, preImageBytes).text;
|
|
959
|
+
let originalParse;
|
|
960
|
+
const parsedOriginal = async () => {
|
|
961
|
+
if (originalParse === void 0) {
|
|
962
|
+
try {
|
|
963
|
+
originalParse = await parseForSafety(match.fileId, original);
|
|
964
|
+
} catch {
|
|
965
|
+
originalParse = null;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return originalParse;
|
|
969
|
+
};
|
|
970
|
+
const declaredRange = async () => {
|
|
971
|
+
const parsed = await parsedOriginal();
|
|
972
|
+
if (!parsed) return null;
|
|
973
|
+
return declarationRangeOf(parsed, symbolName, match.byteStart, match.byteEnd);
|
|
974
|
+
};
|
|
975
|
+
if (request.mode === "update") {
|
|
976
|
+
const newContent = String(request.newContent ?? request.content ?? "");
|
|
977
|
+
if (await contentDeclaresSymbol(match.fileId, newContent, symbolName, match.symbolType)) {
|
|
978
|
+
const range = await declaredRange() ?? { start: match.byteStart, end: match.byteEnd };
|
|
979
|
+
return { changes: [{ path: match.fileId, before: original, after: replaceRange(original, range.start, range.end, withNewline(newContent, dominantNewline(original))) }], warnings };
|
|
980
|
+
}
|
|
981
|
+
const parsed = await parsedOriginal();
|
|
982
|
+
if (!parsed) throw new Error(`update ${symbolName}: cannot parse ${path2.basename(match.fileId)} to locate a body block`);
|
|
983
|
+
const body = findSymbolBody(parsed, match.byteStart, match.byteEnd);
|
|
984
|
+
if (body && original[body.startIndex] === "{") {
|
|
985
|
+
const open = body.startIndex;
|
|
986
|
+
const close = body.endIndex - 1;
|
|
987
|
+
const inner = original.slice(open + 1, close);
|
|
988
|
+
const innerIndent = inner.includes("\n") ? computeInnerIndent(original, body) : lineLeadingWhitespace(original, open) + indentUnit(original);
|
|
989
|
+
const nl = dominantNewline(original);
|
|
990
|
+
const spliced = withNewline("\n" + indentLines(newContent, innerIndent, true) + "\n" + lineLeadingWhitespace(original, open), nl);
|
|
991
|
+
const after = replaceRange(original, open + 1, close, spliced);
|
|
992
|
+
return { changes: [{ path: match.fileId, before: original, after }], warnings };
|
|
993
|
+
}
|
|
994
|
+
throw new Error(`update ${symbolName}: newContent does not declare the symbol and no replaceable body block was found; pass the full declaration to replace it`);
|
|
995
|
+
}
|
|
996
|
+
if (request.mode === "insert") {
|
|
997
|
+
const position = request.position ?? "after";
|
|
998
|
+
const content = String(request.newContent ?? request.content ?? "");
|
|
999
|
+
let start = position === "before" ? match.byteStart : match.byteEnd;
|
|
1000
|
+
let end = start;
|
|
1001
|
+
let replacement = `
|
|
1002
|
+
${content}
|
|
1003
|
+
`;
|
|
1004
|
+
if (position === "before" || position === "after") {
|
|
1005
|
+
const anchor = await declaredRange() ?? { start: match.byteStart, end: match.byteEnd };
|
|
1006
|
+
start = end = position === "before" ? anchor.start : anchor.end;
|
|
1007
|
+
}
|
|
1008
|
+
if (position === "inside-top" || position === "inside-bottom") {
|
|
1009
|
+
const parsed = await parsedOriginal();
|
|
1010
|
+
if (!parsed) throw new Error(`cannot insert inside ${symbolName}: ${path2.basename(match.fileId)} could not be parsed`);
|
|
1011
|
+
const body = findSymbolBody(parsed, match.byteStart, match.byteEnd);
|
|
1012
|
+
if (!body) throw new Error(`cannot insert inside ${symbolName}: no recognizable body block (unsupported language or syntax)`);
|
|
1013
|
+
if (original[body.startIndex] === "{") {
|
|
1014
|
+
const open = body.startIndex;
|
|
1015
|
+
const close = body.endIndex - 1;
|
|
1016
|
+
const inner = original.slice(open + 1, close);
|
|
1017
|
+
const isMultiLine = inner.includes("\n");
|
|
1018
|
+
const braceLineIndent = lineLeadingWhitespace(original, open);
|
|
1019
|
+
const innerIndent = isMultiLine ? computeInnerIndent(original, body) : braceLineIndent + indentUnit(original);
|
|
1020
|
+
if (isMultiLine) {
|
|
1021
|
+
if (position === "inside-top") {
|
|
1022
|
+
start = end = open + 1;
|
|
1023
|
+
replacement = "\n" + indentLines(content, innerIndent, true);
|
|
1024
|
+
} else {
|
|
1025
|
+
start = end = close;
|
|
1026
|
+
replacement = indentLines(content, innerIndent, true) + "\n";
|
|
1027
|
+
}
|
|
1028
|
+
} else if (inner.trim() === "") {
|
|
1029
|
+
start = end = open + 1;
|
|
1030
|
+
replacement = "\n" + indentLines(content, innerIndent, true) + "\n" + braceLineIndent;
|
|
1031
|
+
} else {
|
|
1032
|
+
const stmt = inner.trim();
|
|
1033
|
+
const contentLines = indentLines(content, innerIndent, true);
|
|
1034
|
+
start = open + 1;
|
|
1035
|
+
end = close;
|
|
1036
|
+
replacement = position === "inside-top" ? "\n" + contentLines + "\n" + innerIndent + stmt + "\n" + braceLineIndent : "\n" + innerIndent + stmt + "\n" + contentLines + "\n" + braceLineIndent;
|
|
1037
|
+
}
|
|
1038
|
+
} else {
|
|
1039
|
+
const indent = lineIndent(original, body.startIndex);
|
|
1040
|
+
if (!/^[\t ]*$/.test(indent)) {
|
|
1041
|
+
throw new Error(`cannot insert inside ${symbolName}: body is not line-aligned (unsupported body form such as Elixir do/end)`);
|
|
1042
|
+
}
|
|
1043
|
+
if (position === "inside-top") {
|
|
1044
|
+
start = end = body.startIndex;
|
|
1045
|
+
replacement = indentLines(content, indent, false) + "\n" + indent;
|
|
1046
|
+
} else {
|
|
1047
|
+
start = end = body.endIndex;
|
|
1048
|
+
replacement = "\n" + indentLines(content, indent, true);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const nl = dominantNewline(original);
|
|
1053
|
+
const after = withNewline(replaceRange(original, start, end, replacement), nl);
|
|
1054
|
+
return { changes: [{ path: match.fileId, before: original, after }], warnings };
|
|
1055
|
+
}
|
|
1056
|
+
if (request.mode === "delete") {
|
|
1057
|
+
const raw2 = await BrainDatabase.getRawInstance(projectId);
|
|
1058
|
+
const callers = raw2.prepare("SELECT COUNT(*) c FROM brain_relationship WHERE target_symbol_id=? AND relationship_type IN ('calls','instantiates','references','uses')").get(match.id)?.c ?? 0;
|
|
1059
|
+
const removal = await declaredRange() ?? { start: match.byteStart, end: match.byteEnd };
|
|
1060
|
+
const deleted = replaceRange(original, removal.start, removal.end, "");
|
|
1061
|
+
const parsedAfter = await parseForSafety(match.fileId, deleted);
|
|
1062
|
+
const leftovers = parsedAfter.occurrences.filter((o) => o.text === symbolName && isReferenceSite(o, "")).map((o) => `line ${o.startLine}: ${o.parentType || "reference"}`);
|
|
1063
|
+
const declRelPath2 = projectRelativePath(raw2, projectId, match.fileId, projectPath);
|
|
1064
|
+
const importerRows = declRelPath2 === null ? [] : raw2.prepare(
|
|
1065
|
+
// The LIKE is a PREFILTER on the specifier's last segment, nothing more.
|
|
1066
|
+
// It bounds the candidate set without deciding anything: the decision is
|
|
1067
|
+
// made by resolveImportToFile below, which walks the real path.
|
|
1068
|
+
"SELECT DISTINCT i.file_id AS fileId, f.path AS path, f.relative_path AS relPath, i.import_path AS spec FROM brain_import i JOIN brain_file f ON f.id = i.file_id WHERE i.project_id = ? AND i.is_external = 0 AND i.file_id <> ? AND i.import_path IS NOT NULL AND (i.import_path = ? OR i.import_path LIKE ? OR i.import_path LIKE ?)"
|
|
1069
|
+
).all(
|
|
1070
|
+
projectId,
|
|
1071
|
+
match.fileId,
|
|
1072
|
+
basenameNoExt(match.fileId),
|
|
1073
|
+
`%/${basenameNoExt(match.fileId)}`,
|
|
1074
|
+
`./${basenameNoExt(match.fileId)}`
|
|
1075
|
+
);
|
|
1076
|
+
const parsedByFile = /* @__PURE__ */ new Map();
|
|
1077
|
+
const importerLeftovers = [];
|
|
1078
|
+
for (const importer of importerRows) {
|
|
1079
|
+
if (!resolveImportToFile(importer.relPath, importer.spec, declRelPath2)) continue;
|
|
1080
|
+
let parsed = parsedByFile.get(importer.fileId);
|
|
1081
|
+
if (parsed === void 0) {
|
|
1082
|
+
try {
|
|
1083
|
+
parsed = await parseForSafety(importer.path, readFileSync2(importer.path, "utf8"));
|
|
1084
|
+
} catch {
|
|
1085
|
+
parsed = null;
|
|
1086
|
+
}
|
|
1087
|
+
parsedByFile.set(importer.fileId, parsed);
|
|
1088
|
+
}
|
|
1089
|
+
if (!parsed) continue;
|
|
1090
|
+
for (const o of parsed.occurrences) {
|
|
1091
|
+
if (o.text !== symbolName) continue;
|
|
1092
|
+
if (!IMPORT_SPECIFIER_PARENTS.has(o.parentType)) continue;
|
|
1093
|
+
importerLeftovers.push(`${relativeTo(projectPath, importer.path)}:${o.startLine} (${o.parentType})`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const indexedCallers = callers > 0 ? `${callers} indexed caller(s)` : null;
|
|
1097
|
+
const totalLeftovers = leftovers.length + importerLeftovers.length;
|
|
1098
|
+
if ((callers > 0 || totalLeftovers > 0) && request.force !== true) {
|
|
1099
|
+
const parts = [
|
|
1100
|
+
indexedCallers,
|
|
1101
|
+
// The phrase "surviving deletion" is kept because script/m13/verify-edit-unit
|
|
1102
|
+
// asserts on it, and a gate that greps for it is checking that the
|
|
1103
|
+
// refusal SAYS what survives — which is right. The WHERE is new
|
|
1104
|
+
// information layered on top, not a replacement: the declaring file and
|
|
1105
|
+
// the importing files are different risks and the message distinguishes
|
|
1106
|
+
// them.
|
|
1107
|
+
leftovers.length > 0 ? `${leftovers.length} live reference(s) surviving deletion in the declaring file (${leftovers.slice(0, 5).join(", ")}${leftovers.length > 5 ? ", \u2026" : ""})` : null,
|
|
1108
|
+
importerLeftovers.length > 0 ? `${importerLeftovers.length} reference(s) surviving deletion in importing file(s) \u2014 ${[...new Set(importerLeftovers.map((l) => l.split(":")[0]))].slice(0, 4).join(", ")}` : null
|
|
1109
|
+
].filter(Boolean).join("; ");
|
|
1110
|
+
throw new Error(`cannot delete ${symbolName}: ${parts}; pass force=true after reviewing impact`);
|
|
1111
|
+
}
|
|
1112
|
+
if (totalLeftovers > 0) {
|
|
1113
|
+
const where = [...leftovers.map((l) => `${path2.basename(match.fileId)} ${l}`), ...importerLeftovers];
|
|
1114
|
+
warnings.push(`delete ${symbolName} (forced): ${totalLeftovers} reference(s) remain \u2014 ${where.slice(0, 10).join("; ")}${where.length > 10 ? "; \u2026" : ""}`);
|
|
1115
|
+
}
|
|
1116
|
+
if (request.force === true && callers > 0) {
|
|
1117
|
+
warnings.push(`delete ${symbolName} (forced): ${callers} indexed caller(s) were bypassed by force=true \u2014 the index still records them; review with bb_graph or bb_impact before relying on this deletion`);
|
|
1118
|
+
}
|
|
1119
|
+
const pruneReport = await ImportPruner.pruneWithReport(deleted, match.fileId);
|
|
1120
|
+
const prunedContent = pruneReport.content ?? deleted;
|
|
1121
|
+
if (pruneReport.removedBindings.length > 0) {
|
|
1122
|
+
warnings.push(`delete ${symbolName}: removed ${pruneReport.removedBindings.length} import binding(s) the deletion made dead \u2014 ${pruneReport.removedBindings.join(", ")}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (!pruneReport.attempted && pruneReport.reason) {
|
|
1125
|
+
warnings.push(`delete ${symbolName}: import cleanup was not attempted \u2014 ${pruneReport.reason}`);
|
|
1126
|
+
}
|
|
1127
|
+
const importPruning = pruneReport.attempted ? { attempted: true, language: pruneReport.language, removedBindings: pruneReport.removedBindings } : { attempted: false, language: pruneReport.language, removedBindings: [], reason: pruneReport.reason };
|
|
1128
|
+
return { changes: [{ path: match.fileId, before: original, after: prunedContent }], warnings, importPruning };
|
|
1129
|
+
}
|
|
1130
|
+
const newName = String(request.newName ?? "");
|
|
1131
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(newName)) throw new Error("refactor requires a valid identifier newName");
|
|
1132
|
+
if (newName === symbolName) {
|
|
1133
|
+
throw new Error(`refactor ${symbolName} \u2192 ${newName}: the new name is the current name; a rename must change the identifier. No change was made.`);
|
|
1134
|
+
}
|
|
1135
|
+
const raw = await BrainDatabase.getRawInstance(projectId);
|
|
1136
|
+
const callerRows = raw.prepare(
|
|
1137
|
+
"SELECT DISTINCT f.id AS fileId, f.path AS path, s.byte_start AS byteStart, s.byte_end AS byteEnd, r.relationship_type AS relType FROM brain_relationship r LEFT JOIN brain_symbol s ON s.id = r.source_symbol_id LEFT JOIN brain_file f ON f.id = s.file_id WHERE r.target_symbol_id = ? AND r.relationship_type IN ('calls','instantiates','references','uses')"
|
|
1138
|
+
).all(match.id);
|
|
1139
|
+
const locatableCallerRows = callerRows.filter((r) => r.path != null && r.byteStart != null);
|
|
1140
|
+
const sourcelessCallerRows = callerRows.filter((r) => r.path == null);
|
|
1141
|
+
const declRelPath = projectRelativePath(raw, projectId, match.fileId, projectPath);
|
|
1142
|
+
const importers = /* @__PURE__ */ new Map();
|
|
1143
|
+
if (declRelPath !== null) {
|
|
1144
|
+
const stem = basenameNoExt(match.fileId);
|
|
1145
|
+
const importerRows = raw.prepare(
|
|
1146
|
+
"SELECT i.file_id AS fileId, f.path AS path, f.relative_path AS relPath, i.imported_name AS importedName, i.imported_as AS importedAs, i.import_path AS spec, i.line_number AS lineNumber FROM brain_import i JOIN brain_file f ON f.id = i.file_id WHERE i.project_id = ? AND i.is_external = 0 AND i.file_id <> ? AND i.import_path IS NOT NULL AND i.imported_name = ? AND (i.import_path = ? OR i.import_path LIKE ? OR i.import_path LIKE ?)"
|
|
1147
|
+
).all(projectId, match.fileId, symbolName, stem, `%/${stem}`, `./${stem}`);
|
|
1148
|
+
for (const row of importerRows) {
|
|
1149
|
+
if (!resolveImportToFile(row.relPath, row.spec, declRelPath)) continue;
|
|
1150
|
+
const entry = importers.get(row.path) ?? { fileId: row.fileId, bindings: [] };
|
|
1151
|
+
entry.bindings.push({
|
|
1152
|
+
line: row.lineNumber ?? null,
|
|
1153
|
+
aliased: row.importedAs != null && row.importedAs !== "" && row.importedAs !== symbolName
|
|
1154
|
+
});
|
|
1155
|
+
importers.set(row.path, entry);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const callerFileIds = [.../* @__PURE__ */ new Set([
|
|
1159
|
+
...locatableCallerRows.map((r) => r.fileId).filter((id) => id != null),
|
|
1160
|
+
...[...importers.values()].map((i) => i.fileId)
|
|
1161
|
+
])].filter((id) => id !== match.fileId);
|
|
1162
|
+
if (callerFileIds.length > 0) assertFreshFiles(raw, callerFileIds, `refactor ${symbolName}`);
|
|
1163
|
+
const fileRanges = /* @__PURE__ */ new Map();
|
|
1164
|
+
const declEntry = fileRanges.get(match.fileId) ?? { callers: [] };
|
|
1165
|
+
declEntry.declaration = { start: match.byteStart, end: match.byteEnd };
|
|
1166
|
+
fileRanges.set(match.fileId, declEntry);
|
|
1167
|
+
for (const row of locatableCallerRows) {
|
|
1168
|
+
if (row.path == null || row.byteStart == null) continue;
|
|
1169
|
+
const entry = fileRanges.get(row.path) ?? { callers: [] };
|
|
1170
|
+
entry.callers.push({ start: row.byteStart, end: row.byteEnd ?? Number.MAX_SAFE_INTEGER, relType: row.relType || "" });
|
|
1171
|
+
fileRanges.set(row.path, entry);
|
|
1172
|
+
}
|
|
1173
|
+
for (const [importerPath, importer] of importers) {
|
|
1174
|
+
const entry = fileRanges.get(importerPath) ?? { callers: [] };
|
|
1175
|
+
entry.imports = [...entry.imports ?? [], ...importer.bindings];
|
|
1176
|
+
fileRanges.set(importerPath, entry);
|
|
1177
|
+
}
|
|
1178
|
+
const unrenamed = [];
|
|
1179
|
+
const changes = [];
|
|
1180
|
+
let renamedFiles = 0;
|
|
1181
|
+
let renamedSites = 0;
|
|
1182
|
+
let declarationRenamed = false;
|
|
1183
|
+
for (const [file, ranges] of fileRanges) {
|
|
1184
|
+
resolveSafeEditPath(projectPath, file);
|
|
1185
|
+
const before = readPreImage(file, preImages, preImageBytes).text;
|
|
1186
|
+
const parsed = await parseForSafety(file, before);
|
|
1187
|
+
const sites = /* @__PURE__ */ new Set();
|
|
1188
|
+
const declStart = ranges.declaration ? findDeclarationName(parsed.occurrences, symbolName, ranges.declaration.start, ranges.declaration.end) : void 0;
|
|
1189
|
+
if (declStart != null) {
|
|
1190
|
+
sites.add(declStart);
|
|
1191
|
+
declarationRenamed = true;
|
|
1192
|
+
}
|
|
1193
|
+
const importAnchor = parsed.occurrences.find(
|
|
1194
|
+
(o) => o.text === symbolName && IMPORT_SPECIFIER_PARENTS.has(o.parentType)
|
|
1195
|
+
)?.startIndex;
|
|
1196
|
+
const shadowScopes = collectShadowScopes(parsed, symbolName, declStart ?? importAnchor ?? 0);
|
|
1197
|
+
const shadowedSites = /* @__PURE__ */ new Set();
|
|
1198
|
+
const isShadowed = (start, end) => shadowScopes.some((s) => start >= s.start && end <= s.end);
|
|
1199
|
+
if (ranges.declaration && declStart != null) {
|
|
1200
|
+
for (const o of parsed.occurrences) {
|
|
1201
|
+
if (o.text !== symbolName) continue;
|
|
1202
|
+
if (!isReferenceSite(o, "")) continue;
|
|
1203
|
+
if (isShadowed(o.startIndex, o.endIndex)) {
|
|
1204
|
+
shadowedSites.add(o.startIndex);
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
sites.add(o.startIndex);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
for (const span of ranges.callers) {
|
|
1211
|
+
const spanEnd = span.end === Number.MAX_SAFE_INTEGER ? before.length : span.end;
|
|
1212
|
+
if (span.start < 0 || span.start >= before.length || spanEnd < span.start || spanEnd > before.length) {
|
|
1213
|
+
throw new Error(`refactor ${symbolName}: caller span out of bounds in ${file} (start=${span.start}, end=${span.end}, length=${before.length}); reindex before editing`);
|
|
1214
|
+
}
|
|
1215
|
+
for (const o of parsed.occurrences) {
|
|
1216
|
+
if (o.text !== symbolName) continue;
|
|
1217
|
+
if (o.startIndex < span.start || o.endIndex > spanEnd) continue;
|
|
1218
|
+
if (!isReferenceSite(o, span.relType)) continue;
|
|
1219
|
+
if (isShadowed(o.startIndex, o.endIndex)) {
|
|
1220
|
+
shadowedSites.add(o.startIndex);
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
sites.add(o.startIndex);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (ranges.imports?.length) {
|
|
1227
|
+
const specifiers = parsed.occurrences.filter((o) => o.text === symbolName && IMPORT_SPECIFIER_PARENTS.has(o.parentType));
|
|
1228
|
+
for (const binding of ranges.imports) {
|
|
1229
|
+
const onLine = binding.line == null ? [] : specifiers.filter((o) => o.startLine === binding.line);
|
|
1230
|
+
const chosen = onLine.length > 0 ? onLine : specifiers;
|
|
1231
|
+
for (const o of chosen) sites.add(o.startIndex);
|
|
1232
|
+
}
|
|
1233
|
+
if (ranges.imports.some((b) => !b.aliased)) {
|
|
1234
|
+
for (const o of parsed.occurrences) {
|
|
1235
|
+
if (o.text !== symbolName) continue;
|
|
1236
|
+
if (!isReferenceSite(o, "")) continue;
|
|
1237
|
+
if (isShadowed(o.startIndex, o.endIndex)) {
|
|
1238
|
+
shadowedSites.add(o.startIndex);
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
sites.add(o.startIndex);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const fileHasEvidence = ranges.declaration != null || (ranges.imports?.length ?? 0) > 0;
|
|
1246
|
+
if (fileHasEvidence) {
|
|
1247
|
+
for (const o of parsed.occurrences) {
|
|
1248
|
+
if (o.text !== symbolName) continue;
|
|
1249
|
+
if (sites.has(o.startIndex) || shadowedSites.has(o.startIndex)) continue;
|
|
1250
|
+
if (!IMPORT_SPECIFIER_PARENTS.has(o.parentType) && !isReferenceSite(o, "")) continue;
|
|
1251
|
+
unrenamed.push(`${relativeTo(projectPath, file)}:${o.startLine} (${o.parentType || "reference"})`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (sites.size === 0) continue;
|
|
1255
|
+
let after = before;
|
|
1256
|
+
for (const offset of [...sites].sort((a, b) => b - a)) after = replaceRange(after, offset, offset + symbolName.length, newName);
|
|
1257
|
+
changes.push({ path: file, before, after });
|
|
1258
|
+
renamedFiles++;
|
|
1259
|
+
renamedSites += sites.size;
|
|
1260
|
+
}
|
|
1261
|
+
if (unrenamed.length > 0) {
|
|
1262
|
+
throw new Error(`refactor ${symbolName} \u2192 ${newName}: ${unrenamed.length} reference(s) this rename found but could not rewrite \u2014 ${unrenamed.slice(0, 6).join(", ")}${unrenamed.length > 6 ? ", \u2026" : ""}; reindex the project or rename them manually`);
|
|
1263
|
+
}
|
|
1264
|
+
if (renamedSites === 0) {
|
|
1265
|
+
throw new Error(`refactor ${symbolName} \u2192 ${newName}: resolved 0 rename site(s) in 0 file(s) \u2014 the symbol was located but nothing could be renamed (this grammar may expose no identifier node for it, as a YAML key or a Makefile target does not). No change was made.`);
|
|
1266
|
+
}
|
|
1267
|
+
const collisions = [];
|
|
1268
|
+
for (const change of changes) {
|
|
1269
|
+
const parsedAfter = await parseForSafety(change.path, change.after);
|
|
1270
|
+
const topLevel = parsedAfter.declaredNames.filter((d) => d.text === newName && !insideStatementBody(parsedAfter, d));
|
|
1271
|
+
const expected = change.path === match.fileId && declarationRenamed ? 1 : 0;
|
|
1272
|
+
for (const extra of topLevel.slice(expected)) {
|
|
1273
|
+
const line = parsedAfter.content.slice(0, extra.startIndex).split("\n").length;
|
|
1274
|
+
collisions.push(`${relativeTo(projectPath, change.path)}:${line} (${extra.declarationType})`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (collisions.length > 0) {
|
|
1278
|
+
throw new Error(`refactor ${symbolName} \u2192 ${newName}: the new name is already declared \u2014 ${collisions.slice(0, 6).join(", ")}${collisions.length > 6 ? ", \u2026" : ""}; renaming onto an existing declaration would leave two declarations with one name. No change was made.`);
|
|
1279
|
+
}
|
|
1280
|
+
if (request.mode === "refactor") {
|
|
1281
|
+
warnings.push(`refactor ${symbolName} \u2192 ${newName}: ${renamedSites} site(s) across ${renamedFiles} file(s)`);
|
|
1282
|
+
if (sourcelessCallerRows.length > 0) {
|
|
1283
|
+
warnings.push(`refactor ${symbolName}: ${sourcelessCallerRows.length} cross-file edge(s) to this symbol have no source symbol in the index; ${importers.size} importing file(s) were covered through brain_import, and a call site in a file that does not import the declaration module is not reachable by this rename`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return { changes, warnings, summary: request.mode === "refactor" ? { renamedFiles, renamedSites } : void 0 };
|
|
1287
|
+
}
|
|
1288
|
+
function plannedEnvelope(projectPath, plan) {
|
|
1289
|
+
return {
|
|
1290
|
+
atomic: true,
|
|
1291
|
+
entries: plan.changes.map((change) => {
|
|
1292
|
+
const owner = plan.owners[change.path] ?? { entry: 0, mode: "write" };
|
|
1293
|
+
return {
|
|
1294
|
+
path: relativeTo(projectPath, change.path),
|
|
1295
|
+
entry: owner.entry,
|
|
1296
|
+
mode: owner.mode,
|
|
1297
|
+
planned: true,
|
|
1298
|
+
committed: false,
|
|
1299
|
+
restored: false,
|
|
1300
|
+
bytesBefore: plan.preImageBytes[change.path] ?? null,
|
|
1301
|
+
bytesAfter: null
|
|
1302
|
+
};
|
|
1303
|
+
}),
|
|
1304
|
+
committed: 0,
|
|
1305
|
+
restored: 0,
|
|
1306
|
+
failed: []
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function closeEnvelope(envelope, changes) {
|
|
1310
|
+
envelope.committed = envelope.entries.filter((entry) => entry.committed).length;
|
|
1311
|
+
envelope.restored = envelope.entries.filter((entry) => entry.restored).length;
|
|
1312
|
+
for (const [i, entry] of envelope.entries.entries()) {
|
|
1313
|
+
try {
|
|
1314
|
+
entry.bytesAfter = statSync(changes[i].path).size;
|
|
1315
|
+
} catch {
|
|
1316
|
+
entry.bytesAfter = null;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return envelope;
|
|
1320
|
+
}
|
|
1321
|
+
async function commitPlan(projectPath, projectId, request, plan) {
|
|
1322
|
+
const { changes, warnings, summary, preImages, batch } = plan;
|
|
1323
|
+
const snapshots = new Map(changes.map((change) => [change.path, snapshotOf(change.path)]));
|
|
1324
|
+
if (snapshots.size !== changes.length) {
|
|
1325
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1326
|
+
const dupes = changes.filter((change) => seen.has(change.path) ? true : (seen.add(change.path), false)).map((change) => relativeTo(projectPath, change.path));
|
|
1327
|
+
throw new Error(`bb_edit wrote nothing: two planned changes target the same file (${[...new Set(dupes)].join(", ")}) \u2014 a plan defect, not a request defect: each change is resolved against the file's pre-edit bytes, so only one of them could ever be applied.`);
|
|
1328
|
+
}
|
|
1329
|
+
const envelope = plannedEnvelope(projectPath, plan);
|
|
1330
|
+
const envelopeByAbs = new Map(changes.map((change, i) => [change.path, envelope.entries[i]]));
|
|
1331
|
+
const moved = preImageMismatches(
|
|
1332
|
+
projectPath,
|
|
1333
|
+
preImages,
|
|
1334
|
+
new Map([...snapshots].map(([path0, snapshot]) => [path0, snapshot.hash]))
|
|
1335
|
+
);
|
|
1336
|
+
if (moved.length > 0) {
|
|
1337
|
+
throw new Error(
|
|
1338
|
+
`bb_edit wrote nothing: ${moved.length} target file(s) changed on disk between planning and committing \u2014 ${moved.join(", ")}. The edit was planned from those bytes, so applying it now would overwrite content it never read; re-run the edit to re-plan against the current file.`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const committed = [];
|
|
1342
|
+
const temps = [];
|
|
1343
|
+
try {
|
|
1344
|
+
for (const change of changes) {
|
|
1345
|
+
mkdirSync(path2.dirname(change.path), { recursive: true });
|
|
1346
|
+
const temporary = `${change.path}.bb-edit-${randomUUID()}`;
|
|
1347
|
+
temps.push(temporary);
|
|
1348
|
+
writeFileSync2(temporary, change.after, "utf8");
|
|
1349
|
+
const originalMode = snapshots.get(change.path).mode;
|
|
1350
|
+
renameSync(temporary, change.path);
|
|
1351
|
+
committed.push(change.path);
|
|
1352
|
+
envelopeByAbs.get(change.path).committed = true;
|
|
1353
|
+
if (originalMode !== null) {
|
|
1354
|
+
try {
|
|
1355
|
+
chmodSync(change.path, originalMode);
|
|
1356
|
+
} catch {
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const formatting = request.format === false ? void 0 : await Promise.all(changes.map(async (change) => {
|
|
1361
|
+
const outcome = await BrainSurgicalPatcher.tryAutoFormat(change.path);
|
|
1362
|
+
return { path: relativeTo(projectPath, change.path), ...outcome };
|
|
1363
|
+
}));
|
|
1364
|
+
for (const entry of formatting ?? []) {
|
|
1365
|
+
if (entry.ran && !entry.ok) warnings.push(`format: ${entry.path}: ${entry.reason ?? "formatter failed"}`);
|
|
1366
|
+
}
|
|
1367
|
+
if (formatting) {
|
|
1368
|
+
const absoluteByRel = new Map(changes.map((change) => [relativeTo(projectPath, change.path), change.path]));
|
|
1369
|
+
for (const entry of formatting) {
|
|
1370
|
+
if (!entry.ran || !entry.ok) continue;
|
|
1371
|
+
const absolute = absoluteByRel.get(entry.path);
|
|
1372
|
+
const preImage = absolute ? snapshots.get(absolute)?.bytes : null;
|
|
1373
|
+
if (!absolute || !preImage) continue;
|
|
1374
|
+
if (dominantNewline(preImage.toString("utf8")) !== "\r\n") continue;
|
|
1375
|
+
const current = readFileSync2(absolute, "utf8");
|
|
1376
|
+
if (!hasLoneLf(current)) continue;
|
|
1377
|
+
writeFileSync2(absolute, withNewline(current, "\r\n"), "utf8");
|
|
1378
|
+
entry.lineEndingRestored = true;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
for (const change of changes) await validateFile(change.path, readFileSync2(change.path, "utf8"));
|
|
1382
|
+
const reindexResult = await reindexFilesParallel(projectId, realpathSync(projectPath), changes.map((change) => ({ path: change.path, type: snapshots.get(change.path).existed ? "modified" : "added" })));
|
|
1383
|
+
const reindexErrors = reindexResult.errors ?? 0;
|
|
1384
|
+
const reindexed = reindexErrors === 0;
|
|
1385
|
+
if (reindexErrors > 0) {
|
|
1386
|
+
const named = (reindexResult.errorDetail ?? []).slice(0, 5).map((d) => `${relativeTo(projectPath, d.file)} (${d.error})`);
|
|
1387
|
+
warnings.push(`reindex: ${reindexErrors} file(s) failed to reindex \u2014 the edit stands but the index is NOT current for them${named.length > 0 ? `: ${named.join("; ")}` : ""}; run bb index --changed`);
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
mode: request.mode,
|
|
1391
|
+
dryRun: false,
|
|
1392
|
+
changes: changes.map((change) => ({ ...change, after: readFileSync2(change.path, "utf8") })),
|
|
1393
|
+
formatted: formatting !== void 0 && formatting.length > 0 && formatting.every((entry) => entry.ok),
|
|
1394
|
+
...formatting !== void 0 ? { formatting } : {},
|
|
1395
|
+
validated: true,
|
|
1396
|
+
reindexed,
|
|
1397
|
+
// visible derived-data outcome — never a silent deferral.
|
|
1398
|
+
derived: reindexResult.derived ?? { pass: "deferred", phases: [], repairedInEdges: 0, touchedSymbols: 0 },
|
|
1399
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
1400
|
+
summary,
|
|
1401
|
+
...batch ? { batch } : {},
|
|
1402
|
+
...plan.importPruning ? { importPruning: plan.importPruning } : {},
|
|
1403
|
+
envelope: closeEnvelope(envelope, changes)
|
|
1404
|
+
};
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
const rollbackFailures = [];
|
|
1407
|
+
for (const file of committed) {
|
|
1408
|
+
const snapshot = snapshots.get(file);
|
|
1409
|
+
try {
|
|
1410
|
+
if (snapshot.existed) writeBytesPreservingMode(file, snapshot.bytes, snapshot.mode);
|
|
1411
|
+
else rmSync(file, { force: true });
|
|
1412
|
+
envelopeByAbs.get(file).restored = true;
|
|
1413
|
+
} catch (e) {
|
|
1414
|
+
rollbackFailures.push(`${file}: ${e?.message ?? String(e)}`);
|
|
1415
|
+
envelope.failed.push({ path: relativeTo(projectPath, file), reason: e?.message ?? String(e) });
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
for (const t of temps) {
|
|
1419
|
+
try {
|
|
1420
|
+
rmSync(t, { force: true });
|
|
1421
|
+
} catch {
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (rollbackFailures.length > 0) {
|
|
1425
|
+
;
|
|
1426
|
+
error.rollbackFailures = rollbackFailures;
|
|
1427
|
+
error.message = `${error.message} \u2014 ROLLBACK INCOMPLETE, these files were NOT restored: ${rollbackFailures.join("; ")}`;
|
|
1428
|
+
}
|
|
1429
|
+
;
|
|
1430
|
+
error.envelope = closeEnvelope(envelope, changes);
|
|
1431
|
+
throw error;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
async function executeEdit(projectPath, projectId, request) {
|
|
1435
|
+
let plan;
|
|
1436
|
+
try {
|
|
1437
|
+
plan = request.mode === "batch" ? await planBatch(projectPath, projectId, request) : await planEdit(projectPath, projectId, request);
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
if (error && typeof error === "object" && error.envelope === void 0) {
|
|
1440
|
+
error.envelope = { atomic: true, entries: [], committed: 0, restored: 0, failed: [] };
|
|
1441
|
+
error.refusedBeforeWriting = true;
|
|
1442
|
+
}
|
|
1443
|
+
throw error;
|
|
1444
|
+
}
|
|
1445
|
+
if (request.dryRun === true) {
|
|
1446
|
+
const { changes, warnings, summary, batch } = plan;
|
|
1447
|
+
return {
|
|
1448
|
+
mode: request.mode,
|
|
1449
|
+
dryRun: true,
|
|
1450
|
+
changes,
|
|
1451
|
+
formatted: false,
|
|
1452
|
+
validated: false,
|
|
1453
|
+
reindexed: false,
|
|
1454
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
1455
|
+
summary,
|
|
1456
|
+
...batch ? { batch } : {},
|
|
1457
|
+
...plan.importPruning ? { importPruning: plan.importPruning } : {},
|
|
1458
|
+
envelope: plannedEnvelope(projectPath, plan)
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
return commitPlan(projectPath, projectId, request, plan);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
export {
|
|
1465
|
+
resolveSafeEditPath,
|
|
1466
|
+
fileHash,
|
|
1467
|
+
preImageMismatches,
|
|
1468
|
+
WRITE_GUARD,
|
|
1469
|
+
lineCount,
|
|
1470
|
+
writeGuardMessage,
|
|
1471
|
+
isBindingParentType,
|
|
1472
|
+
isReferenceSite,
|
|
1473
|
+
findDeclarationName,
|
|
1474
|
+
BATCH_ENTRY_MODES,
|
|
1475
|
+
BATCH_WIDE_OPTIONS,
|
|
1476
|
+
MAX_BATCH_EDITS,
|
|
1477
|
+
expandBatchRequest,
|
|
1478
|
+
executeEdit
|
|
1479
|
+
};
|