hivecode-mcp 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/HIVE_RULES.md +11 -40
  2. package/LICENSE +21 -21
  3. package/README.md +421 -167
  4. package/core.js +91 -91
  5. package/docs/ALWAYS_ON.md +310 -0
  6. package/docs/GITHUB_WORKFLOW.md +334 -0
  7. package/docs/HIVE_RULES.md +41 -41
  8. package/docs/MCP.md +157 -118
  9. package/docs/QUICKSTART.md +266 -0
  10. package/docs/SELF_HOSTING.md +256 -256
  11. package/docs/SLACK_SETUP.md +450 -0
  12. package/docs/TROUBLESHOOTING.md +87 -0
  13. package/hive-activity.js +336 -310
  14. package/hive-await.js +207 -0
  15. package/hive-branch.js +686 -0
  16. package/hive-busy.js +88 -88
  17. package/hive-coord.js +110 -110
  18. package/hive-gate.js +135 -135
  19. package/hive-github.js +1904 -0
  20. package/hive-hook-install.js +179 -103
  21. package/hive-instructions.js +74 -66
  22. package/hive-listen.js +81 -0
  23. package/hive-mailbox.js +520 -386
  24. package/hive-mcp-http.js +580 -0
  25. package/hive-mcp.js +579 -137
  26. package/hive-mux-proto.js +58 -58
  27. package/hive-mux.js +350 -350
  28. package/hive-notify.js +233 -0
  29. package/hive-obligations.js +247 -247
  30. package/hive-pr-staleness.js +189 -189
  31. package/hive-pr.js +350 -0
  32. package/hive-privacy.js +327 -288
  33. package/hive-receipts.js +233 -233
  34. package/hive-relay-health.js +230 -225
  35. package/hive-room-key.js +209 -0
  36. package/hive-secrets.js +89 -0
  37. package/hive-session.js +114 -0
  38. package/hive-setup.js +558 -445
  39. package/hive-slack-agent.js +396 -0
  40. package/hive-slack.js +1809 -0
  41. package/hive-stdio.js +172 -168
  42. package/hive-stop-hook.js +293 -241
  43. package/hive-throttle.js +118 -118
  44. package/hive-time.js +71 -0
  45. package/hive-update.js +58 -58
  46. package/hive-version.js +110 -59
  47. package/hive-webhook.js +54 -0
  48. package/icr-merge.js +58 -58
  49. package/icr.js +851 -745
  50. package/lang-json.js +211 -211
  51. package/lang-ruby.js +230 -230
  52. package/lang-toml.js +136 -136
  53. package/lang-yaml.js +183 -183
  54. package/limits.js +413 -394
  55. package/package.json +189 -164
  56. package/reconcile.cjs +614 -477
  57. package/slack-app-manifest-agent.json +61 -0
  58. package/slack-app-manifest-inbound.json +36 -0
  59. package/slack-app-manifest.json +27 -0
  60. package/substrate.js +252 -252
  61. package/sync.js +1956 -177
  62. package/grm-link.cjs +0 -141
package/lang-toml.js CHANGED
@@ -1,136 +1,136 @@
1
- // TOML language provider for ICR — section/key structural splitter.
2
- // Cargo.toml / pyproject.toml are constantly edited concurrently (two people add
3
- // dependencies). This provider merges by STRUCTURE: every `[section]` (and
4
- // `[[array-of-tables]]`) is a unit keyed by its name; keys in the top-level
5
- // preamble are units of their own; splitUnit descends into a section so two
6
- // edits to DIFFERENT keys of the same table merge cleanly.
7
- //
8
- // Honest heuristic: tracks #-comments and single/double/triple-quoted strings;
9
- // parses() = balanced delimiters + well-formed section headers.
10
-
11
- function scannerAt(src) {
12
- const n = src.length
13
- return function at(i) {
14
- if (src[i] === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; return j }
15
- for (const q of ['"""', "'''"]) if (src.startsWith(q, i)) { let j = i + 3; while (j < n && !src.startsWith(q, j)) { if (src[j] === '\\') j++; j++ } return Math.min(n, j + 3) }
16
- if (src[i] === '"' || src[i] === "'") {
17
- const q = src[i]; let j = i + 1
18
- while (j < n) { if (q === '"' && src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } if (src[j] === '\n') break; j++ }
19
- return j
20
- }
21
- return -1
22
- }
23
- }
24
-
25
- function lines(src) {
26
- const out = []
27
- let i = 0
28
- while (i < src.length) {
29
- let e = src.indexOf('\n', i); e = e < 0 ? src.length : e + 1
30
- const body = src.slice(i, e).replace(/\r?\n$/, '')
31
- const trimmed = body.trim()
32
- out.push({ start: i, end: e, blank: trimmed === '' || trimmed.startsWith('#'), text: body })
33
- i = e
34
- }
35
- return out
36
- }
37
-
38
- const SECTION = /^\s*(\[\[?)([^\]]+)\]\]?\s*(#.*)?$/
39
- const KEYLINE = /^\s*([A-Za-z0-9_."'-]+)\s*=/
40
-
41
- // Bracket delta for one line, string/comment aware (multi-line arrays continue a key).
42
- function bracketDelta(src, at, s, e) {
43
- let d = 0, k = s
44
- while (k < e) {
45
- const j = at(k); if (j > k) { k = Math.min(e, j); continue }
46
- const c = src[k]
47
- if (c === '[' || c === '{') d++
48
- else if (c === ']' || c === '}') d--
49
- k++
50
- }
51
- return d
52
- }
53
-
54
- // Top-level units: a [section] header owns everything until the next header;
55
- // preamble `key = value` lines are individual units (multi-line values included).
56
- function blocks(src) {
57
- const at = scannerAt(src)
58
- const L = lines(src)
59
- const units = []
60
- let i = 0
61
- while (i < L.length) {
62
- while (i < L.length && L[i].text.trim() === '') i++
63
- if (i >= L.length) break
64
- const start = L[i].start
65
- while (i < L.length && L[i].blank && L[i].text.trim().startsWith('#')) i++ // leading comments attach
66
- if (i >= L.length) { units.push({ start, end: L[L.length - 1].end }); break }
67
- if (SECTION.test(L[i].text)) {
68
- i++ // header
69
- while (i < L.length && !SECTION.test(L[i].text)) i++
70
- let last = i - 1
71
- while (last >= 0 && L[last].text.trim() === '') last--
72
- units.push({ start, end: L[last].end })
73
- } else {
74
- // one key (or stray statement); consume continuation lines while brackets open
75
- let depth = bracketDelta(src, at, L[i].start, L[i].end)
76
- let e = L[i].end; i++
77
- while (i < L.length && depth > 0) { depth += bracketDelta(src, at, L[i].start, L[i].end); e = L[i].end; i++ }
78
- units.push({ start, end: e })
79
- }
80
- }
81
- return units
82
- }
83
-
84
- function keyOf(text) {
85
- const head = text.split('\n').find((l) => l.trim() && !l.trim().startsWith('#')) || text
86
- let m = head.match(SECTION); if (m) return (m[1] === '[[' ? 'tables:' : 'table:') + m[2].trim()
87
- m = head.match(KEYLINE); if (m) return 'key:' + m[1].replace(/["']/g, '')
88
- return 'stmt:' + head.trim().replace(/\s+/g, ' ')
89
- }
90
-
91
- function keyed(raw, src) {
92
- const seen = new Map(), out = []
93
- for (const u of raw) {
94
- const text = src.slice(u.start, u.end)
95
- let k = keyOf(text)
96
- const c = (seen.get(k) || 0) + 1; seen.set(k, c)
97
- if (c > 1) k += '#' + c
98
- out.push({ key: k, text, start: u.start, end: u.end })
99
- }
100
- return out
101
- }
102
-
103
- function balanced(src) {
104
- const at = scannerAt(src)
105
- const stack = []
106
- const pair = { ']': '[', '}': '{' }
107
- let i = 0
108
- while (i < src.length) {
109
- const j = at(i); if (j > i) { if (j > src.length) return false; i = j; continue }
110
- const c = src[i]
111
- if (c === '[' || c === '{') stack.push(c)
112
- else if (pair[c]) { if (stack.pop() !== pair[c]) return false }
113
- i++
114
- }
115
- return stack.length === 0
116
- }
117
-
118
- export const toml = {
119
- id: 'toml',
120
- exts: ['.toml'],
121
- parses: (src) => balanced(String(src == null ? '' : src)),
122
- parsesUnit: (src) => balanced(String(src)),
123
- units: (src) => keyed(blocks(String(src)), String(src)),
124
- declaredNames: () => new Set(),
125
- // Descend into a section: each `key = value` inside merges independently.
126
- splitUnit: (text) => {
127
- const src = String(text)
128
- const L = lines(src)
129
- let h = 0; while (h < L.length && L[h].blank) h++
130
- if (h >= L.length || !SECTION.test(L[h].text)) return null
131
- const sig = src.slice(0, L[h].end)
132
- const body = src.slice(L[h].end)
133
- const inner = keyed(blocks(body), body)
134
- return { sig, open: '', close: '', body, units: inner, join: '', spliceable: true }
135
- },
136
- }
1
+ // TOML language provider for ICR — section/key structural splitter.
2
+ // Cargo.toml / pyproject.toml are constantly edited concurrently (two people add
3
+ // dependencies). This provider merges by STRUCTURE: every `[section]` (and
4
+ // `[[array-of-tables]]`) is a unit keyed by its name; keys in the top-level
5
+ // preamble are units of their own; splitUnit descends into a section so two
6
+ // edits to DIFFERENT keys of the same table merge cleanly.
7
+ //
8
+ // Honest heuristic: tracks #-comments and single/double/triple-quoted strings;
9
+ // parses() = balanced delimiters + well-formed section headers.
10
+
11
+ function scannerAt(src) {
12
+ const n = src.length
13
+ return function at(i) {
14
+ if (src[i] === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; return j }
15
+ for (const q of ['"""', "'''"]) if (src.startsWith(q, i)) { let j = i + 3; while (j < n && !src.startsWith(q, j)) { if (src[j] === '\\') j++; j++ } return Math.min(n, j + 3) }
16
+ if (src[i] === '"' || src[i] === "'") {
17
+ const q = src[i]; let j = i + 1
18
+ while (j < n) { if (q === '"' && src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } if (src[j] === '\n') break; j++ }
19
+ return j
20
+ }
21
+ return -1
22
+ }
23
+ }
24
+
25
+ function lines(src) {
26
+ const out = []
27
+ let i = 0
28
+ while (i < src.length) {
29
+ let e = src.indexOf('\n', i); e = e < 0 ? src.length : e + 1
30
+ const body = src.slice(i, e).replace(/\r?\n$/, '')
31
+ const trimmed = body.trim()
32
+ out.push({ start: i, end: e, blank: trimmed === '' || trimmed.startsWith('#'), text: body })
33
+ i = e
34
+ }
35
+ return out
36
+ }
37
+
38
+ const SECTION = /^\s*(\[\[?)([^\]]+)\]\]?\s*(#.*)?$/
39
+ const KEYLINE = /^\s*([A-Za-z0-9_."'-]+)\s*=/
40
+
41
+ // Bracket delta for one line, string/comment aware (multi-line arrays continue a key).
42
+ function bracketDelta(src, at, s, e) {
43
+ let d = 0, k = s
44
+ while (k < e) {
45
+ const j = at(k); if (j > k) { k = Math.min(e, j); continue }
46
+ const c = src[k]
47
+ if (c === '[' || c === '{') d++
48
+ else if (c === ']' || c === '}') d--
49
+ k++
50
+ }
51
+ return d
52
+ }
53
+
54
+ // Top-level units: a [section] header owns everything until the next header;
55
+ // preamble `key = value` lines are individual units (multi-line values included).
56
+ function blocks(src) {
57
+ const at = scannerAt(src)
58
+ const L = lines(src)
59
+ const units = []
60
+ let i = 0
61
+ while (i < L.length) {
62
+ while (i < L.length && L[i].text.trim() === '') i++
63
+ if (i >= L.length) break
64
+ const start = L[i].start
65
+ while (i < L.length && L[i].blank && L[i].text.trim().startsWith('#')) i++ // leading comments attach
66
+ if (i >= L.length) { units.push({ start, end: L[L.length - 1].end }); break }
67
+ if (SECTION.test(L[i].text)) {
68
+ i++ // header
69
+ while (i < L.length && !SECTION.test(L[i].text)) i++
70
+ let last = i - 1
71
+ while (last >= 0 && L[last].text.trim() === '') last--
72
+ units.push({ start, end: L[last].end })
73
+ } else {
74
+ // one key (or stray statement); consume continuation lines while brackets open
75
+ let depth = bracketDelta(src, at, L[i].start, L[i].end)
76
+ let e = L[i].end; i++
77
+ while (i < L.length && depth > 0) { depth += bracketDelta(src, at, L[i].start, L[i].end); e = L[i].end; i++ }
78
+ units.push({ start, end: e })
79
+ }
80
+ }
81
+ return units
82
+ }
83
+
84
+ function keyOf(text) {
85
+ const head = text.split('\n').find((l) => l.trim() && !l.trim().startsWith('#')) || text
86
+ let m = head.match(SECTION); if (m) return (m[1] === '[[' ? 'tables:' : 'table:') + m[2].trim()
87
+ m = head.match(KEYLINE); if (m) return 'key:' + m[1].replace(/["']/g, '')
88
+ return 'stmt:' + head.trim().replace(/\s+/g, ' ')
89
+ }
90
+
91
+ function keyed(raw, src) {
92
+ const seen = new Map(), out = []
93
+ for (const u of raw) {
94
+ const text = src.slice(u.start, u.end)
95
+ let k = keyOf(text)
96
+ const c = (seen.get(k) || 0) + 1; seen.set(k, c)
97
+ if (c > 1) k += '#' + c
98
+ out.push({ key: k, text, start: u.start, end: u.end })
99
+ }
100
+ return out
101
+ }
102
+
103
+ function balanced(src) {
104
+ const at = scannerAt(src)
105
+ const stack = []
106
+ const pair = { ']': '[', '}': '{' }
107
+ let i = 0
108
+ while (i < src.length) {
109
+ const j = at(i); if (j > i) { if (j > src.length) return false; i = j; continue }
110
+ const c = src[i]
111
+ if (c === '[' || c === '{') stack.push(c)
112
+ else if (pair[c]) { if (stack.pop() !== pair[c]) return false }
113
+ i++
114
+ }
115
+ return stack.length === 0
116
+ }
117
+
118
+ export const toml = {
119
+ id: 'toml',
120
+ exts: ['.toml'],
121
+ parses: (src) => balanced(String(src == null ? '' : src)),
122
+ parsesUnit: (src) => balanced(String(src)),
123
+ units: (src) => keyed(blocks(String(src)), String(src)),
124
+ declaredNames: () => new Set(),
125
+ // Descend into a section: each `key = value` inside merges independently.
126
+ splitUnit: (text) => {
127
+ const src = String(text)
128
+ const L = lines(src)
129
+ let h = 0; while (h < L.length && L[h].blank) h++
130
+ if (h >= L.length || !SECTION.test(L[h].text)) return null
131
+ const sig = src.slice(0, L[h].end)
132
+ const body = src.slice(L[h].end)
133
+ const inner = keyed(blocks(body), body)
134
+ return { sig, open: '', close: '', body, units: inner, join: '', spliceable: true }
135
+ },
136
+ }
package/lang-yaml.js CHANGED
@@ -1,183 +1,183 @@
1
- // YAML language provider for ICR — indent-mapping structural splitter.
2
- // YAML configs (CI pipelines, k8s manifests, docker-compose) are merged constantly
3
- // and line merges garble them. This provider merges by STRUCTURE: every top-level
4
- // `key:` block is a unit keyed by its key name, and splitUnit descends into nested
5
- // mapping levels — so two people editing DIFFERENT jobs/services/sections merge
6
- // cleanly, and the SAME key edited two ways is a named conflict.
7
- //
8
- // Honest heuristic (no parser dependency): tracks #-comments, quoted strings, and
9
- // flow brackets; parses() checks bracket balance + indentation coherence (a dedent
10
- // must land on an indent level that exists — the failure mode of a bad splice).
11
- // Block scalars (|, >) are tolerated: their content is deeper-indented free text.
12
-
13
- function scannerAt(src) {
14
- const n = src.length
15
- return function at(i) {
16
- if (src[i] === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; return j }
17
- if (src[i] === '"' || src[i] === "'") {
18
- const q = src[i]; let j = i + 1
19
- while (j < n) { if (q === '"' && src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } if (src[j] === '\n') break; j++ }
20
- return j
21
- }
22
- return -1
23
- }
24
- }
25
-
26
- // Line records with indent; comment-only and blank lines are marked.
27
- function lines(src) {
28
- const out = []
29
- let i = 0
30
- while (i < src.length) {
31
- let e = src.indexOf('\n', i); e = e < 0 ? src.length : e + 1
32
- const raw = src.slice(i, e)
33
- const body = raw.replace(/\r?\n$/, '')
34
- let indent = 0; while (indent < body.length && body[indent] === ' ') indent++
35
- const trimmed = body.trim()
36
- out.push({ start: i, end: e, indent, blank: trimmed === '' || trimmed.startsWith('#'), text: body })
37
- i = e
38
- }
39
- return out
40
- }
41
-
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 '#')
55
- const DASH = /^-(\s|$)/ // list item
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
-
74
- function keyOf(text) {
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())
80
- if (t === '---' || t === '...') return 'doc:' + t
81
- return 'stmt:' + t.replace(/\s+/g, ' ')
82
- }
83
-
84
- // Units whose heads sit at exactly `baseIndent`: a `key:`/`- ` head owns all
85
- // following blank or deeper-indented lines (its block).
86
- function blocks(src, baseIndent = 0) {
87
- const L = lines(src)
88
- const units = []
89
- let i = 0
90
- while (i < L.length) {
91
- while (i < L.length && L[i].text.trim() === '') i++ // pure blanks are gap text
92
- if (i >= L.length) break
93
- const start = L[i].start
94
- // leading comment lines attach to the unit that follows
95
- while (i < L.length && L[i].blank && L[i].text.trim().startsWith('#')) i++
96
- if (i >= L.length) { units.push({ start, end: L[L.length - 1].end }); break }
97
- if (L[i].indent !== baseIndent) { units.push({ start, end: L[i].end }); i++; continue }
98
- i++ // the head line
99
- while (i < L.length && (L[i].blank || L[i].indent > baseIndent)) i++
100
- let last = i - 1
101
- while (last >= 0 && L[last].text.trim() === '') last-- // trailing blanks are gap text
102
- units.push({ start, end: L[last].end })
103
- }
104
- return units
105
- }
106
-
107
- function keyed(raw, src) {
108
- const seen = new Map(), out = []
109
- for (const u of raw) {
110
- const text = src.slice(u.start, u.end)
111
- // key by the HEAD line (skipping attached leading comments)
112
- const head = text.split('\n').find((l) => l.trim() && !l.trim().startsWith('#')) || text
113
- const baseKey = keyOf(head)
114
- let k = baseKey
115
- const c = (seen.get(k) || 0) + 1; seen.set(k, c)
116
- if (c > 1) k += '#' + c
117
- out.push({ key: k, baseKey, text, start: u.start, end: u.end })
118
- }
119
- return out
120
- }
121
-
122
- // Bracket balance (flow style) + unterminated-string detection.
123
- function balanced(src) {
124
- const at = scannerAt(src)
125
- const stack = []
126
- const pair = { ')': '(', ']': '[', '}': '{' }
127
- let i = 0
128
- while (i < src.length) {
129
- const j = at(i); if (j > i) { if (j > src.length) return false; i = j; continue }
130
- const c = src[i]
131
- if (c === '(' || c === '[' || c === '{') stack.push(c)
132
- else if (pair[c]) { if (stack.pop() !== pair[c]) return false }
133
- i++
134
- }
135
- return stack.length === 0
136
- }
137
-
138
- // Indentation coherence: an indent INCREASE opens a new level (always legal in YAML —
139
- // nested mapping, list, or block-scalar content); a DECREASE must land on a level
140
- // that is already on the stack. This is exactly what a bad merge splice violates.
141
- function indentsOk(src) {
142
- const L = lines(src)
143
- let stack = null
144
- for (const l of L) {
145
- if (l.blank) continue
146
- if (stack == null) { stack = [l.indent]; continue }
147
- const top = stack[stack.length - 1]
148
- if (l.indent > top) { stack.push(l.indent); continue }
149
- if (l.indent < top) {
150
- while (stack.length > 1 && stack[stack.length - 1] > l.indent) stack.pop()
151
- if (stack[stack.length - 1] !== l.indent) return false
152
- }
153
- }
154
- return true
155
- }
156
-
157
- export const yaml = {
158
- id: 'yaml',
159
- exts: ['.yml', '.yaml'],
160
- parses: (src) => { const s = String(src == null ? '' : src); return balanced(s) && indentsOk(s) },
161
- parsesUnit: (src) => { const s = String(src); return balanced(s) && indentsOk(s) },
162
- units: (src) => keyed(blocks(String(src), 0), String(src)),
163
- declaredNames: () => new Set(), // keys are structure, not code declarations
164
- // Descend one mapping level: `jobs:` -> each job merges independently.
165
- splitUnit: (text) => {
166
- const src = String(text)
167
- const L = lines(src)
168
- let h = 0; while (h < L.length && L[h].blank) h++
169
- if (h >= L.length) return null
170
- const head = L[h]
171
- if (!KEY.test(head.text.trim())) return null
172
- if (head.text.trim().match(/:\s*[|>][+-]?\s*(#.*)?$/)) return null // block scalar: free text, do not descend
173
- if (!/:\s*(#.*)?$/.test(head.text)) return null // inline value (`key: v`): nothing beneath
174
- let b = h + 1; while (b < L.length && L[b].blank) b++
175
- if (b >= L.length || L[b].indent <= head.indent) return null // empty block
176
- const bodyIndent = L[b].indent
177
- const sig = src.slice(0, L[h].end)
178
- const body = src.slice(L[h].end)
179
- const inner = keyed(blocks(body, bodyIndent), body)
180
- if (inner.some((u) => u.key.startsWith('stmt:-') || DASH.test(u.text.trim()))) return null // list items: order matters, stay whole
181
- return { sig, open: '', close: '', body, units: inner, join: '', spliceable: true }
182
- },
183
- }
1
+ // YAML language provider for ICR — indent-mapping structural splitter.
2
+ // YAML configs (CI pipelines, k8s manifests, docker-compose) are merged constantly
3
+ // and line merges garble them. This provider merges by STRUCTURE: every top-level
4
+ // `key:` block is a unit keyed by its key name, and splitUnit descends into nested
5
+ // mapping levels — so two people editing DIFFERENT jobs/services/sections merge
6
+ // cleanly, and the SAME key edited two ways is a named conflict.
7
+ //
8
+ // Honest heuristic (no parser dependency): tracks #-comments, quoted strings, and
9
+ // flow brackets; parses() checks bracket balance + indentation coherence (a dedent
10
+ // must land on an indent level that exists — the failure mode of a bad splice).
11
+ // Block scalars (|, >) are tolerated: their content is deeper-indented free text.
12
+
13
+ function scannerAt(src) {
14
+ const n = src.length
15
+ return function at(i) {
16
+ if (src[i] === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; return j }
17
+ if (src[i] === '"' || src[i] === "'") {
18
+ const q = src[i]; let j = i + 1
19
+ while (j < n) { if (q === '"' && src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } if (src[j] === '\n') break; j++ }
20
+ return j
21
+ }
22
+ return -1
23
+ }
24
+ }
25
+
26
+ // Line records with indent; comment-only and blank lines are marked.
27
+ function lines(src) {
28
+ const out = []
29
+ let i = 0
30
+ while (i < src.length) {
31
+ let e = src.indexOf('\n', i); e = e < 0 ? src.length : e + 1
32
+ const raw = src.slice(i, e)
33
+ const body = raw.replace(/\r?\n$/, '')
34
+ let indent = 0; while (indent < body.length && body[indent] === ' ') indent++
35
+ const trimmed = body.trim()
36
+ out.push({ start: i, end: e, indent, blank: trimmed === '' || trimmed.startsWith('#'), text: body })
37
+ i = e
38
+ }
39
+ return out
40
+ }
41
+
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 '#')
55
+ const DASH = /^-(\s|$)/ // list item
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
+
74
+ function keyOf(text) {
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())
80
+ if (t === '---' || t === '...') return 'doc:' + t
81
+ return 'stmt:' + t.replace(/\s+/g, ' ')
82
+ }
83
+
84
+ // Units whose heads sit at exactly `baseIndent`: a `key:`/`- ` head owns all
85
+ // following blank or deeper-indented lines (its block).
86
+ function blocks(src, baseIndent = 0) {
87
+ const L = lines(src)
88
+ const units = []
89
+ let i = 0
90
+ while (i < L.length) {
91
+ while (i < L.length && L[i].text.trim() === '') i++ // pure blanks are gap text
92
+ if (i >= L.length) break
93
+ const start = L[i].start
94
+ // leading comment lines attach to the unit that follows
95
+ while (i < L.length && L[i].blank && L[i].text.trim().startsWith('#')) i++
96
+ if (i >= L.length) { units.push({ start, end: L[L.length - 1].end }); break }
97
+ if (L[i].indent !== baseIndent) { units.push({ start, end: L[i].end }); i++; continue }
98
+ i++ // the head line
99
+ while (i < L.length && (L[i].blank || L[i].indent > baseIndent)) i++
100
+ let last = i - 1
101
+ while (last >= 0 && L[last].text.trim() === '') last-- // trailing blanks are gap text
102
+ units.push({ start, end: L[last].end })
103
+ }
104
+ return units
105
+ }
106
+
107
+ function keyed(raw, src) {
108
+ const seen = new Map(), out = []
109
+ for (const u of raw) {
110
+ const text = src.slice(u.start, u.end)
111
+ // key by the HEAD line (skipping attached leading comments)
112
+ const head = text.split('\n').find((l) => l.trim() && !l.trim().startsWith('#')) || text
113
+ const baseKey = keyOf(head)
114
+ let k = baseKey
115
+ const c = (seen.get(k) || 0) + 1; seen.set(k, c)
116
+ if (c > 1) k += '#' + c
117
+ out.push({ key: k, baseKey, text, start: u.start, end: u.end })
118
+ }
119
+ return out
120
+ }
121
+
122
+ // Bracket balance (flow style) + unterminated-string detection.
123
+ function balanced(src) {
124
+ const at = scannerAt(src)
125
+ const stack = []
126
+ const pair = { ')': '(', ']': '[', '}': '{' }
127
+ let i = 0
128
+ while (i < src.length) {
129
+ const j = at(i); if (j > i) { if (j > src.length) return false; i = j; continue }
130
+ const c = src[i]
131
+ if (c === '(' || c === '[' || c === '{') stack.push(c)
132
+ else if (pair[c]) { if (stack.pop() !== pair[c]) return false }
133
+ i++
134
+ }
135
+ return stack.length === 0
136
+ }
137
+
138
+ // Indentation coherence: an indent INCREASE opens a new level (always legal in YAML —
139
+ // nested mapping, list, or block-scalar content); a DECREASE must land on a level
140
+ // that is already on the stack. This is exactly what a bad merge splice violates.
141
+ function indentsOk(src) {
142
+ const L = lines(src)
143
+ let stack = null
144
+ for (const l of L) {
145
+ if (l.blank) continue
146
+ if (stack == null) { stack = [l.indent]; continue }
147
+ const top = stack[stack.length - 1]
148
+ if (l.indent > top) { stack.push(l.indent); continue }
149
+ if (l.indent < top) {
150
+ while (stack.length > 1 && stack[stack.length - 1] > l.indent) stack.pop()
151
+ if (stack[stack.length - 1] !== l.indent) return false
152
+ }
153
+ }
154
+ return true
155
+ }
156
+
157
+ export const yaml = {
158
+ id: 'yaml',
159
+ exts: ['.yml', '.yaml'],
160
+ parses: (src) => { const s = String(src == null ? '' : src); return balanced(s) && indentsOk(s) },
161
+ parsesUnit: (src) => { const s = String(src); return balanced(s) && indentsOk(s) },
162
+ units: (src) => keyed(blocks(String(src), 0), String(src)),
163
+ declaredNames: () => new Set(), // keys are structure, not code declarations
164
+ // Descend one mapping level: `jobs:` -> each job merges independently.
165
+ splitUnit: (text) => {
166
+ const src = String(text)
167
+ const L = lines(src)
168
+ let h = 0; while (h < L.length && L[h].blank) h++
169
+ if (h >= L.length) return null
170
+ const head = L[h]
171
+ if (!KEY.test(head.text.trim())) return null
172
+ if (head.text.trim().match(/:\s*[|>][+-]?\s*(#.*)?$/)) return null // block scalar: free text, do not descend
173
+ if (!/:\s*(#.*)?$/.test(head.text)) return null // inline value (`key: v`): nothing beneath
174
+ let b = h + 1; while (b < L.length && L[b].blank) b++
175
+ if (b >= L.length || L[b].indent <= head.indent) return null // empty block
176
+ const bodyIndent = L[b].indent
177
+ const sig = src.slice(0, L[h].end)
178
+ const body = src.slice(L[h].end)
179
+ const inner = keyed(blocks(body, bodyIndent), body)
180
+ if (inner.some((u) => u.key.startsWith('stmt:-') || DASH.test(u.text.trim()))) return null // list items: order matters, stay whole
181
+ return { sig, open: '', close: '', body, units: inner, join: '', spliceable: true }
182
+ },
183
+ }