hivecode-mcp 0.6.0 → 0.8.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/icr.js ADDED
@@ -0,0 +1,272 @@
1
+ // ICR — Intent-aware Code Replication (v1 proof-of-concept).
2
+ //
3
+ // The first real step from Hivecode-the-tool toward a FOUNDATIONAL primitive:
4
+ // merge code by STRUCTURE and INTENT, not characters, with a hard guarantee — never
5
+ // emit code more broken than its inputs.
6
+ //
7
+ // Why this matters: CRDTs (Yjs) merge characters. They will happily fuse two edits
8
+ // into syntactically-valid garbage, because they have no idea what code MEANS. ICR
9
+ // parses the code, merges at the level of declarations (functions / classes / vars),
10
+ // descends INTO a declaration both sides touched (so disjoint edits inside one function
11
+ // still merge), detects when two authors changed the SAME thing (a real conflict), and
12
+ // validates that the merged result still parses — falling back safely if it wouldn't.
13
+ //
14
+ // On top of structure it adds an INTENT layer:
15
+ // • RENAME DETECTION — a declaration gone from base whose identical-bodied twin appears
16
+ // under a new name is recognized as a rename; stale call sites are rewritten so the
17
+ // other agent's fresh calls keep working. Nothing else does this.
18
+ // • DANGLING REFERENCE — a declaration removed/renamed but still referenced is flagged
19
+ // as a semantic conflict even though the code parses. CRDTs/git/plain merge miss it.
20
+ //
21
+ // LANGUAGE-AGNOSTIC BY DESIGN: this file is the engine. Everything that knows about a
22
+ // specific language (how to parse, what a declaration is, how to find references) lives
23
+ // behind a provider (see lang-js.js). Adding Python/Go/Rust = writing another provider
24
+ // and registerLanguage()-ing it; the merge logic below never changes. JavaScript via
25
+ // `acorn` is simply the first provider. The production path swaps in tree-sitter
26
+ // providers for every language behind this same interface.
27
+
28
+ import { javascript } from './lang-js.js'
29
+ import { braceLanguages } from './lang-brace.js'
30
+ import { python } from './lang-python.js'
31
+
32
+ // --- language registry ----------------------------------------------------------
33
+ // JavaScript (acorn, full intent layer) first; then structural providers for the
34
+ // C-family (TypeScript, Go, Rust, Java, C/C++, C#, Swift, Kotlin, …) and Python.
35
+ // These cover disjoint extensions, so order only matters on clashes (none today).
36
+ const LANGUAGES = [javascript, ...braceLanguages, python]
37
+
38
+ // Register an additional language provider. Must expose the provider contract:
39
+ // { id, exts:[...], parses, units, declaredNames, usedIdentifiers, declBody,
40
+ // renameRefs, fnParts }. Newest wins on extension clashes.
41
+ export function registerLanguage(provider) { LANGUAGES.unshift(provider) }
42
+
43
+ function extname(p) { const m = /\.[^.\/\\]+$/.exec(p || ''); return m ? m[0].toLowerCase() : '' }
44
+
45
+ // The provider for a filename, or null if no registered language claims its extension.
46
+ export function languageFor(filename) {
47
+ const e = extname(filename)
48
+ return LANGUAGES.find((l) => l.exts.includes(e)) || null
49
+ }
50
+
51
+ // Can ICR merge this file structurally? (Callers gate on this before trying.)
52
+ export function supports(filename) { return languageFor(filename) != null }
53
+
54
+ // --- generic merge primitives (language-independent: operate on {key,text}) -----
55
+ const mapOf = (us) => new Map(us.map((u) => [u.key, u.text]))
56
+ function dedupeOrder(keys) { const seen = new Set(), out = []; for (const k of keys) if (!seen.has(k)) { seen.add(k); out.push(k) } return out }
57
+
58
+ // For a given unit, choose which version survives: whoever CHANGED it wins; if neither
59
+ // changed it, keep the base.
60
+ function pick(k, B, A, Bb) {
61
+ const base = B.has(k) ? B.get(k) : null
62
+ const a = A.has(k) ? A.get(k) : null
63
+ const b = Bb.has(k) ? Bb.get(k) : null
64
+ if (a != null && a !== base) return a
65
+ if (b != null && b !== base) return b
66
+ return base != null ? base : (a != null ? a : b)
67
+ }
68
+
69
+ // Core 3-way merge over a KEYED list of units (works at file level AND, recursively,
70
+ // inside a single declaration). Returns { conflicts:[...], parts:[...text] }.
71
+ // When both sides change the SAME key into different things, it first tries to descend
72
+ // INTO that unit (finer granularity) — only a clash it can't resolve becomes a conflict.
73
+ function mergeKeyed(lang, baseU, aU, bU) {
74
+ const B = mapOf(baseU), A = mapOf(aU), Bb = mapOf(bU)
75
+ const keys = dedupeOrder([...baseU.map((u) => u.key), ...aU.map((u) => u.key), ...bU.map((u) => u.key)])
76
+
77
+ const conflicts = [], resolved = new Map()
78
+ for (const k of keys) {
79
+ const bs = B.has(k) ? B.get(k) : null
80
+ const as_ = A.has(k) ? A.get(k) : null
81
+ const bbs = Bb.has(k) ? Bb.get(k) : null
82
+ const changedA = as_ !== bs, changedB = bbs !== bs
83
+ if (changedA && changedB && as_ !== bbs) {
84
+ // Both touched the same unit, differently. Try finer-grained inner merge first.
85
+ if (bs != null && as_ != null && bbs != null) {
86
+ const inner = tryInnerMerge(lang, bs, as_, bbs)
87
+ if (inner != null) { resolved.set(k, inner); continue }
88
+ }
89
+ conflicts.push(k)
90
+ }
91
+ }
92
+ if (conflicts.length) return { conflicts, parts: [] }
93
+
94
+ // No unresolved clashes → decide which keys survive, honoring one-sided deletions.
95
+ const survives = new Set()
96
+ for (const k of keys) {
97
+ const inBase = B.has(k), inA = A.has(k), inB2 = Bb.has(k)
98
+ if (inBase && (!inA || !inB2)) {
99
+ const deletedByA = !inA, deletedByB = !inB2
100
+ const otherUnchangedFromBase =
101
+ (deletedByA && (!inB2 || Bb.get(k) === B.get(k))) ||
102
+ (deletedByB && (!inA || A.get(k) === B.get(k)))
103
+ if (otherUnchangedFromBase) continue // accept the deletion
104
+ }
105
+ survives.add(k)
106
+ }
107
+
108
+ // CONVERGENCE: the output order must be IDENTICAL no matter which side is `a` vs `b`,
109
+ // or two peers compute different text and never settle (then re-merging the divergence
110
+ // corrupts the file). So: surviving base units keep their base order; units added by a
111
+ // side are appended in a canonical (sorted) order — deterministic and symmetric.
112
+ const baseOrder = dedupeOrder(baseU.map((u) => u.key))
113
+ const baseSet = new Set(baseOrder)
114
+ const final = []
115
+ for (const k of baseOrder) if (survives.has(k)) final.push(k)
116
+ for (const k of [...survives].filter((k) => !baseSet.has(k)).sort()) final.push(k)
117
+
118
+ // Assemble, recording PROVENANCE per surviving unit: who authored the version we kept
119
+ // ('a' / 'b' changed it, 'both' if we merged inside it, 'base' if unchanged).
120
+ const parts = [], provenance = [], order = [], textByKey = new Map()
121
+ for (const k of final) {
122
+ let text, from
123
+ if (resolved.has(k)) { text = resolved.get(k); from = 'both' }
124
+ else {
125
+ text = pick(k, B, A, Bb)
126
+ const bs = B.has(k) ? B.get(k) : null, as_ = A.has(k) ? A.get(k) : null, bbs = Bb.has(k) ? Bb.get(k) : null
127
+ from = (as_ != null && as_ !== bs) ? 'a' : (bbs != null && bbs !== bs) ? 'b' : 'base'
128
+ }
129
+ if (text != null) { parts.push(text); provenance.push({ key: k, from }); order.push(k); textByKey.set(k, text) }
130
+ }
131
+ return { conflicts: [], parts, provenance, order, textByKey }
132
+ }
133
+
134
+ // FINER GRANULARITY: both sides edited the same function OR class. If the signature
135
+ // matches and they edited DIFFERENT inner units (statements / class members), merge those
136
+ // recursively rather than declaring the whole declaration a conflict. Returns merged
137
+ // text, or null meaning "couldn't safely merge inside — treat as a real conflict."
138
+ function tryInnerMerge(lang, baseText, aText, bText) {
139
+ // Language-specific same-key merges first (e.g. unioning import specifiers).
140
+ if (lang.mergeUnit) {
141
+ const t = lang.mergeUnit(baseText, aText, bText)
142
+ if (t != null && lang.parses(t)) return t
143
+ }
144
+ if (!lang.splitUnit) return null
145
+ const pb = lang.splitUnit(baseText), pa = lang.splitUnit(aText), pbb = lang.splitUnit(bText)
146
+ if (!pb || !pa || !pbb) return null
147
+ if (pa.sig.trim() !== pbb.sig.trim()) return null // signature itself changed → real conflict
148
+ const m = mergeKeyed(lang, pb.units, pa.units, pbb.units)
149
+ if (m.conflicts.length) return null
150
+ const text = pa.sig + pa.open + m.parts.join(pa.join) + pa.close
151
+ const valid = lang.parsesUnit || lang.parses // accept class-member fragments when supported
152
+ return valid(text) ? text : null
153
+ }
154
+
155
+ // FORMAT-PRESERVING assembly: rebuild the merged file by splicing the merged units back
156
+ // into the BASE text at their original byte ranges. Unchanged units — and all the
157
+ // whitespace/comments BETWEEN units — survive verbatim; only changed units carry the
158
+ // editing side's bytes, and genuinely new units are appended. This is what stops ICR from
159
+ // reformatting code it merges. Deterministic and symmetric, so peers converge.
160
+ function spliceUnits(baseText, baseUnits, order, textByKey) {
161
+ const baseKeySet = new Set(baseUnits.map((u) => u.key))
162
+ const surviving = new Set(order.filter((k) => baseKeySet.has(k)))
163
+ const emitted = new Set()
164
+ let out = '', pos = 0
165
+ for (const u of baseUnits) {
166
+ out += baseText.slice(pos, u.start) // gap before this unit (comments/blank lines), verbatim
167
+ if (surviving.has(u.key) && !emitted.has(u.key)) { out += textByKey.get(u.key); emitted.add(u.key) }
168
+ pos = u.end
169
+ }
170
+ const added = order.filter((k) => !baseKeySet.has(k))
171
+ if (added.length) {
172
+ const head = out.replace(/\s*$/, '')
173
+ out = (head ? head + '\n\n' : '') + added.map((k) => textByKey.get(k)).join('\n\n') + '\n'
174
+ } else {
175
+ out += baseText.slice(pos) // trailing whitespace (final newline) verbatim
176
+ }
177
+ return out.replace(/^\n+/, '') // drop orphan leading blank lines left by a deleted first unit
178
+ }
179
+
180
+ // --- public API -----------------------------------------------------------------
181
+
182
+ // Does this source parse under the given (or default JS) language?
183
+ export function parses(src, lang = javascript) { return lang.parses(src) }
184
+
185
+ // 3-way STRUCTURAL + INTENT merge of `a` and `b` against common ancestor `base`.
186
+ // opts: { lang } a provider, or { filename } to pick one by extension (defaults to JS).
187
+ // Returns { status, text, conflicts, renames? }:
188
+ // 'auto' — clean merge; `text` is valid, parseable code.
189
+ // 'semantic-conflict' — same declaration changed both sides, or a dangling reference;
190
+ // `conflicts` names them (e.g. ['fn:login'] or ['ref:helper']).
191
+ // 'fallback' — couldn't merge safely (unparseable input, or the merge wouldn't
192
+ // parse, or unsupported language); caller keeps both. Never broken.
193
+ export function structuralMerge(base, a, b, opts = {}) {
194
+ const lang = opts.lang || (opts.filename ? languageFor(opts.filename) : javascript) || javascript
195
+
196
+ if (!lang.parses(base) || !lang.parses(a) || !lang.parses(b))
197
+ return { status: 'fallback', text: null, conflicts: [], reason: 'unparseable input' }
198
+
199
+ // FIXED-POINT fast path: once two peers agree on a text T, re-merging must return T
200
+ // unchanged. This is what makes the live sync settle. Both agree ⇒ nothing to check.
201
+ if (a === b) return { status: 'auto', text: a, conflicts: [], renames: [], provenance: [] }
202
+
203
+ const authors = opts.authors || {}
204
+ const attribute = (prov) => (prov || []).map((p) => ({ unit: p.key, author: authors[p.from] || p.from }))
205
+
206
+ // Determine the structurally-merged text + provenance. A one-sided change takes that
207
+ // side verbatim (format perfectly preserved); otherwise do the keyed 3-way merge and
208
+ // splice it back into base. Either way the INTENT layer below still runs, so a deletion
209
+ // that leaves a dangling reference is caught even when only one side changed.
210
+ let text, provenance
211
+ if (a === base) { text = b; provenance = [] }
212
+ else if (b === base) { text = a; provenance = [] }
213
+ else {
214
+ const baseUnits = lang.units(base)
215
+ const merge = mergeKeyed(lang, baseUnits, lang.units(a), lang.units(b))
216
+ if (merge.conflicts.length) return { status: 'semantic-conflict', text: null, conflicts: merge.conflicts }
217
+ // Prefer the format-preserving splice (units carry byte ranges); fall back to a plain
218
+ // join for languages whose units don't expose ranges.
219
+ const canSplice = baseUnits.every((u) => typeof u.start === 'number')
220
+ text = canSplice ? spliceUnits(base, baseUnits, merge.order, merge.textByKey) : merge.parts.join('\n\n') + '\n'
221
+ provenance = merge.provenance
222
+ }
223
+
224
+ // THE GUARANTEE: the merged result must parse, or we refuse it.
225
+ if (!lang.parses(text)) return { status: 'fallback', text: null, conflicts: [], reason: 'merge would not parse' }
226
+
227
+ // RENAME DETECTION (intent): a declaration gone from base whose identical-bodied twin
228
+ // appears under a new name is a rename — rewrite stale call sites to the new name.
229
+ let merged = text
230
+ const baseNames = lang.declaredNames(base)
231
+ let mergedNames = lang.declaredNames(merged)
232
+ let removed = [...baseNames].filter((n) => !mergedNames.has(n))
233
+ const renames = []
234
+ const rename = lang.renameFreeRefs || lang.renameRefs // prefer scope-aware rewriting
235
+ if (removed.length && lang.declBody && rename) {
236
+ const added = [...mergedNames].filter((n) => !baseNames.has(n))
237
+ const claimed = new Set()
238
+ for (const oldName of removed) {
239
+ const oldBody = lang.declBody(base, oldName)
240
+ if (oldBody == null) continue
241
+ const match = added.find((n) => !claimed.has(n) && lang.declBody(merged, n) === oldBody)
242
+ if (match) { renames.push([oldName, match]); claimed.add(match) }
243
+ }
244
+ let rewritten = merged
245
+ // Rewrite only references that resolve to the renamed binding (scope-aware) — a local
246
+ // variable that merely shares the old name is left untouched.
247
+ for (const [oldName, newName] of renames) rewritten = rename(rewritten, oldName, newName)
248
+ // only accept the rewrite if it still parses — never trade a parse error for intent.
249
+ if (renames.length && lang.parses(rewritten)) { merged = rewritten; mergedNames = lang.declaredNames(merged) }
250
+ }
251
+
252
+ // DANGLING REFERENCE (intent): a declaration removed (and NOT explained by a rename)
253
+ // but still referenced is a semantic conflict even though the code parses cleanly.
254
+ // Uses the SCOPE-AWARE reference set when the language provides one (so a local binding
255
+ // that merely shares the deleted name is correctly NOT treated as a reference to it),
256
+ // falling back to the approximate identifier set otherwise.
257
+ removed = [...baseNames].filter((n) => !mergedNames.has(n))
258
+ const refs = lang.referencedFreeNames ? lang.referencedFreeNames(merged)
259
+ : lang.usedIdentifiers ? lang.usedIdentifiers(merged) : null
260
+ if (removed.length && refs) {
261
+ const dangling = removed.filter((n) => refs.has(n))
262
+ if (dangling.length)
263
+ return { status: 'semantic-conflict', text: null, conflicts: dangling.map((n) => 'ref:' + n), reason: 'dangling reference: a declaration was removed/renamed but is still used' }
264
+ }
265
+ // PROVENANCE: attribute each surviving unit to the author whose version we kept. With
266
+ // opts.authors = { a, b, base } the labels are real names; otherwise they're 'a'/'b'/etc.
267
+ return {
268
+ status: 'auto', text: merged, conflicts: [],
269
+ renames: renames.map(([o, n]) => o + '->' + n),
270
+ provenance: attribute(provenance),
271
+ }
272
+ }
package/lang-brace.js ADDED
@@ -0,0 +1,174 @@
1
+ // Brace-family language provider for ICR — a dependency-free, language-agnostic
2
+ // structural splitter for C-style languages (TypeScript, Go, Rust, Java, C/C++,
3
+ // C#, Swift, Kotlin, Scala, PHP, Dart, …). It implements the ICR provider contract
4
+ // { id, exts, parses, units, declaredNames, splitUnit, parsesUnit } so the SAME
5
+ // merge engine in icr.js merges these languages by structure — different top-level
6
+ // declarations merge cleanly, the same declaration edited two ways is a named
7
+ // conflict, and edits to different members inside one class/function merge via the
8
+ // engine's finer-grained inner merge (splitUnit).
9
+ //
10
+ // It is a heuristic scanner, not a full parser: it tracks strings, line/block
11
+ // comments, and bracket depth to find top-level declarations and their byte ranges.
12
+ // This gives real structural merge and an approximate parses() (balanced delimiters)
13
+ // — honest and useful today; a tree-sitter provider can later replace it behind the
14
+ // same interface with zero engine changes.
15
+
16
+ const LINE = ['//']
17
+ const OPEN = { '{': '}', '(': ')', '[': ']' }
18
+ const CLOSE = { '}': '{', ')': '(', ']': '[' }
19
+
20
+ // Walk `src` once. `at(i)` returns the index just past a string or comment starting
21
+ // at i, or -1 if none starts there — so callers can skip over them uniformly.
22
+ function scannerAt(src, strings) {
23
+ const n = src.length
24
+ return function at(i) {
25
+ if (src[i] === '/' && src[i + 1] === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; return Math.min(n, j + 2) }
26
+ for (const lc of LINE) if (src.startsWith(lc, i)) { let j = i + lc.length; while (j < n && src[j] !== '\n') j++; return j }
27
+ if (strings.includes(src[i])) { const q = src[i]; let j = i + 1; while (j < n) { if (src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } j++ } return j }
28
+ return -1
29
+ }
30
+ }
31
+
32
+ const isWS = (c) => c === ' ' || c === '\t' || c === '\r' || c === '\n'
33
+
34
+ // Extract top-level units [{start,end}] from src[from..to]. A unit is a maximal
35
+ // declaration/statement at bracket depth 0: either a block (signature + balanced
36
+ // {…}) or a statement ending at ';' or end-of-line.
37
+ function topUnits(src, strings, from = 0, to = src.length) {
38
+ const at = scannerAt(src, strings)
39
+ const units = []
40
+ let i = from
41
+ while (i < to) {
42
+ while (i < to && isWS(src[i])) i++
43
+ if (i >= to) break
44
+ // A standalone comment before a declaration belongs to the GAP (kept verbatim by
45
+ // the engine's splice), so skip it without starting a unit.
46
+ if ((src[i] === '/' && (src[i + 1] === '/' || src[i + 1] === '*'))) { const s = at(i); if (s > i) { i = s; continue } }
47
+ const start = i
48
+ let depth = 0, sawBrace = false
49
+ while (i < to) {
50
+ const s = at(i)
51
+ if (s > i) { i = s; continue } // skip string / comment
52
+ const c = src[i]
53
+ if (OPEN[c]) { if (c === '{') sawBrace = true; depth++; i++; continue }
54
+ if (CLOSE[c]) { depth--; i++; if (depth <= 0 && sawBrace) { while (i < to && (src[i] === ' ' || src[i] === '\t')) i++; if (src[i] === ';') i++; break } continue }
55
+ if (depth === 0) {
56
+ if (c === ';') { i++; break }
57
+ if (c === '\n' && !sawBrace) break
58
+ }
59
+ i++
60
+ }
61
+ units.push({ start, end: i })
62
+ }
63
+ return units
64
+ }
65
+
66
+ const DECL = /\b(function|func|fn|def|fun|class|interface|struct|enum|trait|impl|type|namespace|module|package|object|record|protocol|actor|extension)\s+([A-Za-z_$][\w$]*)/
67
+ const VARD = /\b(const|let|var|val|static|final)\s+([A-Za-z_$][\w$]*)/
68
+ // Method/function signature, including TYPE-FIRST forms (`int a()`, `public static
69
+ // void main(String[] a)`, `std::string name()`): optional decorators + any number of
70
+ // modifiers + an optional return-type token, then NAME(args){. The captured name is
71
+ // the identifier directly before `(`.
72
+ const METH = /^(?:@[\w.()]+\s+)*(?:(?:public|private|protected|internal|static|async|final|override|open|pub|suspend|inline|virtual|export|default|abstract|sealed|unsafe|extern|synchronized|native|constexpr)\s+)*(?:(?:function|fn|func|fun)\s+)?(?:[A-Za-z_$][\w$<>\[\].,:&*\s]*?\s+)?([A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*\([^;{]*\)\s*(?:->[^{]+|:[^={]+)?\s*\{/
73
+ // control-flow headers also look like `name (…) {` — never key them as methods
74
+ const CTRL = new Set(['if', 'for', 'while', 'switch', 'catch', 'do', 'else', 'try', 'return', 'new', 'throw', 'match', 'select', 'when', 'until', 'lock', 'using', 'foreach', 'with'])
75
+
76
+ function keyOf(text) {
77
+ const head = text.replace(/^\s+/, '')
78
+ let m = head.match(DECL); if (m) return m[1] + ':' + m[2]
79
+ m = head.match(METH); if (m && !CTRL.has(m[1])) return 'method:' + m[1]
80
+ m = head.match(VARD); if (m) return 'var:' + m[2]
81
+ return 'stmt:' + head.split('\n')[0].trim().replace(/\s+/g, ' ')
82
+ }
83
+
84
+ // De-duplicate keys deterministically (repeated statements / same-named units) so the
85
+ // engine's keyed merge never silently drops one. Order is stable → convergent.
86
+ function keyedUnits(raw, src) {
87
+ const seen = new Map(), out = []
88
+ for (const u of raw) {
89
+ const text = src.slice(u.start, u.end)
90
+ let k = keyOf(text)
91
+ const c = (seen.get(k) || 0) + 1; seen.set(k, c)
92
+ if (c > 1) k += '#' + c
93
+ out.push({ key: k, text, start: u.start, end: u.end })
94
+ }
95
+ return out
96
+ }
97
+
98
+ // Approximate parse check: delimiters balanced and no unterminated string/comment,
99
+ // ignoring the contents of strings and comments.
100
+ function balanced(src, strings) {
101
+ const at = scannerAt(src, strings)
102
+ const stack = []
103
+ let i = 0
104
+ while (i < src.length) {
105
+ const s = at(i)
106
+ if (s > i) { if (s > src.length) return false; i = s; continue }
107
+ const c = src[i]
108
+ if (OPEN[c]) stack.push(c)
109
+ else if (CLOSE[c]) { if (stack.pop() !== CLOSE[c]) return false }
110
+ i++
111
+ }
112
+ return stack.length === 0
113
+ }
114
+
115
+ export function makeBraceLanguage({ id, exts, strings = ['"', "'", '`'] }) {
116
+ const provider = {
117
+ id,
118
+ exts,
119
+ parses: (src) => balanced(String(src == null ? '' : src), strings),
120
+ units: (src) => keyedUnits(topUnits(String(src), strings), String(src)),
121
+ declaredNames: (src) => {
122
+ const names = new Set()
123
+ for (const u of keyedUnits(topUnits(String(src), strings), String(src))) {
124
+ const m = u.key.match(/^(?:[a-z]+):([A-Za-z_$][\w$]*)/)
125
+ if (m && !u.key.startsWith('stmt:')) names.add(m[1])
126
+ }
127
+ return names
128
+ },
129
+ parsesUnit: (src) => balanced(String(src), strings),
130
+ // Finer granularity: descend one level into a block declaration so two agents
131
+ // editing DIFFERENT members of the same class/function merge instead of clashing.
132
+ splitUnit: (text) => {
133
+ const src = String(text)
134
+ // Only descend into NAMED containers (class/struct/impl/…), whose members are
135
+ // keyed by name — so editing different members merges. A function/method body is
136
+ // just statements (no stable keys); descending there would falsely merge two
137
+ // edits to the same code, so we DON'T — the whole unit becomes a clean conflict.
138
+ if (!/\b(class|interface|struct|enum|trait|impl|namespace|module|object|record|extension|protocol|actor)\b/.test(src.split('{')[0]))
139
+ return null
140
+ const at = scannerAt(src, strings)
141
+ let i = 0, open = -1
142
+ while (i < src.length) { const s = at(i); if (s > i) { i = s; continue } if (src[i] === '{') { open = i; break } i++ }
143
+ if (open < 0) return null
144
+ // find matching close
145
+ let depth = 0, j = open, close = -1
146
+ while (j < src.length) { const s = at(j); if (s > j) { j = s; continue } const c = src[j]; if (c === '{') depth++; else if (c === '}') { depth--; if (depth === 0) { close = j; break } } j++ }
147
+ if (close < 0) return null
148
+ const sig = src.slice(0, open)
149
+ const body = src.slice(open + 1, close)
150
+ const inner = keyedUnits(topUnits(body, strings), body)
151
+ // re-base inner ranges into `body` coordinates are already correct (topUnits used body)
152
+ return { sig, open: '{', close: src.slice(close), body, units: inner, join: '' }
153
+ },
154
+ }
155
+ return provider
156
+ }
157
+
158
+ // Ready-made providers covering the common C-family languages. One provider can claim
159
+ // many extensions; comment/string syntax defaults fit the whole family (edge cases like
160
+ // Rust raw strings / lifetimes are tolerated — worst case a unit is merged coarsely).
161
+ export const braceLanguages = [
162
+ makeBraceLanguage({ id: 'ts', exts: ['.ts', '.tsx', '.mts', '.cts'] }),
163
+ makeBraceLanguage({ id: 'jsx', exts: ['.jsx'] }),
164
+ makeBraceLanguage({ id: 'go', exts: ['.go'], strings: ['"', '`'] }),
165
+ makeBraceLanguage({ id: 'rust', exts: ['.rs'] }),
166
+ makeBraceLanguage({ id: 'java', exts: ['.java'] }),
167
+ makeBraceLanguage({ id: 'c', exts: ['.c', '.h', '.cpp', '.hpp', '.cc', '.hh', '.cxx'] }),
168
+ makeBraceLanguage({ id: 'csharp', exts: ['.cs'] }),
169
+ makeBraceLanguage({ id: 'swift', exts: ['.swift'] }),
170
+ makeBraceLanguage({ id: 'kotlin', exts: ['.kt', '.kts'] }),
171
+ makeBraceLanguage({ id: 'scala', exts: ['.scala'] }),
172
+ makeBraceLanguage({ id: 'php', exts: ['.php'] }),
173
+ makeBraceLanguage({ id: 'dart', exts: ['.dart'] }),
174
+ ]