hivecode-mcp 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lang-json.js CHANGED
@@ -82,6 +82,34 @@ function mergeArray(base, a, b) {
82
82
  return out
83
83
  }
84
84
 
85
+ // SYMMETRY. The merged object is re-serialized with JSON.stringify, which emits keys in
86
+ // INSERTION order — so the order we walk the key union IS the output byte order. The
87
+ // obvious union, `[...keys(base), ...keys(a), ...keys(b)]`, is side-ordered: ours's new
88
+ // keys always precede theirs's, so merge(base,a,b) and merge(base,b,a) produce clean
89
+ // 'auto' results that differ byte-for-byte. Measured at 43/167 auto merges (26%) by
90
+ // test/icr-data-torture-test.js. Semantically identical, but P2 SYMMETRY is asserted
91
+ // byte-wise for every other language (icr-torture-test.js), and byte-inequality is
92
+ // precisely what a CRDT peer and a git blob hash react to — the two consumers the README
93
+ // names. So the union order must not depend on which side is called `a`:
94
+ // • keys present in base keep BASE's order (unchanged behaviour, minimal diffs);
95
+ // • keys added by either side follow, ordered by where their own author put them
96
+ // (min index across the sides that have it), ties broken by key name.
97
+ // Both rules are symmetric in a/b, so swapping the sides cannot change the output.
98
+ function unionKeys(base, a, b) {
99
+ const rank = new Map()
100
+ for (const obj of [a, b]) {
101
+ const ks = Object.keys(obj)
102
+ for (let i = 0; i < ks.length; i++) {
103
+ const k = ks[i]
104
+ if (k in base) continue
105
+ const prev = rank.get(k)
106
+ if (prev === undefined || i < prev) rank.set(k, i)
107
+ }
108
+ }
109
+ const added = [...rank.keys()].sort((x, y) => (rank.get(x) - rank.get(y)) || (x < y ? -1 : x > y ? 1 : 0))
110
+ return [...Object.keys(base), ...added]
111
+ }
112
+
85
113
  // Value-wise 3-way merge. Fills `conflicts` (path strings) and `resolvable`
86
114
  // ({key, base, ours, theirs} with pretty-printed values) on same-path clashes;
87
115
  // the merged value keeps BASE at conflicted paths (the engine refuses the whole
@@ -92,8 +120,7 @@ function mergeVal(base, a, b, path, conflicts, resolvable) {
92
120
  if (eq(b, base)) return a
93
121
  if (isObj(base) && isObj(a) && isObj(b)) {
94
122
  const out = {}
95
- const keys = [...new Set([...Object.keys(base), ...Object.keys(a), ...Object.keys(b)])]
96
- for (const k of keys) {
123
+ for (const k of unionKeys(base, a, b)) {
97
124
  const p = path ? path + '.' + k : k
98
125
  const inB = k in base, inA = k in a, inBb = k in b
99
126
  if (inB && !inA && !inBb) continue // deleted on both sides
package/lang-python.js CHANGED
@@ -264,6 +264,224 @@ function isRefToken(src, t) {
264
264
  return !(p >= 0 && src[p] === '.')
265
265
  }
266
266
 
267
+ // --- scope-aware reference analysis ---------------------------------------------
268
+ //
269
+ // WHY THIS IS EXACT AND NOT ANOTHER HEURISTIC. Python binds by assignment: a name
270
+ // assigned anywhere in a function body is local to that function for the WHOLE body, and
271
+ // reading it before the assignment is an UnboundLocalError rather than a global read. So
272
+ // a function that assigns `helper` cannot also be referring to a module-level `helper` —
273
+ // the language forbids the mixed case. That is what makes subtracting local bindings safe
274
+ // here, where a plain "does this name appear anywhere" check has to stay over-cautious.
275
+ //
276
+ // WHAT IT FIXES. usedIdentifiers reports every name it sees, so deleting a top-level
277
+ // `helper` while some unrelated function happens to have a local named `helper` was
278
+ // reported as ref:helper — a merge refused that was perfectly safe, falling back to line
279
+ // merge with conflict markers. Common names (config, data, result, helper) trip this
280
+ // constantly. Reproduced in test/scope-repro.mjs before this existed.
281
+ //
282
+ // THE DIRECTION THAT MATTERS. Missing a REAL reference would let ICR delete something
283
+ // still in use — the one failure this library promises never to have — so every rule
284
+ // below fails towards "still referenced":
285
+ // • module scope subtracts nothing, so top-level rebinding never hides a reference;
286
+ // • a nested def is its own scope, never folded into its parent's bindings;
287
+ // • a class body contributes no bindings at all (its names are attributes, invisible
288
+ // to methods), so they stay free;
289
+ // • `global`/`nonlocal` remove a name from the local set;
290
+ // • subscript and attribute targets (`a[i] = `, `a.b = `) bind nothing — `a` must
291
+ // already exist, so it stays a reference;
292
+ // • anything unrecognised is left free.
293
+ const PY_KW = new Set(['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
294
+ 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for',
295
+ 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass',
296
+ 'raise', 'return', 'try', 'while', 'with', 'yield'])
297
+
298
+ const isName = (k) => /^[A-Za-z_]\w*$/.test(k) && !PY_KW.has(k)
299
+ const OPEN = new Set(['(', '[', '{'])
300
+ const CLOSE = new Set([')', ']', '}'])
301
+
302
+ // A target position binds only a bare name. `a[i] = x` and `a.b = x` require `a` to exist
303
+ // already, so they are reads, not bindings.
304
+ function bareTargets(toks, from, to, out) {
305
+ for (let i = from; i < to; i++) {
306
+ const t = toks[i]
307
+ if (!isName(t.k)) continue
308
+ const prev = toks[i - 1] && toks[i - 1].k
309
+ const next = toks[i + 1] && toks[i + 1].k
310
+ if (prev === '.' || next === '.' || next === '[' || next === '(') continue
311
+ out.add(t.k)
312
+ }
313
+ }
314
+
315
+ // Names this statement binds in its own scope, and names it explicitly disowns.
316
+ function stmtBindings(text) {
317
+ const bind = new Set(), disown = new Set()
318
+ const toks = lexTokens(text).filter((t) => t.k.trim() !== '')
319
+ const kw = toks.length ? toks[0].k : ''
320
+
321
+ if (kw === 'global' || kw === 'nonlocal') {
322
+ for (const t of toks.slice(1)) if (isName(t.k)) disown.add(t.k)
323
+ return { bind, disown }
324
+ }
325
+
326
+ if (kw === 'import' || kw === 'from') {
327
+ // `import a.b as c` binds c; `import a.b` binds a; `from m import x, y as z`.
328
+ const start = kw === 'from' ? toks.findIndex((t) => t.k === 'import') : 0
329
+ if (start >= 0) {
330
+ for (let i = start + 1; i < toks.length; i++) {
331
+ if (toks[i].k === 'as' && isName((toks[i + 1] || {}).k)) { bind.add(toks[i + 1].k); i++; continue }
332
+ // first segment of a dotted path, and only when not renamed by a later `as`
333
+ if (isName(toks[i].k) && (toks[i - 1] || {}).k !== '.' &&
334
+ (toks[i + 1] || {}).k !== '.' && (toks[i + 1] || {}).k !== 'as') bind.add(toks[i].k)
335
+ else if (isName(toks[i].k) && (toks[i + 1] || {}).k === '.' && kw === 'import') bind.add(toks[i].k)
336
+ }
337
+ }
338
+ return { bind, disown }
339
+ }
340
+
341
+ let depth = 0
342
+ for (let i = 0; i < toks.length; i++) {
343
+ const k = toks[i].k
344
+ if (OPEN.has(k)) { depth++; continue }
345
+ if (CLOSE.has(k)) { depth--; continue }
346
+
347
+ // `for <targets> in` — at any depth, so comprehensions bind too.
348
+ if (k === 'for') {
349
+ let j = i + 1
350
+ while (j < toks.length && toks[j].k !== 'in') j++
351
+ bareTargets(toks, i + 1, j, bind)
352
+ i = j
353
+ continue
354
+ }
355
+ // `with x as name`, `except E as name`
356
+ if (k === 'as' && isName((toks[i + 1] || {}).k)) { bind.add(toks[i + 1].k); i++; continue }
357
+
358
+ if (k !== '=') continue
359
+ const prev = (toks[i - 1] || {}).k
360
+ const next = (toks[i + 1] || {}).k
361
+ if (next === '=') { i++; continue } // ==
362
+ if (prev === '=' || prev === '!' || prev === '<' || prev === '>') continue
363
+ if (depth > 0) {
364
+ // Inside brackets `=` is a keyword argument, except for the walrus `:=`.
365
+ if (prev === ':' && isName((toks[i - 2] || {}).k)) bind.add(toks[i - 2].k)
366
+ continue
367
+ }
368
+ // Statement-level assignment: simple, augmented (`+=`, prev is an operator), or
369
+ // annotated (`x: int = 1`). Everything left of it is the target list.
370
+ bareTargets(toks, 0, i, bind)
371
+ break
372
+ }
373
+ return { bind, disown }
374
+ }
375
+
376
+ function refsIn(text, emit) {
377
+ for (const t of lexTokens(text)) if (isName(t.k) && isRefToken(text, t)) emit(t.k)
378
+ }
379
+
380
+ // Parameter names of a def header — used as bindings of the CHILD scope, and excluded
381
+ // from the header's own references so a parameter is never mistaken for a free name.
382
+ function defParams(header) {
383
+ const toks = lexTokens(header).filter((t) => t.k.trim() !== '')
384
+ const open = toks.findIndex((t) => t.k === '(')
385
+ if (open < 0) return new Set()
386
+ const params = new Set()
387
+ let depth = 0
388
+ for (let i = open; i < toks.length; i++) {
389
+ const k = toks[i].k
390
+ if (OPEN.has(k)) { depth++; continue }
391
+ if (CLOSE.has(k)) { depth--; if (depth === 0) break; continue }
392
+ if (depth !== 1 || !isName(k)) continue
393
+ const prev = (toks[i - 1] || {}).k
394
+ if (prev === '(' || prev === ',' || prev === '*' || prev === '**') params.add(k)
395
+ }
396
+ return params
397
+ }
398
+
399
+ // Split a def/class unit into the part evaluated in the ENCLOSING scope (decorators and
400
+ // the header line — where default values and base classes live) and the body, which is
401
+ // its own scope.
402
+ function splitDeclUnit(text) {
403
+ const L = lines(text)
404
+ let h = 0
405
+ while (h < L.length && (L[h].blank || /^\s*@/.test(L[h].text))) h++
406
+ if (h >= L.length) return null
407
+ const head = L[h]
408
+ const isDef = HEAD.test(head.text), isCls = CLS.test(head.text)
409
+ if (!isDef && !isCls) return null
410
+ // A signature can span lines while its brackets are open.
411
+ let e = h
412
+ let depth = 0
413
+ do {
414
+ for (const ch of L[e].text) { if (OPEN.has(ch)) depth++; else if (CLOSE.has(ch)) depth-- }
415
+ e++
416
+ } while (e < L.length && depth > 0)
417
+ const header = text.slice(0, L[e - 1].end)
418
+ let b = e
419
+ while (b < L.length && L[b].blank) b++
420
+ const body = b < L.length ? text.slice(L[b].start) : ''
421
+ return { isDef, header, body, bodyIndent: b < L.length ? L[b].indent : 0 }
422
+ }
423
+
424
+ // Walk one scope's statements, descending through compound bodies (same scope) but
425
+ // stopping at def/class (new scope).
426
+ function scopeParts(code, base, onStmt, onDecl) {
427
+ for (const u of blocks(code, base)) {
428
+ const text = code.slice(u.start, u.end)
429
+ if (splitDeclUnit(text)) { onDecl(text); continue }
430
+ const L = lines(text)
431
+ let b = 1
432
+ while (b < L.length && L[b].blank) b++
433
+ if (b < L.length && L[b].indent > base) {
434
+ onStmt(text.slice(0, L[b].start)) // the `if ...:` / `for ...:` head
435
+ scopeParts(text.slice(L[b].start), L[b].indent, onStmt, onDecl)
436
+ } else onStmt(text)
437
+ }
438
+ }
439
+
440
+ // `subtract` is false at module scope: a module-level rebinding must never hide a
441
+ // reference to the declaration being deleted.
442
+ function scopeFree(code, base, enclosing, free, subtract) {
443
+ const stmts = [], decls = []
444
+ scopeParts(code, base, (t) => stmts.push(t), (t) => decls.push(t))
445
+
446
+ const local = new Set(), disowned = new Set()
447
+ for (const t of stmts) {
448
+ const { bind, disown } = stmtBindings(t)
449
+ for (const n of bind) local.add(n)
450
+ for (const n of disown) disowned.add(n)
451
+ }
452
+ for (const t of decls) {
453
+ const s = splitDeclUnit(t)
454
+ const m = s && (s.header.match(HEAD) || s.header.match(CLS))
455
+ if (m) local.add(m[1])
456
+ }
457
+ for (const n of disowned) local.delete(n)
458
+
459
+ const bound = subtract ? new Set([...enclosing, ...local]) : new Set(enclosing)
460
+ for (const t of stmts) refsIn(t, (n) => { if (!bound.has(n)) free.add(n) })
461
+
462
+ for (const t of decls) {
463
+ const s = splitDeclUnit(t)
464
+ if (!s) continue
465
+ const params = s.isDef ? defParams(s.header) : new Set()
466
+ const declName = (s.header.match(HEAD) || s.header.match(CLS) || [])[1]
467
+ // Decorators, annotations and default values run in THIS scope.
468
+ refsIn(s.header, (n) => {
469
+ if (n === declName || params.has(n) || bound.has(n)) return
470
+ free.add(n)
471
+ })
472
+ if (!s.body) continue
473
+ // A class body contributes no bindings to anything nested inside it, so its methods
474
+ // are analysed against the enclosing scope only. `subtract: s.isDef` is the whole of
475
+ // that rule and it is load-bearing: with subtraction left on for a class, `helper = 9`
476
+ // at class level became a binding the methods inherited, and a real `helper()` call
477
+ // inside a method stopped being reported — a merge shipped with a dangling reference,
478
+ // caught by the safety case in test/scope-repro.mjs. Class attributes are reached as
479
+ // `self.helper` or `C.helper`, never as a bare name, so they must never shadow.
480
+ scopeFree(s.body, s.bodyIndent,
481
+ s.isDef ? new Set([...bound, ...params]) : bound, free, s.isDef)
482
+ }
483
+ }
484
+
267
485
  // PER-STATEMENT bracket nesting — the soundness backstop the whole-file `balanced()`
268
486
  // (global) and `indentsOk()` (indentation) both miss. A token merge can splice a result
269
487
  // that is GLOBALLY balanced yet LOCALLY mis-nested: side A reformats `f(x)` to span lines
@@ -308,6 +526,16 @@ export const python = {
308
526
  for (const t of lexTokens(src)) if (isRefToken(src, t)) used.add(t.k)
309
527
  return used
310
528
  },
529
+ // Scope-aware upgrade of the above — the engine prefers this for the dangling-reference
530
+ // check (icr.js). Names that resolve to a LOCAL binding are not reported, so a deleted
531
+ // top-level `helper` is no longer held hostage by an unrelated local of the same name.
532
+ // Kept alongside usedIdentifiers rather than replacing it: the approximate version is
533
+ // still the honest answer for anything this cannot resolve.
534
+ referencedFreeNames: (src) => {
535
+ const free = new Set()
536
+ scopeFree(String(src), 0, new Set(), free, false)
537
+ return free
538
+ },
311
539
  // The def/class text with its own name excluded, so renamed twins compare equal.
312
540
  // trimEnd(): a unit owns its trailing blank lines, and splicing can change how many
313
541
  // survive — trailing whitespace must not defeat the rename match.
package/lang-yaml.js CHANGED
@@ -39,13 +39,44 @@ function lines(src) {
39
39
  return out
40
40
  }
41
41
 
42
- const KEY = /^([^:#\s][^:#]*?):(\s|$)/ // `key:` or `key: value`
42
+ // KEY SHAPES. A YAML key may be QUOTED, and a quoted key legally contains ':' and '#';
43
+ // a plain key may contain '#' too ('#' only opens a comment at line start or after a
44
+ // space). The original class excluded both characters from the key body, so any such key
45
+ // failed KEY and fell through to `'stmt:' + <whole head line>` — a unit key that contains
46
+ // the VALUE. Two sides editing that ONE key then produced two DIFFERENT unit keys, so the
47
+ // engine saw unrelated add/deletes, unioned them, and emitted the key TWICE while
48
+ // reporting the merge 'auto' and clean. Duplicate mapping keys are invalid YAML and every
49
+ // loader keeps the last, so one side's edit was silently discarded — no-silent-loss
50
+ // failing exactly as it did for type-first method signatures in lang-brace.js.
51
+ // Measured before the fix (both top level and nested): "a:b", 'a:b', foo#bar, "a#b" all
52
+ // auto-merged into a duplicated key; plain keys conflicted correctly.
53
+ const QKEY = /^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*')\s*:(\s|$)/ // "quoted key": / 'quoted key':
54
+ const KEY = /^([^:#\s][^:]*?):(\s|$)/ // `key:` or `key: value` (body may contain '#')
43
55
  const DASH = /^-(\s|$)/ // list item
44
56
 
57
+ // Cut a trailing ` #comment` that is not inside quotes, so it never lands in a key.
58
+ function stripComment(line) {
59
+ const at = scannerAt(line)
60
+ let i = 0
61
+ while (i < line.length) {
62
+ if (line[i] === '#' && i > 0 && /\s/.test(line[i - 1])) return line.slice(0, i)
63
+ const j = at(i)
64
+ if (j > i && line[i] !== '#') { i = j; continue }
65
+ i++
66
+ }
67
+ return line
68
+ }
69
+
70
+ // `"a"` and `a` name the SAME key in YAML, so they must key the same — otherwise the
71
+ // pair is the very ambiguity this function exists to remove.
72
+ const unquote = (k) => (/^"(?:[^"\\]|\\.)*"$/.test(k) || /^'(?:[^']|'')*'$/.test(k)) ? k.slice(1, -1) : k
73
+
45
74
  function keyOf(text) {
46
- const t = text.trim()
47
- const m = t.match(KEY)
48
- if (m) return 'key:' + m[1].trim()
75
+ const t = stripComment(text.trim()).trim()
76
+ let m = t.match(QKEY)
77
+ if (m) return 'key:' + unquote(m[1])
78
+ m = t.match(KEY)
79
+ if (m) return 'key:' + unquote(m[1].trim())
49
80
  if (t === '---' || t === '...') return 'doc:' + t
50
81
  return 'stmt:' + t.replace(/\s+/g, ' ')
51
82
  }
@@ -79,10 +110,11 @@ function keyed(raw, src) {
79
110
  const text = src.slice(u.start, u.end)
80
111
  // key by the HEAD line (skipping attached leading comments)
81
112
  const head = text.split('\n').find((l) => l.trim() && !l.trim().startsWith('#')) || text
82
- let k = keyOf(head)
113
+ const baseKey = keyOf(head)
114
+ let k = baseKey
83
115
  const c = (seen.get(k) || 0) + 1; seen.set(k, c)
84
116
  if (c > 1) k += '#' + c
85
- out.push({ key: k, text, start: u.start, end: u.end })
117
+ out.push({ key: k, baseKey, text, start: u.start, end: u.end })
86
118
  }
87
119
  return out
88
120
  }