hivecode-mcp 0.14.0 → 0.14.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.
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 -450
  12. package/docs/TROUBLESHOOTING.md +105 -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 +608 -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 +2004 -1944
  21. package/hive-hook-install.js +179 -179
  22. package/hive-instructions.js +74 -74
  23. package/hive-land.js +695 -691
  24. package/hive-listen.js +81 -81
  25. package/hive-mailbox.js +520 -520
  26. package/hive-mcp-http.js +591 -580
  27. package/hive-mcp.js +2156 -2093
  28. package/hive-mux-proto.js +58 -58
  29. package/hive-mux.js +351 -350
  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 +600 -512
  35. package/hive-privacy.js +327 -327
  36. package/hive-receipts.js +233 -233
  37. package/hive-relay-health.js +234 -230
  38. package/hive-repos.js +430 -430
  39. package/hive-room-key.js +210 -209
  40. package/hive-room-mode.js +1668 -1668
  41. package/hive-room-watcher.js +562 -561
  42. package/hive-secrets.js +89 -89
  43. package/hive-session.js +114 -114
  44. package/hive-setup.js +560 -558
  45. package/hive-slack-agent.js +408 -396
  46. package/hive-slack.js +2384 -1809
  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 +578 -353
  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 +851 -851
  57. package/lang-brace.js +305 -305
  58. package/lang-js.js +651 -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 +192 -192
  66. package/reconcile.cjs +621 -614
  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 +5189 -4994
  72. package/token.js +201 -201
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
+ }