hivecode-mcp 0.14.1 → 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.
- package/HIVE_RULES.md +98 -98
- package/LICENSE +21 -21
- package/README.md +421 -421
- package/core.js +91 -91
- package/docs/ALWAYS_ON.md +310 -310
- package/docs/GITHUB_WORKFLOW.md +394 -394
- package/docs/HIVE_RULES.md +41 -41
- package/docs/MCP.md +157 -157
- package/docs/QUICKSTART.md +266 -266
- package/docs/SELF_HOSTING.md +256 -256
- package/docs/SLACK_SETUP.md +474 -474
- package/docs/TROUBLESHOOTING.md +105 -105
- package/hive-activity.js +336 -336
- package/hive-authors.js +46 -44
- package/hive-await.js +230 -230
- package/hive-branch.js +608 -607
- package/hive-busy.js +88 -88
- package/hive-coord.js +110 -110
- package/hive-gate.js +135 -135
- package/hive-github.js +3 -1
- package/hive-hook-install.js +179 -179
- package/hive-instructions.js +74 -74
- package/hive-land.js +695 -691
- package/hive-listen.js +81 -81
- package/hive-mailbox.js +520 -520
- package/hive-mcp-http.js +591 -591
- package/hive-mcp.js +33 -6
- package/hive-mux-proto.js +58 -58
- package/hive-mux.js +351 -351
- package/hive-notify.js +234 -233
- package/hive-obligations.js +247 -247
- package/hive-poll-proto.js +50 -50
- package/hive-pr-staleness.js +189 -189
- package/hive-pr.js +600 -512
- package/hive-privacy.js +327 -327
- package/hive-receipts.js +233 -233
- package/hive-relay-health.js +234 -234
- package/hive-repos.js +430 -430
- package/hive-room-key.js +210 -210
- package/hive-room-mode.js +1668 -1668
- package/hive-room-watcher.js +562 -561
- package/hive-secrets.js +89 -89
- package/hive-session.js +114 -114
- package/hive-setup.js +560 -560
- package/hive-slack-agent.js +408 -408
- package/hive-slack.js +2384 -2367
- package/hive-stdio.js +172 -172
- package/hive-stop-hook.js +293 -293
- package/hive-throttle.js +118 -118
- package/hive-time.js +71 -71
- package/hive-transport.js +578 -578
- package/hive-update.js +58 -58
- package/hive-version.js +110 -110
- package/hive-webhook.js +55 -54
- package/icr-merge.js +58 -58
- package/icr.js +851 -851
- package/lang-brace.js +305 -305
- package/lang-js.js +651 -651
- package/lang-json.js +211 -211
- package/lang-python.js +588 -588
- package/lang-ruby.js +230 -230
- package/lang-toml.js +136 -136
- package/lang-yaml.js +183 -183
- package/limits.js +413 -413
- package/package.json +1 -1
- package/reconcile.cjs +621 -621
- package/slack-app-manifest-agent.json +61 -61
- package/slack-app-manifest-inbound.json +36 -36
- package/slack-app-manifest.json +27 -27
- package/substrate.js +252 -252
- package/sync.js +169 -11
- 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
|
+
}
|