hivecode-mcp 0.7.0 → 0.11.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 CHANGED
@@ -26,15 +26,30 @@
26
26
  // providers for every language behind this same interface.
27
27
 
28
28
  import { javascript } from './lang-js.js'
29
+ import { braceLanguages } from './lang-brace.js'
30
+ import { python } from './lang-python.js'
31
+ import { ruby } from './lang-ruby.js'
32
+ import { json } from './lang-json.js'
33
+ import { yaml } from './lang-yaml.js'
34
+ import { toml } from './lang-toml.js'
29
35
 
30
36
  // --- language registry ----------------------------------------------------------
31
- const LANGUAGES = [javascript]
37
+ // JavaScript (acorn, full intent layer) first; then structural providers for the
38
+ // C-family (TypeScript, Go, Rust, Java, C/C++, C#, Swift, Kotlin, …), Python, and
39
+ // Ruby; data languages: JSON merges as parsed DATA (real parser, value-wise 3-way,
40
+ // perfect guarantee), YAML/TOML merge by keyed structure (sections/keys).
41
+ // These cover disjoint extensions, so order only matters on clashes (none today).
42
+ const LANGUAGES = [javascript, ...braceLanguages, python, ruby, json, yaml, toml]
32
43
 
33
44
  // Register an additional language provider. Must expose the provider contract:
34
45
  // { id, exts:[...], parses, units, declaredNames, usedIdentifiers, declBody,
35
46
  // renameRefs, fnParts }. Newest wins on extension clashes.
36
47
  export function registerLanguage(provider) { LANGUAGES.unshift(provider) }
37
48
 
49
+ // The live provider list (the actual registered objects, not copies) — used by the
50
+ // optional tree-sitter oracle to upgrade providers' parse checks in place.
51
+ export function registeredLanguages() { return LANGUAGES }
52
+
38
53
  function extname(p) { const m = /\.[^.\/\\]+$/.exec(p || ''); return m ? m[0].toLowerCase() : '' }
39
54
 
40
55
  // The provider for a filename, or null if no registered language claims its extension.
@@ -61,15 +76,80 @@ function pick(k, B, A, Bb) {
61
76
  return base != null ? base : (a != null ? a : b)
62
77
  }
63
78
 
79
+ // SIMILARITY RE-PAIRING (the matcher): a unit that "disappeared" on one side while a
80
+ // near-identical unit "appeared" on the SAME side is the same unit rewritten — e.g.
81
+ // `app.use(...)` becoming `app1.use(...)` when a variable is renamed changes the unit's
82
+ // content-derived key, and key matching alone would call that a delete + an add. Ground-
83
+ // truth replay of real repos showed this is the #1 source of false delete-vs-edit
84
+ // conflicts. Pair them back so the engine sees ONE unit changed on that side and the
85
+ // normal same-key tiers (inner merge → token splice) get their shot.
86
+ // Safety: same key KIND only (call: with call:, fn: with fn:), a high similarity bar,
87
+ // and the best match must be UNIQUE (with margin) and MUTUAL — any ambiguity means no
88
+ // pairing. Side-local and deterministic, so merge(a,b) === merge(b,a) is preserved.
89
+ const keyKind = (k) => k.slice(0, k.indexOf(':') + 1)
90
+ function shingleSet(t) {
91
+ const s = t.replace(/\s+/g, ' ').trim()
92
+ const out = new Set()
93
+ for (let i = 0; i + 4 <= s.length; i++) out.add(s.slice(i, i + 4))
94
+ return out
95
+ }
96
+ function shingleSim(sa, sb) {
97
+ if (!sa.size || !sb.size) return 0
98
+ let inter = 0
99
+ const [small, big] = sa.size <= sb.size ? [sa, sb] : [sb, sa]
100
+ for (const g of small) if (big.has(g)) inter++
101
+ return (2 * inter) / (sa.size + sb.size)
102
+ }
103
+ const SIM_FLOOR = 0.75, SIM_MARGIN = 0.04
104
+ function repairSide(baseU, sideU, otherKeys) {
105
+ const baseKeys = new Set(baseU.map((u) => u.key))
106
+ const sideKeys = new Set(sideU.map((u) => u.key))
107
+ const deleted = baseU.filter((u) => !sideKeys.has(u.key))
108
+ // An added key that ALSO appears on the other side is a genuine concurrent addition
109
+ // (both sides wrote it) — pairing it away would fabricate a duplicate and silently
110
+ // absorb a same-key add/add clash the engine must surface. Never pair those.
111
+ const added = sideU.filter((u) => !baseKeys.has(u.key) && !otherKeys.has(u.key))
112
+ if (!deleted.length || !added.length || deleted.length * added.length > 900) return sideU
113
+ const shD = deleted.map((u) => shingleSet(u.text)), shA = added.map((u) => shingleSet(u.text))
114
+ const bestFor = (sh, kind, pool, shPool) => {
115
+ let best = -1, bestS = 0, second = 0
116
+ for (let j = 0; j < pool.length; j++) {
117
+ if (keyKind(pool[j].key) !== kind) continue
118
+ const s = shingleSim(sh, shPool[j])
119
+ if (s > bestS) { second = bestS; bestS = s; best = j } else if (s > second) second = s
120
+ }
121
+ return { best, bestS, second }
122
+ }
123
+ const rekey = new Map(), claimed = new Set()
124
+ for (let i = 0; i < deleted.length; i++) {
125
+ const kind = keyKind(deleted[i].key)
126
+ const f = bestFor(shD[i], kind, added, shA)
127
+ // Short units are mostly boilerplate (`def f(): return N`), so shingle similarity
128
+ // runs hot on them — demand near-identity before calling two short units "the same".
129
+ const floor = Math.min(shD[i].size, f.best >= 0 ? shA[f.best].size : Infinity) < 24 ? 0.9 : SIM_FLOOR
130
+ if (f.best < 0 || f.bestS < floor || f.bestS - f.second < SIM_MARGIN) continue
131
+ const back = bestFor(shA[f.best], keyKind(added[f.best].key), deleted, shD)
132
+ if (back.best !== i || back.bestS - back.second < SIM_MARGIN) continue
133
+ if (claimed.has(added[f.best].key)) continue
134
+ claimed.add(added[f.best].key)
135
+ rekey.set(added[f.best].key, deleted[i].key)
136
+ }
137
+ if (!rekey.size) return sideU
138
+ return sideU.map((u) => (rekey.has(u.key) ? { ...u, key: rekey.get(u.key) } : u))
139
+ }
140
+
64
141
  // Core 3-way merge over a KEYED list of units (works at file level AND, recursively,
65
142
  // inside a single declaration). Returns { conflicts:[...], parts:[...text] }.
66
143
  // When both sides change the SAME key into different things, it first tries to descend
67
144
  // INTO that unit (finer granularity) — only a clash it can't resolve becomes a conflict.
68
- function mergeKeyed(lang, baseU, aU, bU) {
145
+ function mergeKeyed(lang, baseU, aU, bU, mopts = {}) {
146
+ const aKeys = new Set(aU.map((u) => u.key)), bKeys = new Set(bU.map((u) => u.key))
147
+ aU = repairSide(baseU, aU, bKeys)
148
+ bU = repairSide(baseU, bU, aKeys)
69
149
  const B = mapOf(baseU), A = mapOf(aU), Bb = mapOf(bU)
70
150
  const keys = dedupeOrder([...baseU.map((u) => u.key), ...aU.map((u) => u.key), ...bU.map((u) => u.key)])
71
151
 
72
- const conflicts = [], resolved = new Map()
152
+ const conflicts = [], conflictUnits = [], resolved = new Map()
73
153
  for (const k of keys) {
74
154
  const bs = B.has(k) ? B.get(k) : null
75
155
  const as_ = A.has(k) ? A.get(k) : null
@@ -78,13 +158,17 @@ function mergeKeyed(lang, baseU, aU, bU) {
78
158
  if (changedA && changedB && as_ !== bbs) {
79
159
  // Both touched the same unit, differently. Try finer-grained inner merge first.
80
160
  if (bs != null && as_ != null && bbs != null) {
81
- const inner = tryInnerMerge(lang, bs, as_, bbs)
161
+ const inner = tryInnerMerge(lang, bs, as_, bbs, mopts)
82
162
  if (inner != null) { resolved.set(k, inner); continue }
83
163
  }
84
164
  conflicts.push(k)
165
+ // Carry the three exact versions of the conflicting unit, so a conflict is a
166
+ // MACHINE-RESOLVABLE object (base/ours/theirs of just this declaration) — not a wall
167
+ // of text markers. This is what an intent-aware resolver (a judge agent) reconciles.
168
+ conflictUnits.push({ key: k, base: bs, ours: as_, theirs: bbs })
85
169
  }
86
170
  }
87
- if (conflicts.length) return { conflicts, parts: [] }
171
+ if (conflicts.length) return { conflicts, conflictUnits, parts: [] }
88
172
 
89
173
  // No unresolved clashes → decide which keys survive, honoring one-sided deletions.
90
174
  const survives = new Set()
@@ -100,15 +184,53 @@ function mergeKeyed(lang, baseU, aU, bU) {
100
184
  survives.add(k)
101
185
  }
102
186
 
103
- // CONVERGENCE: the output order must be IDENTICAL no matter which side is `a` vs `b`,
104
- // or two peers compute different text and never settle (then re-merging the divergence
105
- // corrupts the file). So: surviving base units keep their base order; units added by a
106
- // side are appended in a canonical (sorted) order — deterministic and symmetric.
187
+ // CONVERGENCE + TOPOLOGY: new units are inserted where their author put them — anchored
188
+ // to the base unit that immediately preceded them on the side that added them — while
189
+ // staying SYMMETRIC: merge(a,b) must equal merge(b,a) byte-for-byte or two live peers
190
+ // never settle. Symmetry rules: (1) a unit added by BOTH sides at different anchors gets
191
+ // the anchor earlier in base order (side-independent); (2) when both sides insert runs at
192
+ // the SAME anchor, each side's run keeps its own internal order, and the two runs are
193
+ // ordered by comparing their joined keys (content, not side).
107
194
  const baseOrder = dedupeOrder(baseU.map((u) => u.key))
108
195
  const baseSet = new Set(baseOrder)
196
+ const basePos = new Map(baseOrder.map((k, i) => [k, i]))
197
+
198
+ const anchorsFor = (units) => { // newKey -> base key it followed (null = top of file)
199
+ const m = new Map()
200
+ let lastBase = null
201
+ for (const u of units) {
202
+ if (baseSet.has(u.key)) lastBase = u.key
203
+ else if (survives.has(u.key) && !m.has(u.key)) m.set(u.key, lastBase)
204
+ }
205
+ return m
206
+ }
207
+ const aAnch = anchorsFor(aU), bAnch = anchorsFor(bU)
208
+ const anchorOf = (k) => {
209
+ const inA2 = aAnch.has(k), inB3 = bAnch.has(k)
210
+ if (inA2 && inB3) {
211
+ const x = aAnch.get(k), y = bAnch.get(k)
212
+ if (x === y) return x
213
+ const px = x == null ? -1 : basePos.get(x), py = y == null ? -1 : basePos.get(y)
214
+ return px <= py ? x : y
215
+ }
216
+ return inA2 ? aAnch.get(k) : bAnch.get(k)
217
+ }
218
+ const runsA = new Map(), runsB = new Map() // anchor -> that side's new keys, in its order
219
+ for (const k of aAnch.keys()) { const an = anchorOf(k); if (!runsA.has(an)) runsA.set(an, []); runsA.get(an).push(k) }
220
+ for (const k of bAnch.keys()) { const an = anchorOf(k); if (!runsB.has(an)) runsB.set(an, []); runsB.get(an).push(k) }
221
+ const newAt = (anchor) => {
222
+ const ra = runsA.get(anchor) || [], rb = runsB.get(anchor) || []
223
+ const [first, second] = ra.join('') <= rb.join('') ? [ra, rb] : [rb, ra]
224
+ const firstSet = new Set(first)
225
+ return [...first, ...second.filter((k) => !firstSet.has(k))]
226
+ }
227
+
109
228
  const final = []
110
- for (const k of baseOrder) if (survives.has(k)) final.push(k)
111
- for (const k of [...survives].filter((k) => !baseSet.has(k)).sort()) final.push(k)
229
+ final.push(...newAt(null))
230
+ for (const k of baseOrder) {
231
+ if (survives.has(k)) final.push(k)
232
+ final.push(...newAt(k))
233
+ }
112
234
 
113
235
  // Assemble, recording PROVENANCE per surviving unit: who authored the version we kept
114
236
  // ('a' / 'b' changed it, 'both' if we merged inside it, 'base' if unchanged).
@@ -130,52 +252,364 @@ function mergeKeyed(lang, baseU, aU, bU) {
130
252
  // matches and they edited DIFFERENT inner units (statements / class members), merge those
131
253
  // recursively rather than declaring the whole declaration a conflict. Returns merged
132
254
  // text, or null meaning "couldn't safely merge inside — treat as a real conflict."
133
- function tryInnerMerge(lang, baseText, aText, bText) {
255
+ function tryInnerMerge(lang, baseText, aText, bText, mopts = {}) {
134
256
  // Language-specific same-key merges first (e.g. unioning import specifiers).
135
257
  if (lang.mergeUnit) {
136
258
  const t = lang.mergeUnit(baseText, aText, bText)
137
259
  if (t != null && lang.parses(t)) return t
138
260
  }
261
+ // Structural inner merge (declaration → statements, class → members): format-preserving
262
+ // and well-tested, so it goes first. Returns null when it can't safely resolve.
263
+ const structural = tryStructuralInner(lang, baseText, aText, bText, mopts)
264
+ if (structural != null) return structural
265
+ // Unit-level oracle: providers with a fragment validator (parsesUnit) get every finer-
266
+ // grained result checked HERE, so an invalid inner merge becomes an honest per-unit
267
+ // conflict (judge-resolvable) instead of poisoning the whole file into fallback.
268
+ // Providers without one (JS: a bare `return …` fragment is valid only in context) keep
269
+ // deferring to the whole-file gate, as before.
270
+ const unitOk = lang.parsesUnit ? (t) => t != null && lang.parsesUnit(t) : (t) => t != null
271
+ // LINE-LEVEL diff3: whole lines as atoms, so indentation travels with its line — the
272
+ // safe multi-hunk tier, and the workhorse for "both sides edited different lines of the
273
+ // same declaration" (the most common same-unit case in real repos).
274
+ const lm = lineMerge3(baseText, aText, bText, mopts)
275
+ if (unitOk(lm)) return lm
276
+ // FINEST GRANULARITY: a single line/statement both sides edited — token-level 3-way, so
277
+ // two edits INSIDE one line (`foo(1, 2)` → one side edits each argument) merge the way a
278
+ // tree-sitter merge does, instead of conflicting on the whole statement. For providers
279
+ // without parsesUnit, validity is enforced where the result lands (whole-file re-parse).
280
+ const tm = tokenMerge(lang, baseText, aText, bText, mopts)
281
+ if (tm != null && lang.parsesUnit && !lang.parsesUnit(tm)) return null
282
+ return tm
283
+ }
284
+
285
+ function tryStructuralInner(lang, baseText, aText, bText, mopts = {}) {
139
286
  if (!lang.splitUnit) return null
140
287
  const pb = lang.splitUnit(baseText), pa = lang.splitUnit(aText), pbb = lang.splitUnit(bText)
141
288
  if (!pb || !pa || !pbb) return null
142
- if (pa.sig.trim() !== pbb.sig.trim()) return null // signature itself changed → real conflict
143
- const m = mergeKeyed(lang, pb.units, pa.units, pbb.units)
289
+ if (pa.sig.trim() !== pbb.sig.trim()) return null
290
+ const m = mergeKeyed(lang, pb.units, pa.units, pbb.units, mopts)
144
291
  if (m.conflicts.length) return null
145
- const text = pa.sig + pa.open + m.parts.join(pa.join) + pa.close
292
+ // FORMAT-PRESERVING inner assembly: when the provider exposes the body text + byte-ranged
293
+ // units (spliceable), rebuild the body by splicing merged units back into the BASE body,
294
+ // so everything BETWEEN statements — comments, blank lines, indentation — survives verbatim.
295
+ // (Join-based reassembly with a fixed separator silently drops inner comments and reflows
296
+ // whitespace; found in the wild merging ICR's own source, and the C-family class-brace glue.)
297
+ // Objects and any non-spliceable provider fall back to the separator join.
298
+ let text
299
+ const spliceable = pb.spliceable && typeof pb.body === 'string' && pb.units.every((u) => typeof u.start === 'number' && typeof u.end === 'number')
300
+ if (spliceable) {
301
+ // COMMENT-EDIT PRESERVATION: gaps (comments/blank lines between statements) come from
302
+ // the splice BASIS. Splicing from base loses a side's comment-only edit. So: if exactly
303
+ // ONE side changed only gaps (its units are byte-identical to base's), splice into THAT
304
+ // side's body — its comment edit survives and the other side's code edits land on top.
305
+ // If BOTH sides changed only gaps, refuse (null) so the unit surfaces as a conflict
306
+ // instead of silently dropping either side's comment.
307
+ const sameUnits = (u1, u2) => u1.length === u2.length && u1.every((u, i) => u.key === u2[i].key && u.text === u2[i].text)
308
+ const aGapOnly = pa.body !== pb.body && sameUnits(pa.units, pb.units)
309
+ const bGapOnly = pbb.body !== pb.body && sameUnits(pbb.units, pb.units)
310
+ if (aGapOnly && bGapOnly) return null
311
+ const [basisBody, basisUnits] = aGapOnly ? [pa.body, pa.units] : bGapOnly ? [pbb.body, pbb.units] : [pb.body, pb.units]
312
+ const body = spliceUnits(basisBody, basisUnits, m.order, m.textByKey, { trimLeadingBlank: false })
313
+ text = pa.sig + pa.open + body + pa.close
314
+ } else {
315
+ text = pa.sig + pa.open + m.parts.join(pa.join) + pa.close
316
+ }
146
317
  const valid = lang.parsesUnit || lang.parses // accept class-member fragments when supported
147
318
  return valid(text) ? text : null
148
319
  }
149
320
 
321
+ // TOKEN-LEVEL 3-way merge of one unit. base/a/b are the unit texts, both differing from base
322
+ // (the caller only asks when both sides changed the same unit). Diff each side against base at
323
+ // TOKEN granularity — one changed span via common prefix + common suffix — map each span to a
324
+ // base CHARACTER range, and, if the two base spans are DISJOINT, splice both sides' replacement
325
+ // text into base. Everything OUTSIDE the two edits comes from base verbatim (whitespace,
326
+ // comments); inside, each side's exact bytes. Overlapping or ambiguously-adjacent edits → null
327
+ // (a real conflict). Symmetric by construction: the result is base with two position-disjoint
328
+ // substring replacements, independent of which side is a or b — so live peers converge.
329
+ function tokenDiffRange(base, other) {
330
+ const n = base.length, m = other.length
331
+ let eq = n === m
332
+ if (eq) { for (let i = 0; i < n; i++) if (base[i] !== other[i]) { eq = false; break } }
333
+ if (eq) return null
334
+ let p = 0
335
+ const maxp = Math.min(n, m)
336
+ while (p < maxp && base[p] === other[p]) p++
337
+ let s = 0
338
+ while (s < n - p && s < m - p && base[n - 1 - s] === other[m - 1 - s]) s++
339
+ return { bStart: p, bEnd: n - s, oStart: p, oEnd: m - s }
340
+ }
341
+
342
+ function tokenMerge(lang, baseText, aText, bText, mopts = {}) {
343
+ if (!lang.tokenize) return null
344
+ const B = lang.tokenize(baseText), A = lang.tokenize(aText), Bb = lang.tokenize(bText)
345
+ if (!B || !A || !Bb) return null
346
+ const ra = tokenDiffRange(B.map((t) => t.k), A.map((t) => t.k))
347
+ const rb = tokenDiffRange(B.map((t) => t.k), Bb.map((t) => t.k))
348
+ if (!ra && !rb) return null // both sides changed only whitespace/comments → can't rank them
349
+ // REFORMAT vs CONTENT: one side changed only whitespace/comments (token stream identical
350
+ // to base), the other changed tokens. Apply the content side's token edit ONTO the
351
+ // reformatting side's text — its token stream matches base's, so token indices map its
352
+ // own byte ranges exactly. Both intents survive: the reformat/comment edit AND the code
353
+ // change. (Ground-truth replay: this was a top false-conflict source on real repos.)
354
+ // Symmetric because the roles are decided by CONTENT (which side has the token diff).
355
+ if (!ra || !rb) {
356
+ const [wsToks, wsText, r, toks, text] = !ra ? [A, aText, rb, Bb, bText] : [Bb, bText, ra, A, aText]
357
+ const cStart = r.bStart < wsToks.length ? wsToks[r.bStart].start : wsText.length
358
+ const cEnd = r.bEnd > r.bStart ? wsToks[r.bEnd - 1].end : cStart
359
+ const ins = r.oEnd > r.oStart ? text.slice(toks[r.oStart].start, toks[r.oEnd - 1].end) : ''
360
+ return wsText.slice(0, cStart) + ins + wsText.slice(cEnd)
361
+ }
362
+ const charRange = (r) => {
363
+ const cStart = r.bStart < B.length ? B[r.bStart].start : baseText.length
364
+ const cEnd = r.bEnd > r.bStart ? B[r.bEnd - 1].end : cStart
365
+ return [cStart, cEnd]
366
+ }
367
+ const repl = (toks, text, r) => (r.oEnd > r.oStart ? text.slice(toks[r.oStart].start, toks[r.oEnd - 1].end) : '')
368
+ const [aS, aE] = charRange(ra), [bS, bE] = charRange(rb)
369
+ // Single contiguous spans that don't collide: splice both (the cheap common case).
370
+ if (!(aS < bE && bS < aE) && aS !== bS) {
371
+ const edits = [{ s: aS, e: aE, t: repl(A, aText, ra) }, { s: bS, e: bE, t: repl(Bb, bText, rb) }].sort((x, y) => y.s - x.s)
372
+ let out = baseText
373
+ for (const ed of edits) out = out.slice(0, ed.s) + ed.t + out.slice(ed.e)
374
+ return out
375
+ }
376
+ // The prefix/suffix spans collide — but each is the BOUNDING BOX of that side's edits,
377
+ // and boxes overlap whenever a side edited in two places (a wrapper rewrite touches both
378
+ // ends, so its box swallows the whole unit). Re-diff each side at REAL multi-hunk
379
+ // granularity (Myers) and do token-level diff3: disjoint hunks interleave, identical
380
+ // hunks dedupe, truly overlapping hunks are the only conflict. Ground-truth replay:
381
+ // "one side rewrote the wrapper, the other edited inside" is a real-repo staple.
382
+ return tokenMerge3(baseText, aText, bText, B, A, Bb, mopts)
383
+ }
384
+
385
+ // Myers O(ND) diff on token-key arrays → edit hunks {bs,be,os,oe} (base/other token ranges).
386
+ function tokenHunks(bk, ok, maxD) {
387
+ const N = bk.length, M = ok.length
388
+ const MAX = Math.min(N + M, maxD)
389
+ const V = new Map([[1, 0]])
390
+ const trace = []
391
+ let D = -1
392
+ outer: for (let d = 0; d <= MAX; d++) {
393
+ trace.push(new Map(V))
394
+ for (let k = -d; k <= d; k += 2) {
395
+ let x = (k === -d || (k !== d && (V.get(k - 1) ?? 0) < (V.get(k + 1) ?? 0))) ? (V.get(k + 1) ?? 0) : (V.get(k - 1) ?? 0) + 1
396
+ let y = x - k
397
+ while (x < N && y < M && bk[x] === ok[y]) { x++; y++ }
398
+ V.set(k, x)
399
+ if (x >= N && y >= M) { D = d; break outer }
400
+ }
401
+ }
402
+ if (D < 0) return null // too different for the budget → caller falls back
403
+ // backtrack into hunks
404
+ const ops = [] // 1 = del base token, 2 = ins other token, stored back-to-front
405
+ let x = N, y = M
406
+ for (let d = D; d > 0; d--) {
407
+ const Vp = trace[d]
408
+ const k = x - y
409
+ const down = k === -d || (k !== d && (Vp.get(k - 1) ?? 0) < (Vp.get(k + 1) ?? 0))
410
+ const pk = down ? k + 1 : k - 1
411
+ const px = Vp.get(pk) ?? 0
412
+ const py = px - pk
413
+ while (x > px + (down ? 0 : 1) && y > py + (down ? 1 : 0)) { x--; y-- } // snake
414
+ if (down) { y--; ops.push([2, x, y]) } else { x--; ops.push([1, x, y]) }
415
+ }
416
+ ops.reverse()
417
+ const hunks = []
418
+ for (const [op, bx, oy] of ops) {
419
+ const last = hunks[hunks.length - 1]
420
+ if (op === 1) {
421
+ if (last && last.be === bx && last.oe === oy) last.be = bx + 1
422
+ else hunks.push({ bs: bx, be: bx + 1, os: oy, oe: oy })
423
+ } else {
424
+ if (last && last.be === bx && last.oe === oy) last.oe = oy + 1
425
+ else hunks.push({ bs: bx, be: bx, os: oy, oe: oy + 1 })
426
+ }
427
+ }
428
+ return hunks
429
+ }
430
+
431
+ // diff3 over two hunk lists: identical hunks dedupe; disjoint hunks both apply; overlap
432
+ // → conflict (null). Two INSERTIONS at the same base point are ambiguous (order
433
+ // unknowable) → conflict. `ak`/`bbk` are each side's atom arrays (token keys or lines),
434
+ // used to compare replacement content for the dedupe check. Returns [{h, side}] in base
435
+ // order, or null. Symmetric: ordering decisions are content/position-based, never
436
+ // side-identity-based, so merge(a,b) === merge(b,a) is preserved by construction.
437
+ function diff3Interleave(ha, hb, ak, bbk, opts = {}) {
438
+ const sameHunk = (x, y) => x.bs === y.bs && x.be === y.be && (x.oe - x.os) === (y.oe - y.os) &&
439
+ ak.slice(x.os, x.oe).every((k, i) => k === bbk[y.os + i])
440
+ const edits = []
441
+ let i = 0, j = 0
442
+ while (i < ha.length || j < hb.length) {
443
+ const x = ha[i], y = hb[j]
444
+ if (x && y) {
445
+ const overlap = x.bs < y.be && y.bs < x.be
446
+ const samePointInsert = x.bs === y.bs && x.be === x.bs && y.be === y.bs
447
+ if (overlap || samePointInsert) {
448
+ if (sameHunk(x, y)) { edits.push({ h: x, side: 'a' }); i++; j++; continue }
449
+ // UNION MODE (opt-in, for independent-intent merges — the multi-agent case where
450
+ // the medium KNOWS the two edits serve different tasks): two pure INSERTIONS at
451
+ // the same base point both survive, in content order (side-independent, so peers
452
+ // converge). Everything else (true overlaps, edit-vs-edit) still conflicts, and
453
+ // the parse gates downstream still refuse any union that breaks the code.
454
+ if (samePointInsert && opts.unionInserts) {
455
+ const ka = ak.slice(x.os, x.oe).join('\n'), kb = bbk.slice(y.os, y.oe).join('\n')
456
+ const [first, second] = ka <= kb ? [{ h: x, side: 'a' }, { h: y, side: 'b' }] : [{ h: y, side: 'b' }, { h: x, side: 'a' }]
457
+ edits.push(first, second); i++; j++; continue
458
+ }
459
+ return null
460
+ }
461
+ if (x.bs < y.bs || (x.bs === y.bs && x.be <= y.bs && x.be < y.be)) { edits.push({ h: x, side: 'a' }); i++ }
462
+ else { edits.push({ h: y, side: 'b' }); j++ }
463
+ } else if (x) { edits.push({ h: x, side: 'a' }); i++ }
464
+ else { edits.push({ h: y, side: 'b' }); j++ }
465
+ }
466
+ return edits
467
+ }
468
+
469
+ const TOKEN3_MAX_TOKENS = 20000, TOKEN3_MAX_D = 4000
470
+ function tokenMerge3(baseText, aText, bText, B, A, Bb, mopts = {}) {
471
+ if (B.length > TOKEN3_MAX_TOKENS || A.length > TOKEN3_MAX_TOKENS || Bb.length > TOKEN3_MAX_TOKENS) return null
472
+ const bk = B.map((t) => t.k)
473
+ const ha = tokenHunks(bk, A.map((t) => t.k), TOKEN3_MAX_D)
474
+ const hb = tokenHunks(bk, Bb.map((t) => t.k), TOKEN3_MAX_D)
475
+ if (!ha || !hb) return null
476
+ const edits = diff3Interleave(ha, hb, A.map((t) => t.k), Bb.map((t) => t.k), mopts)
477
+ if (!edits) return null
478
+ // splice back-to-front, mapping token hunks to base char ranges; replacement bytes come
479
+ // from the editing side's exact text between its tokens. A guard space prevents two word
480
+ // tokens fusing at a splice boundary (parse validation downstream still has final say).
481
+ const wordish = (c) => c && /[A-Za-z0-9_$]/.test(c)
482
+ let out = baseText
483
+ for (let e = edits.length - 1; e >= 0; e--) {
484
+ const { h, side } = edits[e]
485
+ const [toks, text] = side === 'a' ? [A, aText] : [Bb, bText]
486
+ const cS = h.bs < B.length ? B[h.bs].start : baseText.length
487
+ const cE = h.be > h.bs ? B[h.be - 1].end : cS
488
+ let ins = h.oe > h.os ? text.slice(toks[h.os].start, toks[h.oe - 1].end) : ''
489
+ if (ins && wordish(out[cS - 1]) && wordish(ins[0])) ins = ' ' + ins
490
+ if (ins && wordish(out[cE]) && wordish(ins[ins.length - 1])) ins = ins + ' '
491
+ out = out.slice(0, cS) + ins + out.slice(cE)
492
+ }
493
+ return out
494
+ }
495
+
496
+ // LINE-LEVEL 3-way merge of one unit: the same Myers + diff3 interleave with WHOLE LINES
497
+ // as atoms. Every replacement carries complete lines verbatim — indentation can never be
498
+ // stranded or doubled, which is what makes it the safe multi-hunk tier for indentation
499
+ // languages (the ConGra corpus showed token-splice arithmetic mangling Python indent when
500
+ // hunks straddle newline tokens: `conn = None` landing at column 0, a doubled 16-space
501
+ // indent). Token-level still runs after this for same-line micro-edits; this tier catches
502
+ // the far more common "both sides edited different LINES of the same unit" cleanly.
503
+ function splitLinesKeepEnds(text) {
504
+ const out = []
505
+ let i = 0
506
+ while (i < text.length) { let e = text.indexOf('\n', i); e = e < 0 ? text.length : e + 1; out.push(text.slice(i, e)); i = e }
507
+ return out
508
+ }
509
+ function lineMerge3(baseText, aText, bText, mopts = {}) {
510
+ const L = splitLinesKeepEnds(baseText), La = splitLinesKeepEnds(aText), Lb = splitLinesKeepEnds(bText)
511
+ if (L.length > TOKEN3_MAX_TOKENS) return null
512
+ const ha = tokenHunks(L, La, TOKEN3_MAX_D)
513
+ const hb = tokenHunks(L, Lb, TOKEN3_MAX_D)
514
+ if (!ha || !hb) return null
515
+ const edits = diff3Interleave(ha, hb, La, Lb, mopts)
516
+ if (!edits) return null
517
+ const out = L.slice()
518
+ for (let e = edits.length - 1; e >= 0; e--) {
519
+ const { h, side } = edits[e]
520
+ const src = side === 'a' ? La : Lb
521
+ out.splice(h.bs, h.be - h.bs, ...src.slice(h.os, h.oe))
522
+ }
523
+ return out.join('')
524
+ }
525
+
150
526
  // FORMAT-PRESERVING assembly: rebuild the merged file by splicing the merged units back
151
527
  // into the BASE text at their original byte ranges. Unchanged units — and all the
152
528
  // whitespace/comments BETWEEN units — survive verbatim; only changed units carry the
153
529
  // editing side's bytes, and genuinely new units are appended. This is what stops ICR from
154
530
  // reformatting code it merges. Deterministic and symmetric, so peers converge.
155
- function spliceUnits(baseText, baseUnits, order, textByKey) {
531
+ function spliceUnits(baseText, baseUnits, order, textByKey, opts = {}) {
156
532
  const baseKeySet = new Set(baseUnits.map((u) => u.key))
157
- const surviving = new Set(order.filter((k) => baseKeySet.has(k)))
158
- const emitted = new Set()
533
+ const baseByKey = new Map(baseUnits.map((u) => [u.key, u]))
534
+ const orderSet = new Set(order)
535
+ const deleted = baseUnits.filter((u) => !orderSet.has(u.key))
159
536
  let out = '', pos = 0
160
- for (const u of baseUnits) {
161
- out += baseText.slice(pos, u.start) // gap before this unit (comments/blank lines), verbatim
162
- if (surviving.has(u.key) && !emitted.has(u.key)) { out += textByKey.get(u.key); emitted.add(u.key) }
163
- pos = u.end
537
+
538
+ function emitGap(from, to) {
539
+ let cursor = from
540
+ for (const d of deleted) {
541
+ if (d.start >= cursor && d.end <= to) {
542
+ out += baseText.slice(cursor, d.start)
543
+ cursor = d.end
544
+ }
545
+ }
546
+ out += baseText.slice(cursor, to)
164
547
  }
165
- const added = order.filter((k) => !baseKeySet.has(k))
166
- if (added.length) {
167
- const head = out.replace(/\s*$/, '')
168
- out = (head ? head + '\n\n' : '') + added.map((k) => textByKey.get(k)).join('\n\n') + '\n'
169
- } else {
170
- out += baseText.slice(pos) // trailing whitespace (final newline) verbatim
548
+
549
+ const seenKeys = new Set()
550
+ for (const k of order) {
551
+ if (seenKeys.has(k)) continue
552
+ seenKeys.add(k)
553
+ if (baseKeySet.has(k)) {
554
+ const u = baseByKey.get(k)
555
+ if (u.start >= pos) {
556
+ emitGap(pos, u.start)
557
+ pos = u.end
558
+ }
559
+ const val = textByKey.get(k)
560
+ if (val != null) out += val
561
+ } else {
562
+ // New unit: insert it exactly where the merge order dictated
563
+ const val = textByKey.get(k)
564
+ if (val != null) {
565
+ out += (out.endsWith('\n') ? '' : '\n') + val + '\n'
566
+ }
567
+ }
171
568
  }
172
- return out.replace(/^\n+/, '') // drop orphan leading blank lines left by a deleted first unit
569
+ if (pos < baseText.length) {
570
+ emitGap(pos, baseText.length)
571
+ }
572
+ // Top-level: drop orphan leading blank lines a deleted first decl leaves behind. Inner
573
+ // splice (trimLeadingBlank:false): keep the body's own leading `\n ` so indentation holds.
574
+ return opts.trimLeadingBlank === false ? out : out.replace(/^\n+/, '')
575
+ }
576
+
577
+ // --- HyperAST Caching -----------------------------------------------------------
578
+ // A memory cache to bypass redundant AST parsing. This gives O(1) lookups for
579
+ // unchanged structures during interleaved diff3 merges, dropping engine execution
580
+ // time drastically on large files.
581
+ const providerCache = new WeakMap();
582
+
583
+ function withHyperAST(lang) {
584
+ if (providerCache.has(lang)) return providerCache.get(lang);
585
+
586
+ const cache = new Map();
587
+ const get = (op, text, fn) => {
588
+ if (typeof text !== 'string' || text.length > 500000) return fn(); // Bypass for massive strings
589
+ const key = op + ':' + text;
590
+ let res = cache.get(key);
591
+ if (res !== undefined) return res;
592
+ res = fn();
593
+ if (cache.size > 10000) cache.clear();
594
+ cache.set(key, res);
595
+ return res;
596
+ };
597
+
598
+ const wrapped = Object.create(lang);
599
+ if (lang.parses) wrapped.parses = (src) => get('parses', src, () => lang.parses(src));
600
+ if (lang.units) wrapped.units = (src) => get('units', src, () => lang.units(src));
601
+ if (lang.tokenize) wrapped.tokenize = (src) => get('tokenize', src, () => lang.tokenize(src));
602
+ if (lang.splitUnit) wrapped.splitUnit = (src) => get('splitUnit', src, () => lang.splitUnit(src));
603
+ if (lang.declaredNames) wrapped.declaredNames = (src) => get('declaredNames', src, () => lang.declaredNames(src));
604
+
605
+ providerCache.set(lang, wrapped);
606
+ return wrapped;
173
607
  }
174
608
 
175
609
  // --- public API -----------------------------------------------------------------
176
610
 
177
611
  // Does this source parse under the given (or default JS) language?
178
- export function parses(src, lang = javascript) { return lang.parses(src) }
612
+ export function parses(src, lang = javascript) { return withHyperAST(lang).parses(src) }
179
613
 
180
614
  // 3-way STRUCTURAL + INTENT merge of `a` and `b` against common ancestor `base`.
181
615
  // opts: { lang } a provider, or { filename } to pick one by extension (defaults to JS).
@@ -186,7 +620,8 @@ export function parses(src, lang = javascript) { return lang.parses(src) }
186
620
  // 'fallback' — couldn't merge safely (unparseable input, or the merge wouldn't
187
621
  // parse, or unsupported language); caller keeps both. Never broken.
188
622
  export function structuralMerge(base, a, b, opts = {}) {
189
- const lang = opts.lang || (opts.filename ? languageFor(opts.filename) : javascript) || javascript
623
+ let lang = opts.lang || (opts.filename ? languageFor(opts.filename) : javascript) || javascript
624
+ lang = withHyperAST(lang)
190
625
 
191
626
  if (!lang.parses(base) || !lang.parses(a) || !lang.parses(b))
192
627
  return { status: 'fallback', text: null, conflicts: [], reason: 'unparseable input' }
@@ -205,10 +640,20 @@ export function structuralMerge(base, a, b, opts = {}) {
205
640
  let text, provenance
206
641
  if (a === base) { text = b; provenance = [] }
207
642
  else if (b === base) { text = a; provenance = [] }
643
+ else if (lang.mergeWhole) {
644
+ // WHOLE-DOCUMENT MERGE: a provider that understands the entire document as DATA
645
+ // (e.g. JSON) merges it directly — real-parser accuracy, value-wise 3-way semantics,
646
+ // conflicts named by path. Returns { text } on success, { conflicts, resolvable? } on
647
+ // a same-value clash, or null when it can't (engine falls back safely).
648
+ const w = lang.mergeWhole(base, a, b)
649
+ if (!w) return { status: 'fallback', text: null, conflicts: [], reason: 'whole-document merge failed' }
650
+ if (w.conflicts && w.conflicts.length) return { status: 'semantic-conflict', text: null, conflicts: w.conflicts, resolvable: w.resolvable || [] }
651
+ text = w.text; provenance = w.provenance || []
652
+ }
208
653
  else {
209
654
  const baseUnits = lang.units(base)
210
- const merge = mergeKeyed(lang, baseUnits, lang.units(a), lang.units(b))
211
- if (merge.conflicts.length) return { status: 'semantic-conflict', text: null, conflicts: merge.conflicts }
655
+ const merge = mergeKeyed(lang, baseUnits, lang.units(a), lang.units(b), { unionInserts: !!opts.unionInserts })
656
+ if (merge.conflicts.length) return { status: 'semantic-conflict', text: null, conflicts: merge.conflicts, resolvable: merge.conflictUnits }
212
657
  // Prefer the format-preserving splice (units carry byte ranges); fall back to a plain
213
658
  // join for languages whose units don't expose ranges.
214
659
  const canSplice = baseUnits.every((u) => typeof u.start === 'number')
@@ -217,7 +662,10 @@ export function structuralMerge(base, a, b, opts = {}) {
217
662
  }
218
663
 
219
664
  // THE GUARANTEE: the merged result must parse, or we refuse it.
220
- if (!lang.parses(text)) return { status: 'fallback', text: null, conflicts: [], reason: 'merge would not parse' }
665
+ // THE GUARANTEE stands: a merge that would not parse is REFUSED — text stays null so no
666
+ // caller can ever mistake broken output for a merge. (Debugging wants the bytes? That is
667
+ // what `debugText` is for; it is deliberately not `text`.)
668
+ if (!lang.parses(text)) return { status: 'fallback', text: null, debugText: text, conflicts: [], reason: 'merge would not parse' }
221
669
 
222
670
  // RENAME DETECTION (intent): a declaration gone from base whose identical-bodied twin
223
671
  // appears under a new name is a rename — rewrite stale call sites to the new name.
@@ -227,6 +675,7 @@ export function structuralMerge(base, a, b, opts = {}) {
227
675
  let removed = [...baseNames].filter((n) => !mergedNames.has(n))
228
676
  const renames = []
229
677
  const rename = lang.renameFreeRefs || lang.renameRefs // prefer scope-aware rewriting
678
+
230
679
  if (removed.length && lang.declBody && rename) {
231
680
  const added = [...mergedNames].filter((n) => !baseNames.has(n))
232
681
  const claimed = new Set()
@@ -244,6 +693,24 @@ export function structuralMerge(base, a, b, opts = {}) {
244
693
  if (renames.length && lang.parses(rewritten)) { merged = rewritten; mergedNames = lang.declaredNames(merged) }
245
694
  }
246
695
 
696
+ // ASYNC/AWAIT SYNCHRONIZATION (intent):
697
+ // When a declaration was synchronous in base and became ASYNC in the merged result,
698
+ // upgrade call sites inside async callers with `await` so the contract holds.
699
+ const asyncUpgrades = []
700
+ if (lang.isAsyncDecl && lang.upgradeCallSitesToAwait) {
701
+ for (const name of mergedNames) {
702
+ const wasAsyncInBase = baseNames.has(name) ? lang.isAsyncDecl(base, name) : false
703
+ const isAsyncInMerged = lang.isAsyncDecl(merged, name)
704
+ if (!wasAsyncInBase && isAsyncInMerged) {
705
+ const upgraded = lang.upgradeCallSitesToAwait(merged, name)
706
+ if (upgraded !== merged && lang.parses(upgraded)) {
707
+ merged = upgraded
708
+ asyncUpgrades.push(name)
709
+ }
710
+ }
711
+ }
712
+ }
713
+
247
714
  // DANGLING REFERENCE (intent): a declaration removed (and NOT explained by a rename)
248
715
  // but still referenced is a semantic conflict even though the code parses cleanly.
249
716
  // Uses the SCOPE-AWARE reference set when the language provides one (so a local binding
@@ -252,6 +719,7 @@ export function structuralMerge(base, a, b, opts = {}) {
252
719
  removed = [...baseNames].filter((n) => !mergedNames.has(n))
253
720
  const refs = lang.referencedFreeNames ? lang.referencedFreeNames(merged)
254
721
  : lang.usedIdentifiers ? lang.usedIdentifiers(merged) : null
722
+ if (process.env.ICR_DEBUG_MERGED) console.error('--- ICR merged intermediate ---\n' + merged + '\n--- refs: ' + [...(refs || [])].join(',') + ' ---')
255
723
  if (removed.length && refs) {
256
724
  const dangling = removed.filter((n) => refs.has(n))
257
725
  if (dangling.length)
@@ -262,6 +730,10 @@ export function structuralMerge(base, a, b, opts = {}) {
262
730
  return {
263
731
  status: 'auto', text: merged, conflicts: [],
264
732
  renames: renames.map(([o, n]) => o + '->' + n),
733
+ asyncUpgrades,
265
734
  provenance: attribute(provenance),
266
735
  }
267
736
  }
737
+
738
+ // Internal handles for diagnostic harnesses (not part of the public API).
739
+ export const _internals = { tokenMerge, tokenMerge3, tokenHunks, tryInnerMerge, lineMerge3 }