hivecode-mcp 0.8.0 → 0.11.1

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/lang-json.js ADDED
@@ -0,0 +1,184 @@
1
+ // JSON language provider for ICR — the one provider where the guarantee is PERFECT,
2
+ // because JSON.parse IS the real parser (no heuristics anywhere).
3
+ //
4
+ // JSON is the most-merged file type in a repository (package.json, tsconfig, lockfile
5
+ // fragments, CI configs), and the classic line-merge failure is two sides editing
6
+ // nearby keys — git conflicts or, worse, fuses into invalid JSON. ICR merges JSON as
7
+ // DATA: value-wise 3-way merge, keyed by object path, so:
8
+ // • different keys edited on each side -> clean merge, both kept
9
+ // • the SAME path changed differently -> semantic conflict naming the path
10
+ // (surfaced as a machine-resolvable unit, so an agent judge can reconcile it)
11
+ // • one side deletes a key the other changed -> conflict (delete vs change)
12
+ // • arrays: one-sided changes merge; both-sided changes merge when the edited
13
+ // regions are DISJOINT (prefix/suffix diff — appends, prepends, middle inserts);
14
+ // overlapping array edits are an honest conflict
15
+ // Output is serialized with the file's own indentation style (detected from the base)
16
+ // and preserves the trailing-newline convention, so diffs stay minimal.
17
+
18
+ const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
19
+ const eq = (x, y) => JSON.stringify(x) === JSON.stringify(y)
20
+
21
+ // PRECISION SAFETY GATE. The data merge re-serializes the whole document, which means
22
+ // every number goes through JS float64 — and that CORRUPTS values nobody touched:
23
+ // 9007199254740993 (beyond 2^53) silently loses a digit; 1e999 becomes Infinity and
24
+ // then `null`. Shipping a "clean" merge that corrupts untouched data is the cardinal
25
+ // sin, so: scan every number literal (string-aware); if ANY cannot survive the
26
+ // round-trip value-identically, refuse the data merge entirely — the engine falls back
27
+ // to the safe line tier, which never rewrites untouched bytes. (Found by the
28
+ // adversarial crucible, not by users — the way it should be.)
29
+ function numbersSafe(src) {
30
+ const n = src.length
31
+ let i = 0
32
+ while (i < n) {
33
+ const c = src[i]
34
+ if (c === '"') { i++; while (i < n) { if (src[i] === '\\') { i += 2; continue } if (src[i] === '"') { i++; break } i++ } continue }
35
+ if (c === '-' || (c >= '0' && c <= '9')) {
36
+ let j = i + 1
37
+ while (j < n && /[0-9eE+.-]/.test(src[j])) j++
38
+ const tok = src.slice(i, j)
39
+ const v = Number(tok)
40
+ if (!Number.isFinite(v)) return false // 1e999 → Infinity → null
41
+ if (Number.isInteger(v) && Math.abs(v) > Number.MAX_SAFE_INTEGER) return false // beyond 2^53
42
+ const digits = tok.replace(/[^0-9]/g, '').replace(/^0+/, '')
43
+ if (digits.length > 15 && !eq(Number(tok), Number(v.toPrecision(15)))) return false // >15 sig digits at risk
44
+ i = j; continue
45
+ }
46
+ i++
47
+ }
48
+ return true
49
+ }
50
+
51
+ // Detect the base file's indent unit (' ', ' ', '\t', …). Defaults to two spaces.
52
+ function detectIndent(src) {
53
+ const m = /\n([ \t]+)\S/.exec(src)
54
+ return m ? m[1] : ' '
55
+ }
56
+
57
+ // 3-way merge of two ARRAYS against base, by disjoint-region splice (the same
58
+ // prefix/suffix discipline the engine's token merge uses, on JSON-equal elements).
59
+ // Returns the merged array, or null for an overlapping (real) conflict.
60
+ function mergeArray(base, a, b) {
61
+ const K = (arr) => arr.map((x) => JSON.stringify(x))
62
+ const diffRange = (B, O) => {
63
+ if (B.length === O.length && B.every((x, i) => x === O[i])) return null
64
+ let p = 0
65
+ const maxp = Math.min(B.length, O.length)
66
+ while (p < maxp && B[p] === O[p]) p++
67
+ let s = 0
68
+ while (s < B.length - p && s < O.length - p && B[B.length - 1 - s] === O[O.length - 1 - s]) s++
69
+ return { bStart: p, bEnd: B.length - s, oStart: p, oEnd: O.length - s }
70
+ }
71
+ const KB = K(base), ra = diffRange(KB, K(a)), rb = diffRange(KB, K(b))
72
+ if (!ra) return b
73
+ if (!rb) return a
74
+ if (ra.bStart < rb.bEnd && rb.bStart < ra.bEnd) return null // overlapping regions
75
+ if (ra.bStart === rb.bStart) return null // ambiguous same-point edits
76
+ const edits = [
77
+ { s: ra.bStart, e: ra.bEnd, ins: a.slice(ra.oStart, ra.oEnd) },
78
+ { s: rb.bStart, e: rb.bEnd, ins: b.slice(rb.oStart, rb.oEnd) },
79
+ ].sort((x, y) => y.s - x.s)
80
+ let out = base.slice()
81
+ for (const ed of edits) out.splice(ed.s, ed.e - ed.s, ...ed.ins)
82
+ return out
83
+ }
84
+
85
+ // Value-wise 3-way merge. Fills `conflicts` (path strings) and `resolvable`
86
+ // ({key, base, ours, theirs} with pretty-printed values) on same-path clashes;
87
+ // the merged value keeps BASE at conflicted paths (the engine refuses the whole
88
+ // merge when conflicts exist, so nothing partial ever ships).
89
+ function mergeVal(base, a, b, path, conflicts, resolvable) {
90
+ if (eq(a, b)) return a
91
+ if (eq(a, base)) return b
92
+ if (eq(b, base)) return a
93
+ if (isObj(base) && isObj(a) && isObj(b)) {
94
+ const out = {}
95
+ const keys = [...new Set([...Object.keys(base), ...Object.keys(a), ...Object.keys(b)])]
96
+ for (const k of keys) {
97
+ const p = path ? path + '.' + k : k
98
+ const inB = k in base, inA = k in a, inBb = k in b
99
+ if (inB && !inA && !inBb) continue // deleted on both sides
100
+ if (inB && !inA) { // ours deleted it
101
+ if (eq(b[k], base[k])) continue // theirs untouched -> deletion wins
102
+ conflicts.push(p); resolvable.push(resUnit(p, base[k], undefined, b[k])); continue
103
+ }
104
+ if (inB && !inBb) { // theirs deleted it
105
+ if (eq(a[k], base[k])) continue
106
+ conflicts.push(p); resolvable.push(resUnit(p, base[k], a[k], undefined)); continue
107
+ }
108
+ if (!inB && inA && inBb) { // both added
109
+ if (eq(a[k], b[k])) { out[k] = a[k]; continue }
110
+ if (isObj(a[k]) && isObj(b[k])) { out[k] = mergeVal({}, a[k], b[k], p, conflicts, resolvable); continue }
111
+ conflicts.push(p); resolvable.push(resUnit(p, undefined, a[k], b[k])); continue
112
+ }
113
+ if (!inB) { out[k] = inA ? a[k] : b[k]; continue } // one side added
114
+ out[k] = mergeVal(base[k], a[k], b[k], p, conflicts, resolvable) // present everywhere
115
+ }
116
+ return out
117
+ }
118
+ if (Array.isArray(base) && Array.isArray(a) && Array.isArray(b)) {
119
+ const m = mergeArray(base, a, b)
120
+ if (m) return m
121
+ conflicts.push(path || '(root)'); resolvable.push(resUnit(path || '(root)', base, a, b))
122
+ return base
123
+ }
124
+ conflicts.push(path || '(root)') // scalar/shape clash
125
+ resolvable.push(resUnit(path || '(root)', base, a, b))
126
+ return base
127
+ }
128
+
129
+ const show = (v) => (v === undefined ? '(deleted)' : JSON.stringify(v, null, 2))
130
+ const resUnit = (p, base, ours, theirs) => ({ key: 'json:' + p, base: show(base), ours: show(ours), theirs: show(theirs) })
131
+
132
+ export const json = {
133
+ id: 'json',
134
+ exts: ['.json'],
135
+ parses: (src) => { try { JSON.parse(String(src == null ? '' : src)); return true } catch { return false } },
136
+ // The engine's whole-document tier: merge the file as parsed data.
137
+ mergeWhole: (base, a, b) => {
138
+ if (!numbersSafe(base) || !numbersSafe(a) || !numbersSafe(b)) return null // precision hazard → safe fallback, never corrupt
139
+ let B, A, Bb
140
+ try { B = JSON.parse(base); A = JSON.parse(a); Bb = JSON.parse(b) } catch { return null }
141
+ const conflicts = [], resolvable = []
142
+ const merged = mergeVal(B, A, Bb, '', conflicts, resolvable)
143
+ if (conflicts.length) return { conflicts, resolvable }
144
+ const nl = /\n$/.test(base) || /\n$/.test(a) ? '\n' : ''
145
+ return { text: JSON.stringify(merged, null, detectIndent(base)) + nl }
146
+ },
147
+ // Minimal unit view (whole document) so generic engine paths stay well-defined;
148
+ // real merging happens in mergeWhole above.
149
+ units: (src) => [{ key: 'doc', text: String(src), start: 0, end: String(src).length }],
150
+ declaredNames: () => new Set(),
151
+ // INTENT RESOLUTION for data: a conflicted unit's "text" is a rendering of a parsed
152
+ // VALUE, so a judge's reconciliation is applied by setting the value at the unit's
153
+ // path (key 'json:<path>'), not by string replacement. The reconciled string must
154
+ // itself be valid JSON ('(deleted)' removes the key). Returns the re-serialized
155
+ // document, or null when it can't apply — resolveMerge then falls back to the safe
156
+ // conflict, so a malformed judgment never ships.
157
+ applyResolution: (text, unit, reconciled) => {
158
+ if (!unit || typeof unit.key !== 'string' || !unit.key.startsWith('json:')) return null
159
+ if (!numbersSafe(text)) return null // same precision gate: never re-serialize a doc we'd corrupt
160
+ let doc, value
161
+ try { doc = JSON.parse(text) } catch { return null }
162
+ const del = String(reconciled).trim() === '(deleted)'
163
+ if (!del) { try { value = JSON.parse(reconciled) } catch { return null } }
164
+ const path = unit.key.slice(5)
165
+ if (path === '(root)' || path === '') {
166
+ if (del) return null
167
+ doc = value
168
+ } else {
169
+ const parts = path.split('.')
170
+ let cur = doc
171
+ for (let i = 0; i < parts.length - 1; i++) {
172
+ if (cur == null || typeof cur !== 'object') return null
173
+ if (!(parts[i] in cur)) cur[parts[i]] = {}
174
+ cur = cur[parts[i]]
175
+ }
176
+ if (cur == null || typeof cur !== 'object') return null
177
+ const leaf = parts[parts.length - 1]
178
+ if (del) delete cur[leaf]
179
+ else cur[leaf] = value
180
+ }
181
+ const nl = /\n$/.test(text) ? '\n' : ''
182
+ return JSON.stringify(doc, null, detectIndent(text)) + nl
183
+ },
184
+ }