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/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
+ }
package/lang-python.js ADDED
@@ -0,0 +1,360 @@
1
+ // Python language provider for ICR — indentation-based structural splitter.
2
+ // Implements the ICR provider contract { id, exts, parses, units, declaredNames,
3
+ // splitUnit, parsesUnit } so the shared merge engine merges Python by structure:
4
+ // top-level defs/classes/statements are units keyed by name; editing different
5
+ // methods of the same class merges via splitUnit. Heuristic (tracks #-comments,
6
+ // triple-quoted and normal strings, bracket depth) — not a full parser; parses()
7
+ // is an approximate balanced-delimiter check.
8
+
9
+ function scannerAt(src) {
10
+ const n = src.length
11
+ return function at(i) {
12
+ if (src[i] === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; return j }
13
+ for (const q of ['"""', "'''"]) if (src.startsWith(q, i)) { let j = i + 3; while (j < n && !src.startsWith(q, j)) { if (src[j] === '\\') j++; j++ } return Math.min(n, j + 3) }
14
+ if (src[i] === '"' || 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 } if (src[j] === '\n') break; j++ } return j }
15
+ return -1
16
+ }
17
+ }
18
+
19
+ // Line records: { start, end (incl newline), indent, blank, text }
20
+ function lines(src) {
21
+ const out = []
22
+ let i = 0
23
+ while (i < src.length) {
24
+ let e = src.indexOf('\n', i); if (e < 0) e = src.length; else e = e + 1
25
+ const raw = src.slice(i, e)
26
+ const trimmed = raw.replace(/\r?\n$/, '')
27
+ let indent = 0; while (indent < trimmed.length && (trimmed[indent] === ' ' || trimmed[indent] === '\t')) indent++
28
+ const blank = trimmed.trim() === '' || trimmed.trim().startsWith('#')
29
+ out.push({ start: i, end: e, indent, blank, text: raw })
30
+ i = e
31
+ }
32
+ return out
33
+ }
34
+
35
+ const HEAD = /^\s*(?:async\s+def|def)\s+([A-Za-z_]\w*)/
36
+ const CLS = /^\s*class\s+([A-Za-z_]\w*)/
37
+ // Continuation clauses of a COMPOUND statement. They are not independent statements —
38
+ // an `else:` only means something relative to its `if`/`for`/`try` — so blocks() must
39
+ // attach them to the preceding unit. Keying them separately let two sides' restructured
40
+ // if/else halves interleave into adjacent dangling `else:` blocks (ConGra, transformers
41
+ // file_utils.py: output had `else:` twice in a row — ast-invalid, heuristics blind).
42
+ const COMPOUND_CONT = /^\s*(?:else\b|elif\b|except\b|finally\b)/
43
+
44
+ function keyOf(text) {
45
+ let m = text.match(HEAD); if (m) return 'def:' + m[1]
46
+ m = text.match(CLS); if (m) return 'class:' + m[1]
47
+ return 'stmt:' + text.split('\n')[0].trim().replace(/\s+/g, ' ')
48
+ }
49
+
50
+ // Split src into units whose heads sit at exactly `base` indentation. A def/class
51
+ // head owns the following blank + more-indented lines (its body). A run of leading
52
+ // @decorator lines attaches to the head that follows. Everything else is a
53
+ // one-logical-line statement (continued while bracket depth > 0).
54
+ function blocks(src, base = 0) {
55
+ const L = lines(src)
56
+ const at = scannerAt(src)
57
+ const units = []
58
+ let i = 0
59
+ // find bracket depth over a line range (to keep continued statements together)
60
+ const bracketDelta = (s, e) => {
61
+ let d = 0, k = s
62
+ while (k < e) { const j = at(k); if (j > k) { k = j; continue } const c = src[k]; if (c === '(' || c === '[' || c === '{') d++; else if (c === ')' || c === ']' || c === '}') d--; k++ }
63
+ return d
64
+ }
65
+ while (i < L.length) {
66
+ while (i < L.length && L[i].text.trim() === '') i++ // skip ONLY pure blank lines, NOT comments
67
+ if (i >= L.length) break
68
+ if (L[i].indent !== base) { // stray deeper line (shouldn't happen at top) — treat as its own stmt
69
+ const s = L[i].start; let e = L[i].end; i++
70
+ units.push({ start: s, end: e })
71
+ continue
72
+ }
73
+ const start = L[i].start
74
+ // consume leading decorators
75
+ let isDecorator = /^\s*@/.test(L[i].text)
76
+ while (i < L.length && L[i].indent === base && /^\s*@/.test(L[i].text)) i++
77
+ const headIdx = i
78
+ const head = i < L.length ? L[i] : null
79
+ if (head && (HEAD.test(head.text) || CLS.test(head.text) || isDecorator)) {
80
+ // a block: consume the header line, then its body (blank or deeper-indented lines)
81
+ i++ // header line
82
+ while (i < L.length && (L[i].blank || L[i].indent > base)) i++
83
+ // Trailing PURE-BLANK lines are GAP text, not part of the unit — if the unit owned
84
+ // them, appending a new unit after the last one would mutate the last unit's text
85
+ // and raise a false same-key conflict (found by multilang fuzz). Trailing comments
86
+ // stay attached (comment-attachment convention).
87
+ let last = i - 1
88
+ while (last > headIdx && L[last].text.trim() === '') last--
89
+ units.push({ start, end: L[last].end })
90
+ } else if (head) {
91
+ // simple statement: consume continued lines while bracket depth remains open
92
+ let e = head.end, depth = bracketDelta(head.start, head.end); i++
93
+ while (i < L.length && (depth > 0 || /\\\s*$/.test(L[i - 1].text.replace(/\r?\n$/, '')))) { depth += bracketDelta(L[i].start, L[i].end); e = L[i].end; i++ }
94
+ // COMPOUND statement (if/for/while/try/with/else/…): the logical line ends with ':',
95
+ // so this header OWNS its indented body — one unit, like def/class. Without this the
96
+ // body lines become stray per-line units and the clause structure can't survive a
97
+ // structured merge coherently.
98
+ const logicalEndsColon = (() => {
99
+ const at = scannerAt(src)
100
+ let lastC = '', k = start
101
+ while (k < e) { const j = at(k); if (j > k) { if (src[k] !== '#') lastC = 's'; k = Math.min(e, j); continue } const c = src[k]; if (c !== ' ' && c !== '\t' && c !== '\r' && c !== '\n') lastC = c; k++ }
102
+ return lastC === ':'
103
+ })()
104
+ if (logicalEndsColon) {
105
+ const bodyHead = i - 1
106
+ while (i < L.length && (L[i].blank || L[i].indent > base)) i++
107
+ let last = i - 1
108
+ while (last > bodyHead && L[last].text.trim() === '') last--
109
+ e = L[last].end
110
+ }
111
+ // Continuation clause (else/elif/except/finally): not an independent statement —
112
+ // extend the PREVIOUS unit so the whole compound statement is one mergeable unit.
113
+ if (COMPOUND_CONT.test(head.text) && units.length) units[units.length - 1].end = e
114
+ else units.push({ start, end: e })
115
+ }
116
+ }
117
+ return units
118
+ }
119
+
120
+ function keyed(raw, src) {
121
+ const seen = new Map(), out = []
122
+ for (const u of raw) {
123
+ const text = src.slice(u.start, u.end)
124
+ let k = keyOf(text)
125
+ const c = (seen.get(k) || 0) + 1; seen.set(k, c)
126
+ if (c > 1) k += '#' + c
127
+ out.push({ key: k, text, start: u.start, end: u.end })
128
+ }
129
+ return out
130
+ }
131
+
132
+ function balanced(src) {
133
+ const at = scannerAt(src)
134
+ const stack = []
135
+ const pair = { ')': '(', ']': '[', '}': '{' }
136
+ let i = 0
137
+ while (i < src.length) {
138
+ const j = at(i); if (j > i) { if (j > src.length) return false; i = j; continue }
139
+ const c = src[i]
140
+ if (c === '(' || c === '[' || c === '{') stack.push(c)
141
+ else if (pair[c]) { if (stack.pop() !== pair[c]) return false }
142
+ i++
143
+ }
144
+ return stack.length === 0
145
+ }
146
+
147
+ // INDENTATION validity — the half of Python syntax that delimiter balance cannot see.
148
+ // Walks LOGICAL lines (joining bracket continuations, backslash continuations, and
149
+ // multi-line strings) and enforces Python's block rules: a line ending with ':' must
150
+ // open a deeper-indented block; an indent may only appear after such an opener; every
151
+ // dedent must land on an indent level already on the stack. This is what lets parses()
152
+ // refuse a merge that spliced statements into the wrong block — balance alone passes it.
153
+ // The first line's indent is taken as the baseline so unit fragments validate too.
154
+ function indentsOk(src) {
155
+ const at = scannerAt(src)
156
+ const n = src.length
157
+ let stack = null // [indent levels]; seeded from the first logical line
158
+ let i = 0, prevOpened = false
159
+ while (i < n) {
160
+ let indent = 0
161
+ while (i < n && (src[i] === ' ' || src[i] === '\t')) { indent++; i++ }
162
+ if (i >= n) break
163
+ if (src[i] === '\r' || src[i] === '\n') { i++; continue } // blank line
164
+ if (src[i] === '#') { i = Math.min(n, at(i)); continue } // comment-only line
165
+ if (stack == null) stack = [indent] // baseline (fragment-friendly)
166
+ if (prevOpened) {
167
+ if (indent <= stack[stack.length - 1]) return false // ':' promised a block; none came
168
+ stack.push(indent)
169
+ } else if (indent > stack[stack.length - 1]) {
170
+ return false // indent without an opener
171
+ } else if (indent < stack[stack.length - 1]) {
172
+ while (stack.length > 1 && stack[stack.length - 1] > indent) stack.pop()
173
+ if (stack[stack.length - 1] !== indent) return false // dedent to a level never opened
174
+ }
175
+ // scan to the end of this LOGICAL line (brackets, backslash, strings span physical lines)
176
+ let depth = 0, last = ''
177
+ while (i < n) {
178
+ const j = at(i)
179
+ if (j > i) { if (src[i] !== '#') last = 's'; i = Math.min(n, j); continue } // string counts as content; comment doesn't
180
+ const c = src[i]
181
+ if (c === '(' || c === '[' || c === '{') depth++
182
+ else if (c === ')' || c === ']' || c === '}') depth--
183
+ if (c === '\n') {
184
+ if (depth > 0 || last === '\\') { if (last === '\\') last = ''; i++; continue } // continuation
185
+ i++; break
186
+ }
187
+ if (c !== ' ' && c !== '\t' && c !== '\r') last = c
188
+ i++
189
+ }
190
+ prevOpened = last === ':'
191
+ }
192
+ return !prevOpened // must not end still promising a block
193
+ }
194
+
195
+ // Generic lexer for the token-level inner merge + the intent layer. Identifiers,
196
+ // numbers, whole strings are tokens; comments live in the gaps (a comment-only edit
197
+ // is a no-op at this tier, same contract as the JS provider).
198
+ function lexTokens(src) {
199
+ const at = scannerAt(src)
200
+ const out = []
201
+ const n = src.length
202
+ let i = 0
203
+ while (i < n) {
204
+ const c = src[i]
205
+ if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { i++; continue }
206
+ const j = at(i)
207
+ if (j > i) {
208
+ if (c === '#') { i = j; continue } // comment → gap
209
+ out.push({ start: i, end: j, k: src.slice(i, j) }); i = j; continue // string → one token
210
+ }
211
+ if (/[A-Za-z_]/.test(c)) { let e = i + 1; while (e < n && /\w/.test(src[e])) e++; out.push({ start: i, end: e, k: src.slice(i, e) }); i = e; continue }
212
+ if (/[0-9]/.test(c)) { let e = i + 1; while (e < n && /[\w.]/.test(src[e])) e++; out.push({ start: i, end: e, k: src.slice(i, e) }); i = e; continue }
213
+ out.push({ start: i, end: i + 1, k: c }); i++
214
+ }
215
+ return out
216
+ }
217
+
218
+ // Token stream for the token-level MERGE tier, in an indentation language. Unlike
219
+ // lexTokens (used by the intent layer, where whitespace is noise), this emits a NEWLINE
220
+ // token for every real line break — a statement boundary. Without it a token splice can
221
+ // weld two statements onto one physical line (`x = f(y)if z:`), which balance+indent
222
+ // checks read as a single logical line and wrongly accept — a parse-guarantee violation
223
+ // found by the ConGra corpus (ast.parse rejects it). With newlines as tokens, any spliced
224
+ // run that crosses a line carries its break, so statements can never fuse. Newlines inside
225
+ // strings/comments stay inside those tokens; a bare `\n` between brackets is still emitted
226
+ // (harmless: it only ever makes the token diff finer, never coarser).
227
+ function lexTokensNL(src) {
228
+ const at = scannerAt(src)
229
+ const out = []
230
+ const n = src.length
231
+ let i = 0, depth = 0, contBackslash = false
232
+ while (i < n) {
233
+ const c = src[i]
234
+ if (c === '\n') {
235
+ // Emit a NEWLINE token only at a real statement boundary: not inside brackets, not
236
+ // after a line-continuation backslash. Inside brackets a newline is insignificant
237
+ // (like whitespace) — emitting it there corrupts multi-line-call token interleaving.
238
+ if (depth === 0 && !contBackslash) out.push({ start: i, end: i + 1, k: '\n' })
239
+ contBackslash = false; i++; continue
240
+ }
241
+ if (c === ' ' || c === '\t' || c === '\r') { i++; continue }
242
+ const j = at(i)
243
+ if (j > i) {
244
+ contBackslash = false
245
+ if (c === '#') { i = j; continue }
246
+ out.push({ start: i, end: j, k: src.slice(i, j) }); i = j; continue
247
+ }
248
+ if (c === '\\') { contBackslash = true; i++; continue }
249
+ contBackslash = false
250
+ if (c === '(' || c === '[' || c === '{') depth++
251
+ else if (c === ')' || c === ']' || c === '}') { if (depth > 0) depth-- }
252
+ if (/[A-Za-z_]/.test(c)) { let e = i + 1; while (e < n && /\w/.test(src[e])) e++; out.push({ start: i, end: e, k: src.slice(i, e) }); i = e; continue }
253
+ if (/[0-9]/.test(c)) { let e = i + 1; while (e < n && /[\w.]/.test(src[e])) e++; out.push({ start: i, end: e, k: src.slice(i, e) }); i = e; continue }
254
+ out.push({ start: i, end: i + 1, k: c }); i++
255
+ }
256
+ return out
257
+ }
258
+
259
+ // Reference-position identifier (not attribute access `.name`)?
260
+ function isRefToken(src, t) {
261
+ if (!/^[A-Za-z_]\w*$/.test(t.k)) return false
262
+ let p = t.start - 1
263
+ while (p >= 0 && (src[p] === ' ' || src[p] === '\t')) p--
264
+ return !(p >= 0 && src[p] === '.')
265
+ }
266
+
267
+ // PER-STATEMENT bracket nesting — the soundness backstop the whole-file `balanced()`
268
+ // (global) and `indentsOk()` (indentation) both miss. A token merge can splice a result
269
+ // that is GLOBALLY balanced yet LOCALLY mis-nested: side A reformats `f(x)` to span lines
270
+ // and a concurrent edit detaches the `)`, so `( ... { ... )` lands in one statement and a
271
+ // stray `)` balances the count elsewhere. In VALID Python every logical statement is
272
+ // independently balanced AND properly nested, so we check exactly that: segment the token
273
+ // stream at depth-0 NEWLINE tokens (which lexTokensNL only emits between statements) and
274
+ // require each segment's brackets to nest correctly. Sound — it never rejects valid Python
275
+ // — and it closes the token-merge corruption class the ConGra corpus exposed via ast.parse.
276
+ function stmtBracketsOk(src) {
277
+ const pair = { ')': '(', ']': '[', '}': '{' }
278
+ let stack = []
279
+ for (const t of lexTokensNL(src)) {
280
+ if (t.k === '\n') { if (stack.length) return false; continue } // statement boundary: must be closed
281
+ if (t.k === '(' || t.k === '[' || t.k === '{') stack.push(t.k)
282
+ else if (pair[t.k]) { if (stack.pop() !== pair[t.k]) return false }
283
+ }
284
+ return stack.length === 0
285
+ }
286
+
287
+ export const python = {
288
+ id: 'python',
289
+ exts: ['.py', '.pyi'],
290
+ parses: (src) => { const s = String(src == null ? '' : src); return balanced(s) && stmtBracketsOk(s) && indentsOk(s) },
291
+ parsesUnit: (src) => { const s = String(src); return balanced(s) && indentsOk(s) },
292
+ units: (src) => keyed(blocks(String(src), 0), String(src)),
293
+ declaredNames: (src) => {
294
+ const names = new Set()
295
+ for (const u of keyed(blocks(String(src), 0), String(src))) {
296
+ const m = u.key.match(/^(?:def|class):([A-Za-z_]\w*)/)
297
+ if (m) names.add(m[1])
298
+ }
299
+ return names
300
+ },
301
+ // --- intent layer (heuristic but honest): dangling refs, rename, token merge ---
302
+ // Every identifier used in reference position (skips `.attribute` access). Approximate
303
+ // and OVER-inclusive — which only makes the dangling check MORE cautious: a deleted-
304
+ // but-still-referenced def/class is surfaced as a conflict, never shipped broken.
305
+ usedIdentifiers: (src) => {
306
+ src = String(src)
307
+ const used = new Set()
308
+ for (const t of lexTokens(src)) if (isRefToken(src, t)) used.add(t.k)
309
+ return used
310
+ },
311
+ // The def/class text with its own name excluded, so renamed twins compare equal.
312
+ // trimEnd(): a unit owns its trailing blank lines, and splicing can change how many
313
+ // survive — trailing whitespace must not defeat the rename match.
314
+ declBody: (src, name) => {
315
+ src = String(src)
316
+ for (const u of keyed(blocks(src, 0), src)) {
317
+ const m = u.key.match(/^(?:def|class):([A-Za-z_]\w*)/)
318
+ if (!m || m[1] !== name) continue
319
+ return u.text.replace(name, '').trimEnd()
320
+ }
321
+ return null
322
+ },
323
+ // Rewrite reference-position uses of oldName (never `.oldName` attribute access).
324
+ // The engine only accepts the rewrite if the result still passes parses().
325
+ renameRefs: (src, oldName, newName) => {
326
+ src = String(src)
327
+ const spots = []
328
+ for (const t of lexTokens(src)) if (t.k === oldName && isRefToken(src, t)) spots.push(t)
329
+ let out = src
330
+ for (const t of spots.reverse()) out = out.slice(0, t.start) + newName + out.slice(t.end)
331
+ return out
332
+ },
333
+ // Finest granularity: token stream for the token-level inner merge. NEWLINE-aware
334
+ // (lexTokensNL) so a token splice can never weld two statements onto one line — the
335
+ // parse guarantee holds even where the indentation heuristic alone would be fooled.
336
+ tokenize: (src) => lexTokensNL(String(src)),
337
+ // Descend into a class/def body so two agents editing DIFFERENT methods merge.
338
+ splitUnit: (text) => {
339
+ const src = String(text)
340
+ const L = lines(src)
341
+ let h = 0; while (h < L.length && (L[h].blank || /^\s*@/.test(L[h].text))) h++
342
+ if (h >= L.length) return null
343
+ // Only descend into a CLASS (named methods → mergeable). A def body is bare
344
+ // statements with no stable keys; descending would falsely merge two edits to the
345
+ // same function, so we don't — same-def edits become a clean conflict instead.
346
+ if (!CLS.test(L[h].text)) return null
347
+ const headEnd = L[h].end
348
+ // body = everything after the header line; its base indent is the first body line's indent
349
+ let b = h + 1; while (b < L.length && L[b].blank) b++
350
+ if (b >= L.length) return null
351
+ const bodyIndent = L[b].indent
352
+ const sig = src.slice(0, headEnd)
353
+ const body = src.slice(headEnd)
354
+ const inner = keyed(blocks(body, bodyIndent), body)
355
+ // spliceable: inner units carry byte ranges in `body`, so the engine rebuilds the
356
+ // class body by splicing merged methods back in — comments/blank lines between
357
+ // methods survive verbatim instead of being reflowed by a join.
358
+ return { sig, open: '', close: '', body, units: inner, join: '', spliceable: true }
359
+ },
360
+ }