hivecode-mcp 0.12.2 → 0.12.4

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 CHANGED
@@ -1,211 +1,211 @@
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
- // SYMMETRY. The merged object is re-serialized with JSON.stringify, which emits keys in
86
- // INSERTION order — so the order we walk the key union IS the output byte order. The
87
- // obvious union, `[...keys(base), ...keys(a), ...keys(b)]`, is side-ordered: ours's new
88
- // keys always precede theirs's, so merge(base,a,b) and merge(base,b,a) produce clean
89
- // 'auto' results that differ byte-for-byte. Measured at 43/167 auto merges (26%) by
90
- // test/icr-data-torture-test.js. Semantically identical, but P2 SYMMETRY is asserted
91
- // byte-wise for every other language (icr-torture-test.js), and byte-inequality is
92
- // precisely what a CRDT peer and a git blob hash react to — the two consumers the README
93
- // names. So the union order must not depend on which side is called `a`:
94
- // • keys present in base keep BASE's order (unchanged behaviour, minimal diffs);
95
- // • keys added by either side follow, ordered by where their own author put them
96
- // (min index across the sides that have it), ties broken by key name.
97
- // Both rules are symmetric in a/b, so swapping the sides cannot change the output.
98
- function unionKeys(base, a, b) {
99
- const rank = new Map()
100
- for (const obj of [a, b]) {
101
- const ks = Object.keys(obj)
102
- for (let i = 0; i < ks.length; i++) {
103
- const k = ks[i]
104
- if (k in base) continue
105
- const prev = rank.get(k)
106
- if (prev === undefined || i < prev) rank.set(k, i)
107
- }
108
- }
109
- const added = [...rank.keys()].sort((x, y) => (rank.get(x) - rank.get(y)) || (x < y ? -1 : x > y ? 1 : 0))
110
- return [...Object.keys(base), ...added]
111
- }
112
-
113
- // Value-wise 3-way merge. Fills `conflicts` (path strings) and `resolvable`
114
- // ({key, base, ours, theirs} with pretty-printed values) on same-path clashes;
115
- // the merged value keeps BASE at conflicted paths (the engine refuses the whole
116
- // merge when conflicts exist, so nothing partial ever ships).
117
- function mergeVal(base, a, b, path, conflicts, resolvable) {
118
- if (eq(a, b)) return a
119
- if (eq(a, base)) return b
120
- if (eq(b, base)) return a
121
- if (isObj(base) && isObj(a) && isObj(b)) {
122
- const out = {}
123
- for (const k of unionKeys(base, a, b)) {
124
- const p = path ? path + '.' + k : k
125
- const inB = k in base, inA = k in a, inBb = k in b
126
- if (inB && !inA && !inBb) continue // deleted on both sides
127
- if (inB && !inA) { // ours deleted it
128
- if (eq(b[k], base[k])) continue // theirs untouched -> deletion wins
129
- conflicts.push(p); resolvable.push(resUnit(p, base[k], undefined, b[k])); continue
130
- }
131
- if (inB && !inBb) { // theirs deleted it
132
- if (eq(a[k], base[k])) continue
133
- conflicts.push(p); resolvable.push(resUnit(p, base[k], a[k], undefined)); continue
134
- }
135
- if (!inB && inA && inBb) { // both added
136
- if (eq(a[k], b[k])) { out[k] = a[k]; continue }
137
- if (isObj(a[k]) && isObj(b[k])) { out[k] = mergeVal({}, a[k], b[k], p, conflicts, resolvable); continue }
138
- conflicts.push(p); resolvable.push(resUnit(p, undefined, a[k], b[k])); continue
139
- }
140
- if (!inB) { out[k] = inA ? a[k] : b[k]; continue } // one side added
141
- out[k] = mergeVal(base[k], a[k], b[k], p, conflicts, resolvable) // present everywhere
142
- }
143
- return out
144
- }
145
- if (Array.isArray(base) && Array.isArray(a) && Array.isArray(b)) {
146
- const m = mergeArray(base, a, b)
147
- if (m) return m
148
- conflicts.push(path || '(root)'); resolvable.push(resUnit(path || '(root)', base, a, b))
149
- return base
150
- }
151
- conflicts.push(path || '(root)') // scalar/shape clash
152
- resolvable.push(resUnit(path || '(root)', base, a, b))
153
- return base
154
- }
155
-
156
- const show = (v) => (v === undefined ? '(deleted)' : JSON.stringify(v, null, 2))
157
- const resUnit = (p, base, ours, theirs) => ({ key: 'json:' + p, base: show(base), ours: show(ours), theirs: show(theirs) })
158
-
159
- export const json = {
160
- id: 'json',
161
- exts: ['.json'],
162
- parses: (src) => { try { JSON.parse(String(src == null ? '' : src)); return true } catch { return false } },
163
- // The engine's whole-document tier: merge the file as parsed data.
164
- mergeWhole: (base, a, b) => {
165
- if (!numbersSafe(base) || !numbersSafe(a) || !numbersSafe(b)) return null // precision hazard → safe fallback, never corrupt
166
- let B, A, Bb
167
- try { B = JSON.parse(base); A = JSON.parse(a); Bb = JSON.parse(b) } catch { return null }
168
- const conflicts = [], resolvable = []
169
- const merged = mergeVal(B, A, Bb, '', conflicts, resolvable)
170
- if (conflicts.length) return { conflicts, resolvable }
171
- const nl = /\n$/.test(base) || /\n$/.test(a) ? '\n' : ''
172
- return { text: JSON.stringify(merged, null, detectIndent(base)) + nl }
173
- },
174
- // Minimal unit view (whole document) so generic engine paths stay well-defined;
175
- // real merging happens in mergeWhole above.
176
- units: (src) => [{ key: 'doc', text: String(src), start: 0, end: String(src).length }],
177
- declaredNames: () => new Set(),
178
- // INTENT RESOLUTION for data: a conflicted unit's "text" is a rendering of a parsed
179
- // VALUE, so a judge's reconciliation is applied by setting the value at the unit's
180
- // path (key 'json:<path>'), not by string replacement. The reconciled string must
181
- // itself be valid JSON ('(deleted)' removes the key). Returns the re-serialized
182
- // document, or null when it can't apply — resolveMerge then falls back to the safe
183
- // conflict, so a malformed judgment never ships.
184
- applyResolution: (text, unit, reconciled) => {
185
- if (!unit || typeof unit.key !== 'string' || !unit.key.startsWith('json:')) return null
186
- if (!numbersSafe(text)) return null // same precision gate: never re-serialize a doc we'd corrupt
187
- let doc, value
188
- try { doc = JSON.parse(text) } catch { return null }
189
- const del = String(reconciled).trim() === '(deleted)'
190
- if (!del) { try { value = JSON.parse(reconciled) } catch { return null } }
191
- const path = unit.key.slice(5)
192
- if (path === '(root)' || path === '') {
193
- if (del) return null
194
- doc = value
195
- } else {
196
- const parts = path.split('.')
197
- let cur = doc
198
- for (let i = 0; i < parts.length - 1; i++) {
199
- if (cur == null || typeof cur !== 'object') return null
200
- if (!(parts[i] in cur)) cur[parts[i]] = {}
201
- cur = cur[parts[i]]
202
- }
203
- if (cur == null || typeof cur !== 'object') return null
204
- const leaf = parts[parts.length - 1]
205
- if (del) delete cur[leaf]
206
- else cur[leaf] = value
207
- }
208
- const nl = /\n$/.test(text) ? '\n' : ''
209
- return JSON.stringify(doc, null, detectIndent(text)) + nl
210
- },
211
- }
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
+ // SYMMETRY. The merged object is re-serialized with JSON.stringify, which emits keys in
86
+ // INSERTION order — so the order we walk the key union IS the output byte order. The
87
+ // obvious union, `[...keys(base), ...keys(a), ...keys(b)]`, is side-ordered: ours's new
88
+ // keys always precede theirs's, so merge(base,a,b) and merge(base,b,a) produce clean
89
+ // 'auto' results that differ byte-for-byte. Measured at 43/167 auto merges (26%) by
90
+ // test/icr-data-torture-test.js. Semantically identical, but P2 SYMMETRY is asserted
91
+ // byte-wise for every other language (icr-torture-test.js), and byte-inequality is
92
+ // precisely what a CRDT peer and a git blob hash react to — the two consumers the README
93
+ // names. So the union order must not depend on which side is called `a`:
94
+ // • keys present in base keep BASE's order (unchanged behaviour, minimal diffs);
95
+ // • keys added by either side follow, ordered by where their own author put them
96
+ // (min index across the sides that have it), ties broken by key name.
97
+ // Both rules are symmetric in a/b, so swapping the sides cannot change the output.
98
+ function unionKeys(base, a, b) {
99
+ const rank = new Map()
100
+ for (const obj of [a, b]) {
101
+ const ks = Object.keys(obj)
102
+ for (let i = 0; i < ks.length; i++) {
103
+ const k = ks[i]
104
+ if (k in base) continue
105
+ const prev = rank.get(k)
106
+ if (prev === undefined || i < prev) rank.set(k, i)
107
+ }
108
+ }
109
+ const added = [...rank.keys()].sort((x, y) => (rank.get(x) - rank.get(y)) || (x < y ? -1 : x > y ? 1 : 0))
110
+ return [...Object.keys(base), ...added]
111
+ }
112
+
113
+ // Value-wise 3-way merge. Fills `conflicts` (path strings) and `resolvable`
114
+ // ({key, base, ours, theirs} with pretty-printed values) on same-path clashes;
115
+ // the merged value keeps BASE at conflicted paths (the engine refuses the whole
116
+ // merge when conflicts exist, so nothing partial ever ships).
117
+ function mergeVal(base, a, b, path, conflicts, resolvable) {
118
+ if (eq(a, b)) return a
119
+ if (eq(a, base)) return b
120
+ if (eq(b, base)) return a
121
+ if (isObj(base) && isObj(a) && isObj(b)) {
122
+ const out = {}
123
+ for (const k of unionKeys(base, a, b)) {
124
+ const p = path ? path + '.' + k : k
125
+ const inB = k in base, inA = k in a, inBb = k in b
126
+ if (inB && !inA && !inBb) continue // deleted on both sides
127
+ if (inB && !inA) { // ours deleted it
128
+ if (eq(b[k], base[k])) continue // theirs untouched -> deletion wins
129
+ conflicts.push(p); resolvable.push(resUnit(p, base[k], undefined, b[k])); continue
130
+ }
131
+ if (inB && !inBb) { // theirs deleted it
132
+ if (eq(a[k], base[k])) continue
133
+ conflicts.push(p); resolvable.push(resUnit(p, base[k], a[k], undefined)); continue
134
+ }
135
+ if (!inB && inA && inBb) { // both added
136
+ if (eq(a[k], b[k])) { out[k] = a[k]; continue }
137
+ if (isObj(a[k]) && isObj(b[k])) { out[k] = mergeVal({}, a[k], b[k], p, conflicts, resolvable); continue }
138
+ conflicts.push(p); resolvable.push(resUnit(p, undefined, a[k], b[k])); continue
139
+ }
140
+ if (!inB) { out[k] = inA ? a[k] : b[k]; continue } // one side added
141
+ out[k] = mergeVal(base[k], a[k], b[k], p, conflicts, resolvable) // present everywhere
142
+ }
143
+ return out
144
+ }
145
+ if (Array.isArray(base) && Array.isArray(a) && Array.isArray(b)) {
146
+ const m = mergeArray(base, a, b)
147
+ if (m) return m
148
+ conflicts.push(path || '(root)'); resolvable.push(resUnit(path || '(root)', base, a, b))
149
+ return base
150
+ }
151
+ conflicts.push(path || '(root)') // scalar/shape clash
152
+ resolvable.push(resUnit(path || '(root)', base, a, b))
153
+ return base
154
+ }
155
+
156
+ const show = (v) => (v === undefined ? '(deleted)' : JSON.stringify(v, null, 2))
157
+ const resUnit = (p, base, ours, theirs) => ({ key: 'json:' + p, base: show(base), ours: show(ours), theirs: show(theirs) })
158
+
159
+ export const json = {
160
+ id: 'json',
161
+ exts: ['.json'],
162
+ parses: (src) => { try { JSON.parse(String(src == null ? '' : src)); return true } catch { return false } },
163
+ // The engine's whole-document tier: merge the file as parsed data.
164
+ mergeWhole: (base, a, b) => {
165
+ if (!numbersSafe(base) || !numbersSafe(a) || !numbersSafe(b)) return null // precision hazard → safe fallback, never corrupt
166
+ let B, A, Bb
167
+ try { B = JSON.parse(base); A = JSON.parse(a); Bb = JSON.parse(b) } catch { return null }
168
+ const conflicts = [], resolvable = []
169
+ const merged = mergeVal(B, A, Bb, '', conflicts, resolvable)
170
+ if (conflicts.length) return { conflicts, resolvable }
171
+ const nl = /\n$/.test(base) || /\n$/.test(a) ? '\n' : ''
172
+ return { text: JSON.stringify(merged, null, detectIndent(base)) + nl }
173
+ },
174
+ // Minimal unit view (whole document) so generic engine paths stay well-defined;
175
+ // real merging happens in mergeWhole above.
176
+ units: (src) => [{ key: 'doc', text: String(src), start: 0, end: String(src).length }],
177
+ declaredNames: () => new Set(),
178
+ // INTENT RESOLUTION for data: a conflicted unit's "text" is a rendering of a parsed
179
+ // VALUE, so a judge's reconciliation is applied by setting the value at the unit's
180
+ // path (key 'json:<path>'), not by string replacement. The reconciled string must
181
+ // itself be valid JSON ('(deleted)' removes the key). Returns the re-serialized
182
+ // document, or null when it can't apply — resolveMerge then falls back to the safe
183
+ // conflict, so a malformed judgment never ships.
184
+ applyResolution: (text, unit, reconciled) => {
185
+ if (!unit || typeof unit.key !== 'string' || !unit.key.startsWith('json:')) return null
186
+ if (!numbersSafe(text)) return null // same precision gate: never re-serialize a doc we'd corrupt
187
+ let doc, value
188
+ try { doc = JSON.parse(text) } catch { return null }
189
+ const del = String(reconciled).trim() === '(deleted)'
190
+ if (!del) { try { value = JSON.parse(reconciled) } catch { return null } }
191
+ const path = unit.key.slice(5)
192
+ if (path === '(root)' || path === '') {
193
+ if (del) return null
194
+ doc = value
195
+ } else {
196
+ const parts = path.split('.')
197
+ let cur = doc
198
+ for (let i = 0; i < parts.length - 1; i++) {
199
+ if (cur == null || typeof cur !== 'object') return null
200
+ if (!(parts[i] in cur)) cur[parts[i]] = {}
201
+ cur = cur[parts[i]]
202
+ }
203
+ if (cur == null || typeof cur !== 'object') return null
204
+ const leaf = parts[parts.length - 1]
205
+ if (del) delete cur[leaf]
206
+ else cur[leaf] = value
207
+ }
208
+ const nl = /\n$/.test(text) ? '\n' : ''
209
+ return JSON.stringify(doc, null, detectIndent(text)) + nl
210
+ },
211
+ }