hivecode-mcp 0.14.1 → 0.14.3

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.
Files changed (72) hide show
  1. package/HIVE_RULES.md +98 -98
  2. package/LICENSE +21 -21
  3. package/README.md +421 -421
  4. package/core.js +91 -91
  5. package/docs/ALWAYS_ON.md +310 -310
  6. package/docs/GITHUB_WORKFLOW.md +394 -394
  7. package/docs/HIVE_RULES.md +41 -41
  8. package/docs/MCP.md +157 -157
  9. package/docs/QUICKSTART.md +266 -266
  10. package/docs/SELF_HOSTING.md +256 -256
  11. package/docs/SLACK_SETUP.md +474 -474
  12. package/docs/TROUBLESHOOTING.md +106 -105
  13. package/hive-activity.js +336 -336
  14. package/hive-authors.js +46 -44
  15. package/hive-await.js +230 -230
  16. package/hive-branch.js +674 -607
  17. package/hive-busy.js +88 -88
  18. package/hive-coord.js +110 -110
  19. package/hive-gate.js +135 -135
  20. package/hive-github.js +3 -1
  21. package/hive-hook-install.js +179 -179
  22. package/hive-instructions.js +74 -74
  23. package/hive-land.js +709 -691
  24. package/hive-listen.js +81 -81
  25. package/hive-mailbox.js +520 -520
  26. package/hive-mcp-http.js +591 -591
  27. package/hive-mcp.js +92 -12
  28. package/hive-mux-proto.js +58 -58
  29. package/hive-mux.js +354 -351
  30. package/hive-notify.js +234 -233
  31. package/hive-obligations.js +247 -247
  32. package/hive-poll-proto.js +50 -50
  33. package/hive-pr-staleness.js +189 -189
  34. package/hive-pr.js +637 -512
  35. package/hive-privacy.js +327 -327
  36. package/hive-receipts.js +233 -233
  37. package/hive-relay-health.js +234 -234
  38. package/hive-repos.js +430 -430
  39. package/hive-room-key.js +210 -210
  40. package/hive-room-mode.js +1761 -1668
  41. package/hive-room-watcher.js +623 -561
  42. package/hive-secrets.js +89 -89
  43. package/hive-session.js +114 -114
  44. package/hive-setup.js +560 -560
  45. package/hive-slack-agent.js +408 -408
  46. package/hive-slack.js +2417 -2367
  47. package/hive-stdio.js +172 -172
  48. package/hive-stop-hook.js +293 -293
  49. package/hive-throttle.js +118 -118
  50. package/hive-time.js +71 -71
  51. package/hive-transport.js +617 -578
  52. package/hive-update.js +58 -58
  53. package/hive-version.js +110 -110
  54. package/hive-webhook.js +55 -54
  55. package/icr-merge.js +58 -58
  56. package/icr.js +856 -851
  57. package/lang-brace.js +305 -305
  58. package/lang-js.js +695 -651
  59. package/lang-json.js +211 -211
  60. package/lang-python.js +588 -588
  61. package/lang-ruby.js +230 -230
  62. package/lang-toml.js +136 -136
  63. package/lang-yaml.js +183 -183
  64. package/limits.js +413 -413
  65. package/package.json +11 -3
  66. package/reconcile.cjs +621 -621
  67. package/slack-app-manifest-agent.json +61 -61
  68. package/slack-app-manifest-inbound.json +36 -36
  69. package/slack-app-manifest.json +27 -27
  70. package/substrate.js +252 -252
  71. package/sync.js +434 -60
  72. package/token.js +201 -201
package/lang-js.js CHANGED
@@ -1,651 +1,695 @@
1
- // lang-js.js — the JavaScript language provider for ICR.
2
- //
3
- // ICR's merge engine (icr.js) is language-agnostic: it merges a list of keyed "units"
4
- // and asks intent questions ("what names are declared / used / what's a function body").
5
- // Everything that actually knows about a *language* — how to parse it, what a top-level
6
- // declaration is, how to find references — lives behind a provider like this one.
7
- //
8
- // To add a language (Python, Go, Rust…), write another module exposing the same shape
9
- // and register it with icr.js's registerLanguage(). This JS provider is just the first
10
- // plugin; it happens to use `acorn` because acorn is a tiny pure-JS parser. A tree-sitter
11
- // provider would implement the identical interface over tree-sitter grammars.
12
-
13
- import * as acorn from 'acorn'
14
-
15
- // Parse permissively: try module syntax, fall back to script (so plain snippets and
16
- // ESM both work). Throws only when the code genuinely won't parse either way.
17
- function parseRaw(src) {
18
- try { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'module' }) }
19
- catch { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'script' }) }
20
- }
21
-
22
- // Tiny parse cache. One structuralMerge re-parses the SAME three texts (and the merged
23
- // result) many times across its tiers — gate, units, declaredNames, scope analysis,
24
- // final validation. Caching by exact text collapses that to one parse per distinct
25
- // version (a 1MB-file merge dropped ~7.6s → ~3s). Small and bounded: 8 entries, LRU.
26
- // String comparison is a cheap memcmp; ASTs are treated as immutable by all callers.
27
- const PARSE_CACHE = new Map() // src -> ast (Map preserves insertion order → LRU)
28
- function parse(src) {
29
- const hit = PARSE_CACHE.get(src)
30
- if (hit) { PARSE_CACHE.delete(src); PARSE_CACHE.set(src, hit); return hit } // refresh LRU position
31
- const ast = parseRaw(src)
32
- PARSE_CACHE.set(src, ast)
33
- if (PARSE_CACHE.size > 8) PARSE_CACHE.delete(PARSE_CACHE.keys().next().value)
34
- return ast
35
- }
36
-
37
- function parses(src) { try { parse(src); return true } catch { return false } }
38
-
39
- // A stable key per top-level node — what lets us recognize "the same declaration"
40
- // across two edits. Names where we have them; position as a last resort.
41
- // Dotted source path of a simple expression target (Axios.prototype.request,
42
- // utils.forEach, this.config) — or null when it isn't a plain dotted path.
43
- function exprPath(node) {
44
- if (!node) return null
45
- if (node.type === 'Identifier') return node.name
46
- if (node.type === 'ThisExpression') return 'this'
47
- if (node.type === 'MemberExpression' && !node.computed) {
48
- const o = exprPath(node.object)
49
- return o && node.property && node.property.name ? o + '.' + node.property.name : null
50
- }
51
- return null
52
- }
53
-
54
- // CONTENT-ANCHORED key for a statement, or null for truly anonymous ones.
55
- // Index keys (stmt:N) are a last resort ONLY: an insertion above an index-keyed
56
- // statement shifts every index, so unrelated statements pair up as fake
57
- // same-unit conflicts — and deletions mispair so deleted code can resurrect.
58
- // (Both failure modes were found by the merge census replaying axios/express
59
- // history.) Assignments key by their target, calls by callee path plus a
60
- // leading string literal (`it('name')`, `describe('name')`), directives by text.
61
- function baseKeyOf(node) {
62
- let n = node
63
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
64
- if (n.type === 'ExportDefaultDeclaration') return 'export:default'
65
- if (n.type === 'FunctionDeclaration' && n.id) return 'fn:' + n.id.name
66
- if (n.type === 'ClassDeclaration' && n.id) return 'class:' + n.id.name
67
- if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name)
68
- return 'var:' + n.declarations[0].id.name
69
- // Key imports by their SOURCE MODULE, so two agents adding imports from DIFFERENT
70
- // modules never collide, and two touching the SAME module are merged (specifier union).
71
- if (n.type === 'ImportDeclaration') return 'import:' + (n.source && n.source.value != null ? n.source.value : '?')
72
- // Specifier-only exports (`export { a, b }`, `export { x } from './m'`, `export * from …`)
73
- // carry no declaration, so without a stable key they fall to positional stmt:N — and an
74
- // edit to the export list then mispairs, DUPLICATES the statement, and knocks the whole
75
- // merge to the line tier (which silently skips the intent/dangling checks). A stable key
76
- // pairs them so an unedited side is kept and an edited side wins cleanly.
77
- if (n.type === 'ExportNamedDeclaration') return n.source && n.source.value != null ? 'export-from:' + n.source.value : 'export-names'
78
- if (n.type === 'ExportAllDeclaration') return 'export-all:' + (n.source && n.source.value != null ? n.source.value : '?')
79
- if (n.type === 'ExpressionStatement') {
80
- const e = n.expression
81
- if (e.type === 'Literal' && typeof e.value === 'string') return 'directive:' + e.value // 'use strict'
82
- if (e.type === 'AssignmentExpression') { const t = exprPath(e.left); if (t) return 'assign:' + t }
83
- if (e.type === 'CallExpression') {
84
- const t = exprPath(e.callee)
85
- if (t) {
86
- const a0 = e.arguments && e.arguments[0]
87
- return 'call:' + t + (a0 && a0.type === 'Literal' && typeof a0.value === 'string' ? ':' + a0.value : '')
88
- }
89
- }
90
- }
91
- return null
92
- }
93
-
94
- // Key an ordered statement list: content-anchored where possible (duplicates get
95
- // #1, #2… by occurrence), positional stmt:N only for the anonymous remainder.
96
- function keyStatements(nodes) {
97
- const seen = new Map()
98
- return nodes.map((n, i) => {
99
- const k = baseKeyOf(n)
100
- if (k == null) return 'stmt:' + i
101
- const c = seen.get(k) || 0
102
- seen.set(k, c + 1)
103
- return c ? k + '#' + c : k
104
- })
105
- }
106
-
107
- function keyOf(node, i) { const k = baseKeyOf(node); return k == null ? 'stmt:' + i : k }
108
-
109
- // Top-level units: ordered { key, text, start, end } for each declaration/statement.
110
- // start/end are byte offsets in `src` — used by the format-preserving splice so unchanged
111
- // regions (and the whitespace/comments between units) survive a merge verbatim.
112
- function units(src) {
113
- const ast = parse(src)
114
- const keys = keyStatements(ast.body)
115
- return ast.body.map((node, i) => ({ key: keys[i], text: src.slice(node.start, node.end), start: node.start, end: node.end }))
116
- }
117
-
118
- // Bare declared name (foo, Bar, VERSION) of a node, or null for anonymous statements.
119
- function bareName(node) {
120
- let n = node
121
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
122
- if (n.type === 'FunctionDeclaration' && n.id) return n.id.name
123
- if (n.type === 'ClassDeclaration' && n.id) return n.id.name
124
- if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name) return n.declarations[0].id.name
125
- return null
126
- }
127
-
128
- function declaredNames(src) {
129
- const s = new Set()
130
- for (const node of parse(src).body) {
131
- let n = node
132
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
133
- if (n.type === 'VariableDeclaration') {
134
- for (const d of n.declarations) bindingNames(d.id, s)
135
- } else {
136
- const nm = bareName(node); if (nm) s.add(nm)
137
- }
138
- // Import locals count as declared names too — so if an import is ever dropped and its
139
- // binding is still used, the dangling-reference check catches it (safety net for the
140
- // import merge). Keeps the never-emit-broken-code guarantee honest for imports.
141
- if (node.type === 'ImportDeclaration') for (const sp of node.specifiers) s.add(sp.local.name)
142
- if (node.type === 'ExportNamedDeclaration' && !node.declaration && node.specifiers) {
143
- for (const sp of node.specifiers) s.add(sp.exported.name || sp.exported.value || sp.local.name)
144
- }
145
- }
146
- return s
147
- }
148
-
149
- // --- import-aware merging -------------------------------------------------------
150
- // Parse a single import statement into its parts, or null if it isn't one.
151
- function parseImport(src) {
152
- let n
153
- try { n = parse(src).body[0] } catch { return null }
154
- if (!n || n.type !== 'ImportDeclaration') return null
155
- let def = null, ns = null
156
- const named = []
157
- for (const sp of n.specifiers) {
158
- if (sp.type === 'ImportDefaultSpecifier') def = sp.local.name
159
- else if (sp.type === 'ImportNamespaceSpecifier') ns = sp.local.name
160
- else named.push({ imported: sp.imported.name != null ? sp.imported.name : sp.imported.value, local: sp.local.name })
161
- }
162
- return { source: n.source.value, def, ns, named }
163
- }
164
- function renderImport({ source, def, ns, named }) {
165
- const parts = []
166
- if (def) parts.push(def)
167
- if (ns) parts.push('* as ' + ns)
168
- if (named.length) parts.push('{ ' + named.map((s) => s.imported === s.local ? s.local : s.imported + ' as ' + s.local).join(', ') + ' }')
169
- if (!parts.length) return "import '" + source + "'"
170
- return 'import ' + parts.join(', ') + " from '" + source + "'"
171
- }
172
- // Both sides changed an import from the SAME module → union their specifiers into one
173
- // statement (the common "both agents added an import" case). Returns null — meaning
174
- // "let the normal conflict path handle it" — for anything we can't safely combine
175
- // (different modules, clashing defaults/namespaces, namespace-mixed-with-named).
176
- function mergeUnit(baseText, aText, bText) {
177
- const a = parseImport(aText), b = parseImport(bText)
178
- if (!a || !b || a.source !== b.source) return null
179
- let def
180
- if (a.def && b.def) { if (a.def !== b.def) return null; def = a.def } else def = a.def || b.def
181
- let ns
182
- if (a.ns && b.ns) { if (a.ns !== b.ns) return null; ns = a.ns } else ns = a.ns || b.ns
183
- if (ns && (a.named.length || b.named.length)) return null // `* as X` can't share a statement with named imports
184
- const seen = new Set(), named = []
185
- for (const s of [...a.named, ...b.named]) { const k = s.imported + '|' + s.local; if (!seen.has(k)) { seen.add(k); named.push(s) } }
186
- // Canonical specifier order: the union must not depend on which side is `a` — the merge
187
- // has to be SYMMETRIC (merge(a,b) === merge(b,a)) or two live peers never settle on one
188
- // byte-form. (Found by the property fuzzer: a 1-in-40k NOT-SYMMETRIC violation.)
189
- named.sort((x, y) => (x.imported + '|' + x.local).localeCompare(y.imported + '|' + y.local))
190
- return renderImport({ source: a.source, def, ns, named })
191
- }
192
-
193
- // The BODY of a top-level declaration, by name, with the name itself excluded — so two
194
- // declarations that differ only in their name compare equal (that's how we spot a rename).
195
- function declBody(src, name) {
196
- for (const node of parse(src).body) {
197
- if (bareName(node) !== name) continue
198
- let n = node
199
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
200
- if ((n.type === 'FunctionDeclaration' || n.type === 'ClassDeclaration') && n.body)
201
- return src.slice(n.body.start, n.body.end)
202
- if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].init) {
203
- const init = n.declarations[0].init
204
- return src.slice(init.start, init.end)
205
- }
206
- return null
207
- }
208
- return null
209
- }
210
-
211
- // Check if top-level declaration `name` is declared `async`
212
- function isAsyncDecl(src, name) {
213
- try {
214
- for (const node of parse(src).body) {
215
- if (bareName(node) !== name) continue
216
- let n = node
217
- if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
218
- if (n.type === 'ExportDefaultDeclaration' && n.declaration) n = n.declaration
219
- if (n.type === 'FunctionDeclaration') return !!n.async
220
- if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].init) {
221
- const init = n.declarations[0].init
222
- if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
223
- return !!init.async
224
- }
225
- }
226
- }
227
- } catch {}
228
- return false
229
- }
230
-
231
- // Upgrade free call sites of `fnName` to `await fnName(...)` if they are inside an async parent
232
- // and not already awaited.
233
- function upgradeCallSitesToAwait(src, fnName) {
234
- try {
235
- const ast = parse(src)
236
- const spots = []
237
-
238
- function walkAST(node, parent, isAsyncScope = false) {
239
- if (!node || typeof node.type !== 'string') return
240
-
241
- let childAsyncScope = isAsyncScope
242
- if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
243
- childAsyncScope = !!node.async
244
- }
245
-
246
- if (node.type === 'CallExpression') {
247
- let isTarget = false
248
- if (node.callee.type === 'Identifier' && node.callee.name === fnName) isTarget = true
249
- if (isTarget && childAsyncScope) {
250
- const alreadyAwaited = parent && parent.type === 'AwaitExpression'
251
- if (!alreadyAwaited) {
252
- spots.push(node.start)
253
- }
254
- }
255
- }
256
-
257
- for (const k of Object.keys(node)) {
258
- if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
259
- const v = node[k]
260
- if (Array.isArray(v)) {
261
- for (const c of v) walkAST(c, node, childAsyncScope)
262
- } else if (v && typeof v.type === 'string') {
263
- walkAST(v, node, childAsyncScope)
264
- }
265
- }
266
- }
267
-
268
- walkAST(ast, null, false)
269
- if (!spots.length) return src
270
-
271
- spots.sort((a, b) => b - a)
272
- let out = src
273
- for (const pos of spots) {
274
- out = out.slice(0, pos) + 'await ' + out.slice(pos)
275
- }
276
- return out
277
- } catch {
278
- return src
279
- }
280
- }
281
-
282
- // Find callers of `fnName` that are synchronous (cannot be auto-awaited without changing the caller's signature)
283
- function syncCallSitesOf(src, fnName) {
284
- const syncCallers = []
285
- try {
286
- const ast = parse(src)
287
- function walkAST(node, currentFnName = null, isAsyncScope = false) {
288
- if (!node || typeof node.type !== 'string') return
289
-
290
- let childAsyncScope = isAsyncScope
291
- let fnNameContext = currentFnName
292
- if (node.type === 'FunctionDeclaration') {
293
- childAsyncScope = !!node.async
294
- fnNameContext = node.id ? node.id.name : '<anonymous>'
295
- } else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
296
- childAsyncScope = !!node.async
297
- if (node.id) fnNameContext = node.id.name
298
- }
299
-
300
- if (node.type === 'CallExpression') {
301
- if (node.callee.type === 'Identifier' && node.callee.name === fnName) {
302
- if (!childAsyncScope) {
303
- syncCallers.push(fnNameContext || '<top-level>')
304
- }
305
- }
306
- }
307
-
308
- for (const k of Object.keys(node)) {
309
- if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
310
- const v = node[k]
311
- if (Array.isArray(v)) {
312
- for (const c of v) walkAST(c, fnNameContext, childAsyncScope)
313
- } else if (v && typeof v.type === 'string') {
314
- walkAST(v, fnNameContext, childAsyncScope)
315
- }
316
- }
317
- }
318
- walkAST(ast, null, false)
319
- } catch {}
320
- return syncCallers
321
- }
322
-
323
- // Rewrite every reference to `oldName` as `newName` (skipping property/key positions,
324
- // which aren't references to the declaration). Edits back-to-front to keep offsets valid.
325
- function renameRefs(src, oldName, newName) {
326
- const spots = []
327
- walk(parse(src), (node, parent, key) => {
328
- if (node.type !== 'Identifier' || node.name !== oldName) return
329
- if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
330
- if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
331
- spots.push([node.start, node.end])
332
- })
333
- spots.sort((a, b) => b[0] - a[0])
334
- let out = src
335
- for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
336
- return out
337
- }
338
-
339
- // Every identifier USED anywhere (skips obj.prop names and non-computed object keys).
340
- // Approximate — good enough for the intent check; the real version is scope-aware.
341
- function usedIdentifiers(src) {
342
- const used = new Set()
343
- walk(parse(src), (node, parent, key) => {
344
- if (node.type !== 'Identifier') return
345
- if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
346
- if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
347
- used.add(node.name)
348
- })
349
- return used
350
- }
351
-
352
- function walk(node, visit, parent = null, key = null) {
353
- if (!node || typeof node.type !== 'string') return
354
- visit(node, parent, key)
355
- for (const k of Object.keys(node)) {
356
- if (k === 'type' || k === 'start' || k === 'end' || k === 'loc') continue
357
- const v = node[k]
358
- if (Array.isArray(v)) { for (const c of v) walk(c, visit, node, k) }
359
- else if (v && typeof v.type === 'string') walk(v, visit, node, k)
360
- }
361
- }
362
-
363
- // --- scope-aware reference analysis ---------------------------------------------
364
- // The names REFERENCED in `src` that resolve to NOTHING in the file's own scopes —
365
- // i.e. free/global names, including any use of a top-level declaration. This is the
366
- // scope-aware upgrade of usedIdentifiers: a use of `x` that resolves to a LOCAL binding
367
- // (a param, a `const x` in the same function, a catch var, a loop var…) is NOT counted,
368
- // because it isn't a reference to a top-level declaration. That lets ICR tell a deleted
369
- // top-level `helper` apart from an unrelated local `helper` that merely shares its name.
370
- //
371
- // Honest scope coverage: functions (declaration/expression/arrow), blocks, catch
372
- // clauses, for/for-in/for-of loop bindings, params with simple destructuring, imports,
373
- // var-hoisting to the function scope, and let/const/class/function block binding.
374
- // Approximations (rare, and they only ever make the dangling check MORE cautious):
375
- // default-value expressions in patterns and `with`/eval are not modeled.
376
- // Walk the AST and call emit(identifierNode) for every reference that resolves to NO
377
- // local binding (free / would resolve to module or global scope). Both the dangling-ref
378
- // check and scope-aware renaming are built on this single traversal.
379
- function walkFreeRefs(src, emit) { resolveScopes(parse(src), null, emit) }
380
-
381
- function referencedFreeNames(src) {
382
- const free = new Set()
383
- walkFreeRefs(src, (node) => free.add(node.name))
384
- return free
385
- }
386
-
387
- // Rename ONLY the free references to oldName (those that resolve to the top-level/global
388
- // binding) — never a local variable that merely shares the name. Edits back-to-front.
389
- function renameFreeRefs(src, oldName, newName) {
390
- const spots = []
391
- walkFreeRefs(src, (node) => { if (node.name === oldName) spots.push([node.start, node.end]) })
392
- spots.sort((a, b) => b[0] - a[0])
393
- let out = src
394
- for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
395
- return out
396
- }
397
-
398
- const CHILD_SKIP = new Set(['type', 'start', 'end', 'loc', 'range'])
399
- function eachChild(node, fn) {
400
- for (const k of Object.keys(node)) {
401
- if (CHILD_SKIP.has(k)) continue
402
- const v = node[k]
403
- if (Array.isArray(v)) { for (const c of v) if (c && typeof c.type === 'string') fn(c) }
404
- else if (v && typeof v.type === 'string') fn(v)
405
- }
406
- }
407
-
408
- // Names introduced by a binding target (handles identifiers + simple destructuring).
409
- function bindingNames(node, out) {
410
- if (!node) return
411
- switch (node.type) {
412
- case 'Identifier': out.add(node.name); break
413
- case 'ObjectPattern': for (const p of node.properties) bindingNames(p.type === 'RestElement' ? p.argument : p.value, out); break
414
- case 'ArrayPattern': for (const el of node.elements) if (el) bindingNames(el, out); break
415
- case 'AssignmentPattern': bindingNames(node.left, out); break
416
- case 'RestElement': bindingNames(node.argument, out); break
417
- }
418
- }
419
-
420
- // A binding target's NAMES are bindings, but its default values (`a = expr`) and computed
421
- // destructuring keys (`{ [k]: v }`) are REFERENCES that must be resolved against the scope.
422
- // Without this, a name used only in a default value would be missed by the dangling check.
423
- function resolveBindingDefaults(node, scope, emit) {
424
- if (!node) return
425
- switch (node.type) {
426
- case 'AssignmentPattern': resolveBindingDefaults(node.left, scope, emit); resolveScopes(node.right, scope, emit); break
427
- case 'ObjectPattern': for (const p of node.properties) {
428
- if (p.type === 'RestElement') { resolveBindingDefaults(p.argument, scope, emit); break }
429
- if (p.computed) resolveScopes(p.key, scope, emit)
430
- resolveBindingDefaults(p.value, scope, emit)
431
- } break
432
- case 'ArrayPattern': for (const el of node.elements) if (el) resolveBindingDefaults(el, scope, emit); break
433
- case 'RestElement': resolveBindingDefaults(node.argument, scope, emit); break
434
- // Identifier: a pure binding — nothing to resolve.
435
- }
436
- }
437
-
438
- const makeScope = (parent, fnScope) => { const s = { vars: new Set(), parent }; s.fnScope = fnScope || s; return s }
439
- const resolves = (scope, name) => { for (let s = scope; s; s = s.parent) if (s.vars.has(name)) return true; return false }
440
-
441
- // Hoist var declarations + function declarations into the function scope (deep, but not
442
- // crossing into nested functions, which own their own var scope).
443
- function hoistFunctionScope(nodes, scope) { for (const n of nodes) collectHoisted(n, scope) }
444
- function collectHoisted(node, scope) {
445
- if (!node || typeof node.type !== 'string') return
446
- if (node.type === 'FunctionDeclaration') { if (node.id) scope.fnScope.vars.add(node.id.name); return }
447
- if (/Function(Expression)?$|ArrowFunctionExpression/.test(node.type)) return // nested function: stop
448
- if (node.type === 'VariableDeclaration' && node.kind === 'var')
449
- for (const d of node.declarations) bindingNames(d.id, scope.fnScope.vars)
450
- eachChild(node, (c) => collectHoisted(c, scope))
451
- }
452
- // Bind let/const/class/function/import names declared directly in a block/program.
453
- function hoistBlock(nodes, scope) {
454
- for (const n of nodes) {
455
- if (n.type === 'VariableDeclaration' && (n.kind === 'let' || n.kind === 'const'))
456
- for (const d of n.declarations) bindingNames(d.id, scope.vars)
457
- else if (n.type === 'ClassDeclaration' && n.id) scope.vars.add(n.id.name)
458
- else if (n.type === 'FunctionDeclaration' && n.id) scope.vars.add(n.id.name)
459
- else if (n.type === 'ImportDeclaration') for (const sp of n.specifiers) scope.vars.add(sp.local.name)
460
- }
461
- }
462
-
463
- function resolveScopes(node, scope, emit) {
464
- if (!node || typeof node.type !== 'string') return
465
- switch (node.type) {
466
- case 'Program': {
467
- const s = makeScope(null, null)
468
- hoistFunctionScope(node.body, s); hoistBlock(node.body, s)
469
- for (const c of node.body) resolveScopes(c, s, emit)
470
- return
471
- }
472
- case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': {
473
- const s = makeScope(scope, null)
474
- for (const p of node.params) bindingNames(p, s.vars)
475
- for (const p of node.params) resolveBindingDefaults(p, s, emit) // default values are references
476
- if (node.id && node.type === 'FunctionExpression') s.vars.add(node.id.name) // named fn expr
477
- if (node.body.type === 'BlockStatement') {
478
- hoistFunctionScope(node.body.body, s); hoistBlock(node.body.body, s)
479
- for (const c of node.body.body) resolveScopes(c, s, emit)
480
- } else resolveScopes(node.body, s, emit) // arrow with expression body
481
- return
482
- }
483
- case 'BlockStatement': {
484
- const s = makeScope(scope, scope.fnScope); hoistBlock(node.body, s)
485
- for (const c of node.body) resolveScopes(c, s, emit)
486
- return
487
- }
488
- case 'CatchClause': {
489
- const s = makeScope(scope, scope.fnScope)
490
- if (node.param) bindingNames(node.param, s.vars)
491
- resolveScopes(node.body, s, emit)
492
- return
493
- }
494
- case 'ForStatement': {
495
- const s = makeScope(scope, scope.fnScope)
496
- if (node.init && node.init.type === 'VariableDeclaration') hoistBlock([node.init], s)
497
- for (const k of ['init', 'test', 'update', 'body']) if (node[k]) resolveScopes(node[k], s, emit)
498
- return
499
- }
500
- case 'ForInStatement': case 'ForOfStatement': {
501
- const s = makeScope(scope, scope.fnScope)
502
- if (node.left.type === 'VariableDeclaration') hoistBlock([node.left], s)
503
- else resolveScopes(node.left, s, emit)
504
- resolveScopes(node.right, s, emit); resolveScopes(node.body, s, emit)
505
- return
506
- }
507
- case 'VariableDeclarator': { resolveBindingDefaults(node.id, scope, emit); if (node.init) resolveScopes(node.init, scope, emit); return } // id is a binding (defaults are refs)
508
- case 'Identifier': { if (!resolves(scope, node.name)) emit(node); return }
509
- case 'MemberExpression': { resolveScopes(node.object, scope, emit); if (node.computed) resolveScopes(node.property, scope, emit); return }
510
- case 'Property': { if (node.computed) resolveScopes(node.key, scope, emit); resolveScopes(node.value, scope, emit); return }
511
- case 'PropertyDefinition': case 'MethodDefinition': { if (node.computed) resolveScopes(node.key, scope, emit); if (node.value) resolveScopes(node.value, scope, emit); return }
512
- case 'LabeledStatement': { resolveScopes(node.body, scope, emit); return }
513
- case 'BreakStatement': case 'ContinueStatement': return // labels aren't value references
514
- case 'ExportSpecifier': { resolveScopes(node.local, scope, emit); return }
515
- case 'ImportSpecifier': case 'ImportDefaultSpecifier': case 'ImportNamespaceSpecifier': return // binding positions
516
- default: eachChild(node, (c) => resolveScopes(c, scope, emit))
517
- }
518
- }
519
-
520
- // A stable key for a class member, so two agents editing DIFFERENT methods of the same
521
- // class merge, while editing the SAME method conflicts.
522
- function memberKey(m, i) {
523
- const kind = m.type === 'MethodDefinition' ? (m.kind === 'constructor' ? 'ctor' : 'method') : 'field'
524
- if (m.key && m.key.type === 'Identifier' && !m.computed)
525
- return kind + ':' + (m.static ? 'static.' : '') + m.key.name
526
- return 'member:' + i
527
- }
528
-
529
- // For finer-grained merging: split a single declaration into its signature text + keyed
530
- // inner units, plus how to reassemble them. Handles FUNCTIONS (body statements, keyed
531
- // like top-level decls) and CLASSES (members, keyed by method/field name). Returns null
532
- // for anything else (the engine then treats the declaration as indivisible). Tolerates
533
- // fragments that don't parse standalone (e.g. a bare `return`).
534
- const MEMBER_WRAP = 'class __ICR__{'
535
- // A class member (method) doesn't parse standalone — wrap it in a throwaway class so we
536
- // can recurse INTO a method body that both agents edited. Offsets are mapped back to src.
537
- function splitMember(src) {
538
- let ast
539
- try { ast = parse(MEMBER_WRAP + src + '}') } catch { return null }
540
- const cls = ast.body[0]
541
- if (!cls || cls.type !== 'ClassDeclaration' || !cls.body.body.length) return null
542
- const m = cls.body.body[0]
543
- if (!m || m.type !== 'MethodDefinition' || !m.value || !m.value.body || m.value.body.type !== 'BlockStatement') return null
544
- const off = MEMBER_WRAP.length, body = m.value.body
545
- const bo = body.start - off, bc = body.end - 1 - off // brace-open / brace-close in src coords
546
- const bs = bo + 1
547
- const ks = keyStatements(body.body)
548
- const units = body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start - off, s.end - off), start: (s.start - off) - bs, end: (s.end - off) - bs }))
549
- return { sig: src.slice(0, bo), open: '{', close: src.slice(bc), body: src.slice(bs, bc), units, join: '\n ', spliceable: true }
550
- }
551
-
552
- function splitUnit(src) {
553
- let ast
554
- try { ast = parse(src) } catch { return splitMember(src) } // maybe a class-member fragment
555
- if (!ast || !ast.body) return splitMember(src) // defensive: a null AST must never reach .body
556
- let n = ast.body[0]
557
- if (!n) return null
558
- // Unwrap export wrappers — but ALWAYS slice signatures from 0, not the inner node's
559
- // start, or the wrapper text (`export `, `export default `) is silently DROPPED on
560
- // reassembly. Found in the wild: ICR merging its own source lost `export function`.
561
- if ((n.type === 'ExportNamedDeclaration' || n.type === 'ExportDefaultDeclaration') && n.declaration) n = n.declaration
562
- if (n.type === 'FunctionDeclaration' && n.body) {
563
- const bs = n.body.start + 1, be = n.body.end - 1 // between the braces
564
- const ks = keyStatements(n.body.body)
565
- const units = n.body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start, s.end), start: s.start - bs, end: s.end - bs }))
566
- return { sig: src.slice(0, n.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n ', spliceable: true }
567
- }
568
- if (n.type === 'ClassDeclaration' && n.body) {
569
- const bs = n.body.start + 1, be = n.body.end - 1
570
- const units = n.body.body.map((m, i) => ({ key: memberKey(m, i), text: src.slice(m.start, m.end), start: m.start - bs, end: m.end - bs }))
571
- return { sig: src.slice(0, n.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n\n ', spliceable: true }
572
- }
573
- // `const x = { ... }` / `const x = () => { ... }` / `const x = function () { ... }`
574
- // and the CommonJS twins `module.exports = function () { ... }` / `x.y = { ... }` —
575
- // without descending into assignments, two sides editing DIFFERENT lines of one
576
- // exported function read as a same-declaration conflict (census overcount on axios).
577
- let init = null
578
- if (n.type === 'VariableDeclaration' && n.declarations.length === 1 && n.declarations[0].init) init = n.declarations[0].init
579
- else if (n.type === 'ExpressionStatement' && n.expression.type === 'AssignmentExpression') init = n.expression.right
580
- if (init) {
581
- if (init.type === 'ObjectExpression') {
582
- const sig = src.slice(0, init.start)
583
- const units = init.properties.map((p, i) => ({ key: propKey(p, i), text: src.slice(p.start, p.end) }))
584
- return { sig, units, open: '{\n ', join: ',\n ', close: '\n}' }
585
- }
586
- if ((init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') && init.body && init.body.type === 'BlockStatement') {
587
- const bs = init.body.start + 1, be = init.body.end - 1
588
- const ks = keyStatements(init.body.body)
589
- const units = init.body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start, s.end), start: s.start - bs, end: s.end - bs }))
590
- return { sig: src.slice(0, init.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n ', spliceable: true }
591
- }
592
- }
593
- return null
594
- }
595
-
596
- // Stable key for an object-literal property, so two agents adding DIFFERENT keys union.
597
- function propKey(p, i) {
598
- if (p.type === 'SpreadElement') return 'spread:' + i
599
- if (p.key && p.key.type === 'Identifier' && !p.computed) return 'prop:' + p.key.name
600
- if (p.key && p.key.type === 'Literal') return 'prop:' + String(p.key.value)
601
- return 'prop:' + i
602
- }
603
-
604
- // Does `src` parse either as top-level code OR as a class member? (The inner-merge result
605
- // for a method is a fragment that's only valid inside a class — this lets us validate it.)
606
- function parsesUnit(src) {
607
- if (parses(src)) return true
608
- try { parse(MEMBER_WRAP + src + '}'); return true } catch { return false }
609
- }
610
-
611
- // TOKEN STREAM: lex `src` into a flat list of { start, end, k } where k is the token's
612
- // source text -- the atom the engine's token-level inner merge diffs on. This is what lets
613
- // ICR merge two edits INSIDE one statement (e.g. `foo(1, 2)` -- one side edits `1`, the
614
- // other `2`) the way a tree-sitter merge does, instead of conflicting on the whole line.
615
- // Lexing (not parsing) so it also works on fragments (`return x`, a class-member body).
616
- // Comments are NOT tokens -- they live in the gaps between tokens and are carried verbatim
617
- // by the base text, so a comment-only edit is a no-op here (the documented comment limit).
618
- // Returns null if the fragment can't be lexed -> the engine treats it as a conflict.
619
- function tokenize(src) {
620
- const out = []
621
- try {
622
- const tz = acorn.tokenizer(src, { ecmaVersion: 'latest' })
623
- for (;;) {
624
- const t = tz.getToken()
625
- if (t.type === acorn.tokTypes.eof) break
626
- out.push({ start: t.start, end: t.end, k: src.slice(t.start, t.end) })
627
- }
628
- } catch { return null }
629
- return out
630
- }
631
-
632
- // The provider contract every ICR language module implements.
633
- export const javascript = {
634
- id: 'javascript',
635
- exts: ['.js', '.mjs', '.cjs'],
636
- parses,
637
- units,
638
- declaredNames,
639
- usedIdentifiers, // approximate (kept for reference)
640
- referencedFreeNames, // scope-aware — what the engine prefers for the dangling check
641
- declBody,
642
- isAsyncDecl,
643
- upgradeCallSitesToAwait,
644
- syncCallSitesOf,
645
- renameRefs, // rewrites every matching identifier (kept for reference)
646
- renameFreeRefs, // scope-aware — only rewrites references that resolve to the binding
647
- splitUnit, // finer granularity: split a function/class/method into keyed inner units
648
- parsesUnit, // validity oracle that also accepts a class-member fragment
649
- mergeUnit, // language-specific same-key merge (import specifier union)
650
- tokenize, // finest granularity: token stream for the token-level inner merge
651
- }
1
+ // lang-js.js — the JavaScript language provider for ICR.
2
+ //
3
+ // ICR's merge engine (icr.js) is language-agnostic: it merges a list of keyed "units"
4
+ // and asks intent questions ("what names are declared / used / what's a function body").
5
+ // Everything that actually knows about a *language* — how to parse it, what a top-level
6
+ // declaration is, how to find references — lives behind a provider like this one.
7
+ //
8
+ // To add a language (Python, Go, Rust…), write another module exposing the same shape
9
+ // and register it with icr.js's registerLanguage(). This JS provider is just the first
10
+ // plugin; it happens to use `acorn` because acorn is a tiny pure-JS parser. A tree-sitter
11
+ // provider would implement the identical interface over tree-sitter grammars.
12
+
13
+ import * as acorn from 'acorn'
14
+
15
+ // Parse permissively: try module syntax, fall back to script (so plain snippets and
16
+ // ESM both work). Throws only when the code genuinely won't parse either way.
17
+ function parseRaw(src) {
18
+ try { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'module' }) }
19
+ catch { return acorn.parse(src, { ecmaVersion: 'latest', sourceType: 'script' }) }
20
+ }
21
+
22
+ // Tiny parse cache. One structuralMerge re-parses the SAME three texts (and the merged
23
+ // result) many times across its tiers — gate, units, declaredNames, scope analysis,
24
+ // final validation. Caching by exact text collapses that to one parse per distinct
25
+ // version (a 1MB-file merge dropped ~7.6s → ~3s). Small and bounded: 8 entries, LRU.
26
+ // String comparison is a cheap memcmp; ASTs are treated as immutable by all callers.
27
+ const PARSE_CACHE = new Map() // src -> ast (Map preserves insertion order → LRU)
28
+ function parse(src) {
29
+ const hit = PARSE_CACHE.get(src)
30
+ if (hit) { PARSE_CACHE.delete(src); PARSE_CACHE.set(src, hit); return hit } // refresh LRU position
31
+ const ast = parseRaw(src)
32
+ PARSE_CACHE.set(src, ast)
33
+ if (PARSE_CACHE.size > 8) PARSE_CACHE.delete(PARSE_CACHE.keys().next().value)
34
+ return ast
35
+ }
36
+
37
+ function parses(src) { try { parse(src); return true } catch { return false } }
38
+
39
+ // A stable key per top-level node — what lets us recognize "the same declaration"
40
+ // across two edits. Names where we have them; position as a last resort.
41
+ // Dotted source path of a simple expression target (Axios.prototype.request,
42
+ // utils.forEach, this.config) — or null when it isn't a plain dotted path.
43
+ function exprPath(node) {
44
+ if (!node) return null
45
+ if (node.type === 'Identifier') return node.name
46
+ if (node.type === 'ThisExpression') return 'this'
47
+ if (node.type === 'MemberExpression' && !node.computed) {
48
+ const o = exprPath(node.object)
49
+ return o && node.property && node.property.name ? o + '.' + node.property.name : null
50
+ }
51
+ return null
52
+ }
53
+
54
+ // CONTENT-ANCHORED key for a statement, or null for truly anonymous ones.
55
+ // Index keys (stmt:N) are a last resort ONLY: an insertion above an index-keyed
56
+ // statement shifts every index, so unrelated statements pair up as fake
57
+ // same-unit conflicts — and deletions mispair so deleted code can resurrect.
58
+ // (Both failure modes were found by the merge census replaying axios/express
59
+ // history.) Assignments key by their target, calls by callee path plus a
60
+ // leading string literal (`it('name')`, `describe('name')`), directives by text.
61
+ function baseKeyOf(node) {
62
+ let n = node
63
+ if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
64
+ if (n.type === 'ExportDefaultDeclaration') return 'export:default'
65
+ if (n.type === 'FunctionDeclaration' && n.id) return 'fn:' + n.id.name
66
+ if (n.type === 'ClassDeclaration' && n.id) return 'class:' + n.id.name
67
+ if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name)
68
+ return 'var:' + n.declarations[0].id.name
69
+ // Key imports by their SOURCE MODULE, so two agents adding imports from DIFFERENT
70
+ // modules never collide, and two touching the SAME module are merged (specifier union).
71
+ if (n.type === 'ImportDeclaration') return 'import:' + (n.source && n.source.value != null ? n.source.value : '?')
72
+ // Specifier-only exports (`export { a, b }`, `export { x } from './m'`, `export * from …`)
73
+ // carry no declaration, so without a stable key they fall to positional stmt:N — and an
74
+ // edit to the export list then mispairs, DUPLICATES the statement, and knocks the whole
75
+ // merge to the line tier (which silently skips the intent/dangling checks). A stable key
76
+ // pairs them so an unedited side is kept and an edited side wins cleanly.
77
+ if (n.type === 'ExportNamedDeclaration') return n.source && n.source.value != null ? 'export-from:' + n.source.value : 'export-names'
78
+ if (n.type === 'ExportAllDeclaration') return 'export-all:' + (n.source && n.source.value != null ? n.source.value : '?')
79
+ if (n.type === 'ExpressionStatement') {
80
+ const e = n.expression
81
+ if (e.type === 'Literal' && typeof e.value === 'string') return 'directive:' + e.value // 'use strict'
82
+ if (e.type === 'AssignmentExpression') { const t = exprPath(e.left); if (t) return 'assign:' + t }
83
+ if (e.type === 'CallExpression') {
84
+ const t = exprPath(e.callee)
85
+ if (t) {
86
+ const a0 = e.arguments && e.arguments[0]
87
+ return 'call:' + t + (a0 && a0.type === 'Literal' && typeof a0.value === 'string' ? ':' + a0.value : '')
88
+ }
89
+ }
90
+ }
91
+ return null
92
+ }
93
+
94
+ // Key an ordered statement list: content-anchored where possible (duplicates get
95
+ // #1, #2… by occurrence), positional stmt:N only for the anonymous remainder.
96
+ function keyStatements(nodes) {
97
+ const seen = new Map()
98
+ return nodes.map((n, i) => {
99
+ const k = baseKeyOf(n)
100
+ if (k == null) return 'stmt:' + i
101
+ const c = seen.get(k) || 0
102
+ seen.set(k, c + 1)
103
+ return c ? k + '#' + c : k
104
+ })
105
+ }
106
+
107
+ function keyOf(node, i) { const k = baseKeyOf(node); return k == null ? 'stmt:' + i : k }
108
+
109
+ // Top-level units: ordered { key, text, start, end } for each declaration/statement.
110
+ // start/end are byte offsets in `src` — used by the format-preserving splice so unchanged
111
+ // regions (and the whitespace/comments between units) survive a merge verbatim.
112
+ function units(src) {
113
+ const ast = parse(src)
114
+ const keys = keyStatements(ast.body)
115
+ return ast.body.map((node, i) => ({ key: keys[i], text: src.slice(node.start, node.end), start: node.start, end: node.end }))
116
+ }
117
+
118
+ // Bare declared name (foo, Bar, VERSION) of a node, or null for anonymous statements.
119
+ function bareName(node) {
120
+ let n = node
121
+ if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
122
+ if (n.type === 'FunctionDeclaration' && n.id) return n.id.name
123
+ if (n.type === 'ClassDeclaration' && n.id) return n.id.name
124
+ if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].id && n.declarations[0].id.name) return n.declarations[0].id.name
125
+ return null
126
+ }
127
+
128
+ function declaredNames(src) {
129
+ const s = new Set()
130
+ for (const node of parse(src).body) {
131
+ let n = node
132
+ if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
133
+ if (n.type === 'VariableDeclaration') {
134
+ for (const d of n.declarations) bindingNames(d.id, s)
135
+ } else {
136
+ const nm = bareName(node); if (nm) s.add(nm)
137
+ }
138
+ // Import locals count as declared names too — so if an import is ever dropped and its
139
+ // binding is still used, the dangling-reference check catches it (safety net for the
140
+ // import merge). Keeps the never-emit-broken-code guarantee honest for imports.
141
+ if (node.type === 'ImportDeclaration') for (const sp of node.specifiers) s.add(sp.local.name)
142
+ if (node.type === 'ExportNamedDeclaration' && !node.declaration && node.specifiers) {
143
+ for (const sp of node.specifiers) s.add(sp.exported.name || sp.exported.value || sp.local.name)
144
+ }
145
+ }
146
+ return s
147
+ }
148
+
149
+ // --- import-aware merging -------------------------------------------------------
150
+ // Parse a single import statement into its parts, or null if it isn't one.
151
+ function parseImport(src) {
152
+ let n
153
+ try { n = parse(src).body[0] } catch { return null }
154
+ if (!n || n.type !== 'ImportDeclaration') return null
155
+ let def = null, ns = null
156
+ const named = []
157
+ for (const sp of n.specifiers) {
158
+ if (sp.type === 'ImportDefaultSpecifier') def = sp.local.name
159
+ else if (sp.type === 'ImportNamespaceSpecifier') ns = sp.local.name
160
+ else named.push({ imported: sp.imported.name != null ? sp.imported.name : sp.imported.value, local: sp.local.name })
161
+ }
162
+ return { source: n.source.value, def, ns, named }
163
+ }
164
+ function renderImport({ source, def, ns, named }) {
165
+ const parts = []
166
+ if (def) parts.push(def)
167
+ if (ns) parts.push('* as ' + ns)
168
+ if (named.length) parts.push('{ ' + named.map((s) => s.imported === s.local ? s.local : s.imported + ' as ' + s.local).join(', ') + ' }')
169
+ if (!parts.length) return "import '" + source + "'"
170
+ return 'import ' + parts.join(', ') + " from '" + source + "'"
171
+ }
172
+ // Both sides changed an import from the SAME module → union their specifiers into one
173
+ // statement (the common "both agents added an import" case). Returns null — meaning
174
+ // "let the normal conflict path handle it" — for anything we can't safely combine
175
+ // (different modules, clashing defaults/namespaces, namespace-mixed-with-named).
176
+ function mergeUnit(baseText, aText, bText) {
177
+ const a = parseImport(aText), b = parseImport(bText)
178
+ if (!a || !b || a.source !== b.source) return null
179
+ let def
180
+ if (a.def && b.def) { if (a.def !== b.def) return null; def = a.def } else def = a.def || b.def
181
+ let ns
182
+ if (a.ns && b.ns) { if (a.ns !== b.ns) return null; ns = a.ns } else ns = a.ns || b.ns
183
+ if (ns && (a.named.length || b.named.length)) return null // `* as X` can't share a statement with named imports
184
+ const seen = new Set(), named = []
185
+ for (const s of [...a.named, ...b.named]) { const k = s.imported + '|' + s.local; if (!seen.has(k)) { seen.add(k); named.push(s) } }
186
+ // Canonical specifier order: the union must not depend on which side is `a` — the merge
187
+ // has to be SYMMETRIC (merge(a,b) === merge(b,a)) or two live peers never settle on one
188
+ // byte-form. (Found by the property fuzzer: a 1-in-40k NOT-SYMMETRIC violation.)
189
+ named.sort((x, y) => (x.imported + '|' + x.local).localeCompare(y.imported + '|' + y.local))
190
+ return renderImport({ source: a.source, def, ns, named })
191
+ }
192
+
193
+ // The BODY of a top-level declaration, by name, with the name itself excluded — so two
194
+ // declarations that differ only in their name compare equal (that's how we spot a rename).
195
+ function declBody(src, name) {
196
+ for (const node of parse(src).body) {
197
+ if (bareName(node) !== name) continue
198
+ let n = node
199
+ if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
200
+ if ((n.type === 'FunctionDeclaration' || n.type === 'ClassDeclaration') && n.body)
201
+ return src.slice(n.body.start, n.body.end)
202
+ if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].init) {
203
+ const init = n.declarations[0].init
204
+ return src.slice(init.start, init.end)
205
+ }
206
+ return null
207
+ }
208
+ return null
209
+ }
210
+
211
+ // Check if top-level declaration `name` is declared `async`
212
+ function isAsyncDecl(src, name) {
213
+ try {
214
+ for (const node of parse(src).body) {
215
+ if (bareName(node) !== name) continue
216
+ let n = node
217
+ if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
218
+ if (n.type === 'ExportDefaultDeclaration' && n.declaration) n = n.declaration
219
+ if (n.type === 'FunctionDeclaration') return !!n.async
220
+ if (n.type === 'VariableDeclaration' && n.declarations[0] && n.declarations[0].init) {
221
+ const init = n.declarations[0].init
222
+ if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
223
+ return !!init.async
224
+ }
225
+ }
226
+ }
227
+ } catch {}
228
+ return false
229
+ }
230
+
231
+ // Upgrade free call sites of `fnName` to `await fnName(...)` if they are inside an async parent
232
+ // and not already awaited.
233
+ //
234
+ // Left alone (0.14.3 P0-5): a call that is already awaited (`await f()`, `await (f())`); a call that
235
+ // IS the value an async function returns (`return f()`, `async () => f()`) -- OUTSIDE a try, where
236
+ // returning the promise resolves to the same value. Inside a try (its block or its catch) `return f()`
237
+ // is NOT the same once f is async: a rejection escapes the catch and a finally runs before f settles,
238
+ // so there it is upgraded, which is what the feature is for (Claude-cloud's review). A call that is an
239
+ // argument of an awaited call (`await g(f())`) is NOT awaited by that await and is upgraded too.
240
+ // Each position is inserted at most once.
241
+ function upgradeCallSitesToAwait(src, fnName) {
242
+ try {
243
+ const ast = parse(src)
244
+ const spots = new Set()
245
+
246
+ // awaited: this node IS the operand of an await (through parentheses or an optional chain only).
247
+ // inTry: this node sits in a try block or catch clause of the current function.
248
+ function walkAST(node, parent, isAsyncScope = false, awaited = false, inTry = false) {
249
+ if (!node || typeof node.type !== 'string') return
250
+
251
+ let childAsyncScope = isAsyncScope
252
+ let childInTry = inTry
253
+ if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
254
+ childAsyncScope = !!node.async
255
+ childInTry = false // a nested function has its own try scopes
256
+ }
257
+ const passAwait = node.type === 'AwaitExpression' ? true
258
+ : (node.type === 'ParenthesizedExpression' || node.type === 'ChainExpression') ? awaited : false
259
+
260
+ if (node.type === 'CallExpression') {
261
+ let isTarget = false
262
+ if (node.callee.type === 'Identifier' && node.callee.name === fnName) isTarget = true
263
+ if (isTarget && childAsyncScope) {
264
+ const alreadyAwaited = awaited || (parent && parent.type === 'AwaitExpression')
265
+ const returnedAsIs = !inTry && parent && (parent.type === 'ReturnStatement' ||
266
+ (parent.type === 'ArrowFunctionExpression' && parent.body === node))
267
+ if (!alreadyAwaited && !returnedAsIs) spots.add(node.start)
268
+ }
269
+ }
270
+
271
+ for (const k of Object.keys(node)) {
272
+ if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
273
+ const v = node[k]
274
+ // A try's block and its catch clause are "in try"; its finally is not (a return there is final).
275
+ const kInTry = node.type === 'TryStatement' ? (k === 'block' || k === 'handler') || childInTry : childInTry
276
+ if (Array.isArray(v)) {
277
+ for (const c of v) walkAST(c, node, childAsyncScope, passAwait, kInTry)
278
+ } else if (v && typeof v.type === 'string') {
279
+ walkAST(v, node, childAsyncScope, passAwait, kInTry)
280
+ }
281
+ }
282
+ }
283
+
284
+ walkAST(ast, null, false)
285
+ if (!spots.size) return src
286
+
287
+ const order = [...spots].sort((a, b) => b - a)
288
+ spots.clear()
289
+ for (const p of order) spots.add(p)
290
+ let out = src
291
+ for (const pos of spots) {
292
+ out = out.slice(0, pos) + 'await ' + out.slice(pos)
293
+ }
294
+ return out
295
+ } catch {
296
+ return src
297
+ }
298
+ }
299
+
300
+ // STACKED AWAITS LEFT BY A CRDT UNION (0.14.3 P0-5-3). Two peers making the same sync -> async
301
+ // change at once each insert `await ` before the same call; the shared text keeps BOTH inserts, so
302
+ // the room reads `await await f()`. No merge can prevent that (each peer's merge is right on its own),
303
+ // so the engine removes it afterwards. Returns the [start, end) spans to delete: for every
304
+ // `await await x`, the OUTER `await` and the whitespace after it, and only when nothing but whitespace
305
+ // separates the two (so `await (await x)`, a comment, or a string is never touched). On the same
306
+ // converged text every peer computes the same spans, so their deletes are of the same characters.
307
+ function stackedAwaits(src) {
308
+ let ast
309
+ try { ast = parse(src) } catch { return [] }
310
+ const spans = []
311
+ const walk = (node) => {
312
+ if (!node || typeof node.type !== 'string') return
313
+ if (node.type === 'AwaitExpression' && node.argument && node.argument.type === 'AwaitExpression' &&
314
+ /^await\s+$/.test(src.slice(node.start, node.argument.start))) spans.push([node.start, node.argument.start])
315
+ for (const k of Object.keys(node)) {
316
+ if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
317
+ const v = node[k]
318
+ if (Array.isArray(v)) { for (const c of v) walk(c) } else if (v && typeof v.type === 'string') walk(v)
319
+ }
320
+ }
321
+ walk(ast)
322
+ return spans.sort((a, b) => a[0] - b[0])
323
+ }
324
+
325
+ // Find callers of `fnName` that are synchronous (cannot be auto-awaited without changing the caller's signature)
326
+ function syncCallSitesOf(src, fnName) {
327
+ const syncCallers = []
328
+ try {
329
+ const ast = parse(src)
330
+ function walkAST(node, currentFnName = null, isAsyncScope = false) {
331
+ if (!node || typeof node.type !== 'string') return
332
+
333
+ let childAsyncScope = isAsyncScope
334
+ let fnNameContext = currentFnName
335
+ if (node.type === 'FunctionDeclaration') {
336
+ childAsyncScope = !!node.async
337
+ fnNameContext = node.id ? node.id.name : '<anonymous>'
338
+ } else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
339
+ childAsyncScope = !!node.async
340
+ if (node.id) fnNameContext = node.id.name
341
+ }
342
+
343
+ if (node.type === 'CallExpression') {
344
+ if (node.callee.type === 'Identifier' && node.callee.name === fnName) {
345
+ if (!childAsyncScope) {
346
+ syncCallers.push(fnNameContext || '<top-level>')
347
+ }
348
+ }
349
+ }
350
+
351
+ for (const k of Object.keys(node)) {
352
+ if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
353
+ const v = node[k]
354
+ if (Array.isArray(v)) {
355
+ for (const c of v) walkAST(c, fnNameContext, childAsyncScope)
356
+ } else if (v && typeof v.type === 'string') {
357
+ walkAST(v, fnNameContext, childAsyncScope)
358
+ }
359
+ }
360
+ }
361
+ walkAST(ast, null, false)
362
+ } catch {}
363
+ return syncCallers
364
+ }
365
+
366
+ // Rewrite every reference to `oldName` as `newName` (skipping property/key positions,
367
+ // which aren't references to the declaration). Edits back-to-front to keep offsets valid.
368
+ function renameRefs(src, oldName, newName) {
369
+ const spots = []
370
+ walk(parse(src), (node, parent, key) => {
371
+ if (node.type !== 'Identifier' || node.name !== oldName) return
372
+ if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
373
+ if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
374
+ spots.push([node.start, node.end])
375
+ })
376
+ spots.sort((a, b) => b[0] - a[0])
377
+ let out = src
378
+ for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
379
+ return out
380
+ }
381
+
382
+ // Every identifier USED anywhere (skips obj.prop names and non-computed object keys).
383
+ // Approximate — good enough for the intent check; the real version is scope-aware.
384
+ function usedIdentifiers(src) {
385
+ const used = new Set()
386
+ walk(parse(src), (node, parent, key) => {
387
+ if (node.type !== 'Identifier') return
388
+ if (parent && parent.type === 'MemberExpression' && key === 'property' && !parent.computed) return
389
+ if (parent && (parent.type === 'Property' || parent.type === 'PropertyDefinition') && key === 'key' && !parent.computed) return
390
+ used.add(node.name)
391
+ })
392
+ return used
393
+ }
394
+
395
+ function walk(node, visit, parent = null, key = null) {
396
+ if (!node || typeof node.type !== 'string') return
397
+ visit(node, parent, key)
398
+ for (const k of Object.keys(node)) {
399
+ if (k === 'type' || k === 'start' || k === 'end' || k === 'loc') continue
400
+ const v = node[k]
401
+ if (Array.isArray(v)) { for (const c of v) walk(c, visit, node, k) }
402
+ else if (v && typeof v.type === 'string') walk(v, visit, node, k)
403
+ }
404
+ }
405
+
406
+ // --- scope-aware reference analysis ---------------------------------------------
407
+ // The names REFERENCED in `src` that resolve to NOTHING in the file's own scopes —
408
+ // i.e. free/global names, including any use of a top-level declaration. This is the
409
+ // scope-aware upgrade of usedIdentifiers: a use of `x` that resolves to a LOCAL binding
410
+ // (a param, a `const x` in the same function, a catch var, a loop var…) is NOT counted,
411
+ // because it isn't a reference to a top-level declaration. That lets ICR tell a deleted
412
+ // top-level `helper` apart from an unrelated local `helper` that merely shares its name.
413
+ //
414
+ // Honest scope coverage: functions (declaration/expression/arrow), blocks, catch
415
+ // clauses, for/for-in/for-of loop bindings, params with simple destructuring, imports,
416
+ // var-hoisting to the function scope, and let/const/class/function block binding.
417
+ // Approximations (rare, and they only ever make the dangling check MORE cautious):
418
+ // default-value expressions in patterns and `with`/eval are not modeled.
419
+ // Walk the AST and call emit(identifierNode) for every reference that resolves to NO
420
+ // local binding (free / would resolve to module or global scope). Both the dangling-ref
421
+ // check and scope-aware renaming are built on this single traversal.
422
+ function walkFreeRefs(src, emit) { resolveScopes(parse(src), null, emit) }
423
+
424
+ function referencedFreeNames(src) {
425
+ const free = new Set()
426
+ walkFreeRefs(src, (node) => free.add(node.name))
427
+ return free
428
+ }
429
+
430
+ // Rename ONLY the free references to oldName (those that resolve to the top-level/global
431
+ // binding) — never a local variable that merely shares the name. Edits back-to-front.
432
+ function renameFreeRefs(src, oldName, newName) {
433
+ const spots = []
434
+ walkFreeRefs(src, (node) => { if (node.name === oldName) spots.push([node.start, node.end]) })
435
+ spots.sort((a, b) => b[0] - a[0])
436
+ let out = src
437
+ for (const [s, e] of spots) out = out.slice(0, s) + newName + out.slice(e)
438
+ return out
439
+ }
440
+
441
+ const CHILD_SKIP = new Set(['type', 'start', 'end', 'loc', 'range'])
442
+ function eachChild(node, fn) {
443
+ for (const k of Object.keys(node)) {
444
+ if (CHILD_SKIP.has(k)) continue
445
+ const v = node[k]
446
+ if (Array.isArray(v)) { for (const c of v) if (c && typeof c.type === 'string') fn(c) }
447
+ else if (v && typeof v.type === 'string') fn(v)
448
+ }
449
+ }
450
+
451
+ // Names introduced by a binding target (handles identifiers + simple destructuring).
452
+ function bindingNames(node, out) {
453
+ if (!node) return
454
+ switch (node.type) {
455
+ case 'Identifier': out.add(node.name); break
456
+ case 'ObjectPattern': for (const p of node.properties) bindingNames(p.type === 'RestElement' ? p.argument : p.value, out); break
457
+ case 'ArrayPattern': for (const el of node.elements) if (el) bindingNames(el, out); break
458
+ case 'AssignmentPattern': bindingNames(node.left, out); break
459
+ case 'RestElement': bindingNames(node.argument, out); break
460
+ }
461
+ }
462
+
463
+ // A binding target's NAMES are bindings, but its default values (`a = expr`) and computed
464
+ // destructuring keys (`{ [k]: v }`) are REFERENCES that must be resolved against the scope.
465
+ // Without this, a name used only in a default value would be missed by the dangling check.
466
+ function resolveBindingDefaults(node, scope, emit) {
467
+ if (!node) return
468
+ switch (node.type) {
469
+ case 'AssignmentPattern': resolveBindingDefaults(node.left, scope, emit); resolveScopes(node.right, scope, emit); break
470
+ case 'ObjectPattern': for (const p of node.properties) {
471
+ if (p.type === 'RestElement') { resolveBindingDefaults(p.argument, scope, emit); break }
472
+ if (p.computed) resolveScopes(p.key, scope, emit)
473
+ resolveBindingDefaults(p.value, scope, emit)
474
+ } break
475
+ case 'ArrayPattern': for (const el of node.elements) if (el) resolveBindingDefaults(el, scope, emit); break
476
+ case 'RestElement': resolveBindingDefaults(node.argument, scope, emit); break
477
+ // Identifier: a pure binding — nothing to resolve.
478
+ }
479
+ }
480
+
481
+ const makeScope = (parent, fnScope) => { const s = { vars: new Set(), parent }; s.fnScope = fnScope || s; return s }
482
+ const resolves = (scope, name) => { for (let s = scope; s; s = s.parent) if (s.vars.has(name)) return true; return false }
483
+
484
+ // Hoist var declarations + function declarations into the function scope (deep, but not
485
+ // crossing into nested functions, which own their own var scope).
486
+ function hoistFunctionScope(nodes, scope) { for (const n of nodes) collectHoisted(n, scope) }
487
+ function collectHoisted(node, scope) {
488
+ if (!node || typeof node.type !== 'string') return
489
+ if (node.type === 'FunctionDeclaration') { if (node.id) scope.fnScope.vars.add(node.id.name); return }
490
+ if (/Function(Expression)?$|ArrowFunctionExpression/.test(node.type)) return // nested function: stop
491
+ if (node.type === 'VariableDeclaration' && node.kind === 'var')
492
+ for (const d of node.declarations) bindingNames(d.id, scope.fnScope.vars)
493
+ eachChild(node, (c) => collectHoisted(c, scope))
494
+ }
495
+ // Bind let/const/class/function/import names declared directly in a block/program.
496
+ function hoistBlock(nodes, scope) {
497
+ for (const n of nodes) {
498
+ if (n.type === 'VariableDeclaration' && (n.kind === 'let' || n.kind === 'const'))
499
+ for (const d of n.declarations) bindingNames(d.id, scope.vars)
500
+ else if (n.type === 'ClassDeclaration' && n.id) scope.vars.add(n.id.name)
501
+ else if (n.type === 'FunctionDeclaration' && n.id) scope.vars.add(n.id.name)
502
+ else if (n.type === 'ImportDeclaration') for (const sp of n.specifiers) scope.vars.add(sp.local.name)
503
+ }
504
+ }
505
+
506
+ function resolveScopes(node, scope, emit) {
507
+ if (!node || typeof node.type !== 'string') return
508
+ switch (node.type) {
509
+ case 'Program': {
510
+ const s = makeScope(null, null)
511
+ hoistFunctionScope(node.body, s); hoistBlock(node.body, s)
512
+ for (const c of node.body) resolveScopes(c, s, emit)
513
+ return
514
+ }
515
+ case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': {
516
+ const s = makeScope(scope, null)
517
+ for (const p of node.params) bindingNames(p, s.vars)
518
+ for (const p of node.params) resolveBindingDefaults(p, s, emit) // default values are references
519
+ if (node.id && node.type === 'FunctionExpression') s.vars.add(node.id.name) // named fn expr
520
+ if (node.body.type === 'BlockStatement') {
521
+ hoistFunctionScope(node.body.body, s); hoistBlock(node.body.body, s)
522
+ for (const c of node.body.body) resolveScopes(c, s, emit)
523
+ } else resolveScopes(node.body, s, emit) // arrow with expression body
524
+ return
525
+ }
526
+ case 'BlockStatement': {
527
+ const s = makeScope(scope, scope.fnScope); hoistBlock(node.body, s)
528
+ for (const c of node.body) resolveScopes(c, s, emit)
529
+ return
530
+ }
531
+ case 'CatchClause': {
532
+ const s = makeScope(scope, scope.fnScope)
533
+ if (node.param) bindingNames(node.param, s.vars)
534
+ resolveScopes(node.body, s, emit)
535
+ return
536
+ }
537
+ case 'ForStatement': {
538
+ const s = makeScope(scope, scope.fnScope)
539
+ if (node.init && node.init.type === 'VariableDeclaration') hoistBlock([node.init], s)
540
+ for (const k of ['init', 'test', 'update', 'body']) if (node[k]) resolveScopes(node[k], s, emit)
541
+ return
542
+ }
543
+ case 'ForInStatement': case 'ForOfStatement': {
544
+ const s = makeScope(scope, scope.fnScope)
545
+ if (node.left.type === 'VariableDeclaration') hoistBlock([node.left], s)
546
+ else resolveScopes(node.left, s, emit)
547
+ resolveScopes(node.right, s, emit); resolveScopes(node.body, s, emit)
548
+ return
549
+ }
550
+ case 'VariableDeclarator': { resolveBindingDefaults(node.id, scope, emit); if (node.init) resolveScopes(node.init, scope, emit); return } // id is a binding (defaults are refs)
551
+ case 'Identifier': { if (!resolves(scope, node.name)) emit(node); return }
552
+ case 'MemberExpression': { resolveScopes(node.object, scope, emit); if (node.computed) resolveScopes(node.property, scope, emit); return }
553
+ case 'Property': { if (node.computed) resolveScopes(node.key, scope, emit); resolveScopes(node.value, scope, emit); return }
554
+ case 'PropertyDefinition': case 'MethodDefinition': { if (node.computed) resolveScopes(node.key, scope, emit); if (node.value) resolveScopes(node.value, scope, emit); return }
555
+ case 'LabeledStatement': { resolveScopes(node.body, scope, emit); return }
556
+ case 'BreakStatement': case 'ContinueStatement': return // labels aren't value references
557
+ case 'ExportSpecifier': { resolveScopes(node.local, scope, emit); return }
558
+ case 'ImportSpecifier': case 'ImportDefaultSpecifier': case 'ImportNamespaceSpecifier': return // binding positions
559
+ default: eachChild(node, (c) => resolveScopes(c, scope, emit))
560
+ }
561
+ }
562
+
563
+ // A stable key for a class member, so two agents editing DIFFERENT methods of the same
564
+ // class merge, while editing the SAME method conflicts.
565
+ function memberKey(m, i) {
566
+ const kind = m.type === 'MethodDefinition' ? (m.kind === 'constructor' ? 'ctor' : 'method') : 'field'
567
+ if (m.key && m.key.type === 'Identifier' && !m.computed)
568
+ return kind + ':' + (m.static ? 'static.' : '') + m.key.name
569
+ return 'member:' + i
570
+ }
571
+
572
+ // For finer-grained merging: split a single declaration into its signature text + keyed
573
+ // inner units, plus how to reassemble them. Handles FUNCTIONS (body statements, keyed
574
+ // like top-level decls) and CLASSES (members, keyed by method/field name). Returns null
575
+ // for anything else (the engine then treats the declaration as indivisible). Tolerates
576
+ // fragments that don't parse standalone (e.g. a bare `return`).
577
+ const MEMBER_WRAP = 'class __ICR__{'
578
+ // A class member (method) doesn't parse standalone — wrap it in a throwaway class so we
579
+ // can recurse INTO a method body that both agents edited. Offsets are mapped back to src.
580
+ function splitMember(src) {
581
+ let ast
582
+ try { ast = parse(MEMBER_WRAP + src + '}') } catch { return null }
583
+ const cls = ast.body[0]
584
+ if (!cls || cls.type !== 'ClassDeclaration' || !cls.body.body.length) return null
585
+ const m = cls.body.body[0]
586
+ if (!m || m.type !== 'MethodDefinition' || !m.value || !m.value.body || m.value.body.type !== 'BlockStatement') return null
587
+ const off = MEMBER_WRAP.length, body = m.value.body
588
+ const bo = body.start - off, bc = body.end - 1 - off // brace-open / brace-close in src coords
589
+ const bs = bo + 1
590
+ const ks = keyStatements(body.body)
591
+ const units = body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start - off, s.end - off), start: (s.start - off) - bs, end: (s.end - off) - bs }))
592
+ return { sig: src.slice(0, bo), open: '{', close: src.slice(bc), body: src.slice(bs, bc), units, join: '\n ', spliceable: true }
593
+ }
594
+
595
+ function splitUnit(src) {
596
+ let ast
597
+ try { ast = parse(src) } catch { return splitMember(src) } // maybe a class-member fragment
598
+ if (!ast || !ast.body) return splitMember(src) // defensive: a null AST must never reach .body
599
+ let n = ast.body[0]
600
+ if (!n) return null
601
+ // Unwrap export wrappers — but ALWAYS slice signatures from 0, not the inner node's
602
+ // start, or the wrapper text (`export `, `export default `) is silently DROPPED on
603
+ // reassembly. Found in the wild: ICR merging its own source lost `export function`.
604
+ if ((n.type === 'ExportNamedDeclaration' || n.type === 'ExportDefaultDeclaration') && n.declaration) n = n.declaration
605
+ if (n.type === 'FunctionDeclaration' && n.body) {
606
+ const bs = n.body.start + 1, be = n.body.end - 1 // between the braces
607
+ const ks = keyStatements(n.body.body)
608
+ const units = n.body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start, s.end), start: s.start - bs, end: s.end - bs }))
609
+ return { sig: src.slice(0, n.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n ', spliceable: true }
610
+ }
611
+ if (n.type === 'ClassDeclaration' && n.body) {
612
+ const bs = n.body.start + 1, be = n.body.end - 1
613
+ const units = n.body.body.map((m, i) => ({ key: memberKey(m, i), text: src.slice(m.start, m.end), start: m.start - bs, end: m.end - bs }))
614
+ return { sig: src.slice(0, n.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n\n ', spliceable: true }
615
+ }
616
+ // `const x = { ... }` / `const x = () => { ... }` / `const x = function () { ... }`
617
+ // and the CommonJS twins `module.exports = function () { ... }` / `x.y = { ... }` —
618
+ // without descending into assignments, two sides editing DIFFERENT lines of one
619
+ // exported function read as a same-declaration conflict (census overcount on axios).
620
+ let init = null
621
+ if (n.type === 'VariableDeclaration' && n.declarations.length === 1 && n.declarations[0].init) init = n.declarations[0].init
622
+ else if (n.type === 'ExpressionStatement' && n.expression.type === 'AssignmentExpression') init = n.expression.right
623
+ if (init) {
624
+ if (init.type === 'ObjectExpression') {
625
+ const sig = src.slice(0, init.start)
626
+ const units = init.properties.map((p, i) => ({ key: propKey(p, i), text: src.slice(p.start, p.end) }))
627
+ return { sig, units, open: '{\n ', join: ',\n ', close: '\n}' }
628
+ }
629
+ if ((init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') && init.body && init.body.type === 'BlockStatement') {
630
+ const bs = init.body.start + 1, be = init.body.end - 1
631
+ const ks = keyStatements(init.body.body)
632
+ const units = init.body.body.map((s, i) => ({ key: ks[i], text: src.slice(s.start, s.end), start: s.start - bs, end: s.end - bs }))
633
+ return { sig: src.slice(0, init.body.start), open: '{', close: src.slice(be), body: src.slice(bs, be), units, join: '\n ', spliceable: true }
634
+ }
635
+ }
636
+ return null
637
+ }
638
+
639
+ // Stable key for an object-literal property, so two agents adding DIFFERENT keys union.
640
+ function propKey(p, i) {
641
+ if (p.type === 'SpreadElement') return 'spread:' + i
642
+ if (p.key && p.key.type === 'Identifier' && !p.computed) return 'prop:' + p.key.name
643
+ if (p.key && p.key.type === 'Literal') return 'prop:' + String(p.key.value)
644
+ return 'prop:' + i
645
+ }
646
+
647
+ // Does `src` parse either as top-level code OR as a class member? (The inner-merge result
648
+ // for a method is a fragment that's only valid inside a class — this lets us validate it.)
649
+ function parsesUnit(src) {
650
+ if (parses(src)) return true
651
+ try { parse(MEMBER_WRAP + src + '}'); return true } catch { return false }
652
+ }
653
+
654
+ // TOKEN STREAM: lex `src` into a flat list of { start, end, k } where k is the token's
655
+ // source text -- the atom the engine's token-level inner merge diffs on. This is what lets
656
+ // ICR merge two edits INSIDE one statement (e.g. `foo(1, 2)` -- one side edits `1`, the
657
+ // other `2`) the way a tree-sitter merge does, instead of conflicting on the whole line.
658
+ // Lexing (not parsing) so it also works on fragments (`return x`, a class-member body).
659
+ // Comments are NOT tokens -- they live in the gaps between tokens and are carried verbatim
660
+ // by the base text, so a comment-only edit is a no-op here (the documented comment limit).
661
+ // Returns null if the fragment can't be lexed -> the engine treats it as a conflict.
662
+ function tokenize(src) {
663
+ const out = []
664
+ try {
665
+ const tz = acorn.tokenizer(src, { ecmaVersion: 'latest' })
666
+ for (;;) {
667
+ const t = tz.getToken()
668
+ if (t.type === acorn.tokTypes.eof) break
669
+ out.push({ start: t.start, end: t.end, k: src.slice(t.start, t.end) })
670
+ }
671
+ } catch { return null }
672
+ return out
673
+ }
674
+
675
+ // The provider contract every ICR language module implements.
676
+ export const javascript = {
677
+ id: 'javascript',
678
+ exts: ['.js', '.mjs', '.cjs'],
679
+ parses,
680
+ units,
681
+ declaredNames,
682
+ usedIdentifiers, // approximate (kept for reference)
683
+ referencedFreeNames, // scope-aware — what the engine prefers for the dangling check
684
+ declBody,
685
+ isAsyncDecl,
686
+ upgradeCallSitesToAwait,
687
+ stackedAwaits, // spans of `await await` a CRDT union left (P0-5-3)
688
+ syncCallSitesOf,
689
+ renameRefs, // rewrites every matching identifier (kept for reference)
690
+ renameFreeRefs, // scope-aware — only rewrites references that resolve to the binding
691
+ splitUnit, // finer granularity: split a function/class/method into keyed inner units
692
+ parsesUnit, // validity oracle that also accepts a class-member fragment
693
+ mergeUnit, // language-specific same-key merge (import specifier union)
694
+ tokenize, // finest granularity: token stream for the token-level inner merge
695
+ }