hivecode-mcp 0.12.2 → 0.12.4
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/LICENSE +21 -21
- package/README.md +187 -8
- package/core.js +91 -91
- package/docs/HIVE_RULES.md +41 -41
- package/docs/MCP.md +5 -5
- package/docs/SELF_HOSTING.md +256 -256
- package/hive-activity.js +336 -310
- package/hive-await.js +187 -0
- package/hive-branch.js +656 -0
- package/hive-busy.js +88 -88
- package/hive-coord.js +110 -110
- package/hive-gate.js +135 -135
- package/hive-hook-install.js +179 -103
- package/hive-instructions.js +74 -66
- package/hive-listen.js +81 -0
- package/hive-mailbox.js +517 -386
- package/hive-mcp.js +483 -126
- package/hive-mux-proto.js +58 -58
- package/hive-mux.js +350 -350
- package/hive-obligations.js +247 -247
- package/hive-pr-staleness.js +189 -189
- package/hive-pr.js +337 -0
- package/hive-privacy.js +327 -288
- package/hive-receipts.js +233 -233
- package/hive-relay-health.js +7 -2
- package/hive-secrets.js +89 -0
- package/hive-setup.js +548 -445
- package/hive-stdio.js +172 -168
- package/hive-stop-hook.js +290 -241
- package/hive-throttle.js +118 -118
- package/hive-update.js +58 -58
- package/hive-version.js +110 -59
- package/icr-merge.js +58 -58
- package/icr.js +121 -15
- package/lang-json.js +211 -211
- package/lang-ruby.js +230 -230
- package/lang-toml.js +136 -136
- package/lang-yaml.js +183 -183
- package/limits.js +413 -394
- package/package.json +19 -11
- package/reconcile.cjs +614 -477
- package/substrate.js +252 -252
- package/sync.js +4691 -3011
- package/grm-link.cjs +0 -141
package/lang-ruby.js
CHANGED
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
// Ruby language provider for ICR — keyword-nesting structural splitter (def/class/
|
|
2
|
-
// module … end). Same honest-heuristic contract as the brace/python providers: not a
|
|
3
|
-
// full parser, but conservative — anything it can't confidently structure falls back
|
|
4
|
-
// to the engine's safe line tier, so output is never MORE broken than the inputs.
|
|
5
|
-
//
|
|
6
|
-
// Structure model: a line-oriented scan tracking strings, comments (# and =begin/=end),
|
|
7
|
-
// bracket depth, and KEYWORD depth (def/class/module/if/while/… open; `end` closes).
|
|
8
|
-
// Top-level def/class/module blocks become keyed units; the intent layer (dangling
|
|
9
|
-
// refs, rename + call-site rewrite, token merge) rides on a lexer like Python's.
|
|
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
|
-
if (src[i] === '"' || src[i] === "'") {
|
|
16
|
-
const q = src[i]; let j = i + 1
|
|
17
|
-
while (j < n) { if (src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } j++ }
|
|
18
|
-
return j
|
|
19
|
-
}
|
|
20
|
-
return -1
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Per physical line: the line's CODE text (strings/comments blanked), its bracket
|
|
25
|
-
// delta, and byte range. =begin/=end block comments are whole-line constructs.
|
|
26
|
-
function codeLines(src) {
|
|
27
|
-
const at = scannerAt(src)
|
|
28
|
-
const out = []
|
|
29
|
-
let i = 0, inBlockComment = false
|
|
30
|
-
const n = src.length
|
|
31
|
-
while (i < n) {
|
|
32
|
-
let e = src.indexOf('\n', i); e = e < 0 ? n : e + 1
|
|
33
|
-
const raw = src.slice(i, e)
|
|
34
|
-
if (inBlockComment) {
|
|
35
|
-
if (/^=end\b/.test(raw)) inBlockComment = false
|
|
36
|
-
out.push({ start: i, end: e, code: '', brackets: 0 })
|
|
37
|
-
i = e; continue
|
|
38
|
-
}
|
|
39
|
-
if (/^=begin\b/.test(raw)) { inBlockComment = true; out.push({ start: i, end: e, code: '', brackets: 0 }); i = e; continue }
|
|
40
|
-
let code = '', k = i, brackets = 0
|
|
41
|
-
while (k < e) {
|
|
42
|
-
const j = at(k)
|
|
43
|
-
if (j > k) { if (src[k] !== '#') code += ' '; k = Math.min(e, j); continue }
|
|
44
|
-
const c = src[k]
|
|
45
|
-
if (c === '(' || c === '[' || c === '{') brackets++
|
|
46
|
-
else if (c === ')' || c === ']' || c === '}') brackets--
|
|
47
|
-
code += c; k++
|
|
48
|
-
}
|
|
49
|
-
out.push({ start: i, end: e, code, brackets })
|
|
50
|
-
i = e
|
|
51
|
-
}
|
|
52
|
-
return out
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Keyword depth delta for one line of CODE. Conservative rules for the ambiguous
|
|
56
|
-
// keywords (modifier `x if y` does not open; `while … do` opens once, not twice;
|
|
57
|
-
// endless `def foo = expr` does not open).
|
|
58
|
-
const OPENERS = new Set(['if', 'unless', 'while', 'until', 'case', 'begin', 'for'])
|
|
59
|
-
function keywordDelta(code) {
|
|
60
|
-
const words = code.match(/[A-Za-z_]\w*[?!]?/g) || []
|
|
61
|
-
let d = 0
|
|
62
|
-
const trimmed = code.trim()
|
|
63
|
-
const endless = /\bdef\s+[A-Za-z_][\w.?!]*(\([^)]*\))?\s*=(?!=)/.test(code)
|
|
64
|
-
const hasLoop = /\b(while|until|for)\b/.test(code)
|
|
65
|
-
for (let w = 0; w < words.length; w++) {
|
|
66
|
-
const word = words[w]
|
|
67
|
-
if (word === 'def') { if (!endless) d++ }
|
|
68
|
-
else if (word === 'class' || word === 'module') d++
|
|
69
|
-
else if (OPENERS.has(word)) {
|
|
70
|
-
// statement position only: first word of the line, or right after an assignment
|
|
71
|
-
const isFirst = trimmed.startsWith(word)
|
|
72
|
-
const afterAssign = new RegExp('=\\s*' + word + '\\b').test(code)
|
|
73
|
-
if (word === 'while' || word === 'until' || word === 'for') { if (isFirst) d++ }
|
|
74
|
-
else if (isFirst || afterAssign) d++
|
|
75
|
-
}
|
|
76
|
-
else if (word === 'do') { if (!hasLoop) d++ }
|
|
77
|
-
else if (word === 'end') d--
|
|
78
|
-
}
|
|
79
|
-
return d
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const HEAD = /^\s*(def)\s+(self\.)?([A-Za-z_][\w.?!]*)/
|
|
83
|
-
const CONT = /^\s*(class|module)\s+(?:<<\s*self|([A-Z]\w*(?:::[A-Z]\w*)*))/
|
|
84
|
-
|
|
85
|
-
function keyOf(text) {
|
|
86
|
-
let m = text.match(HEAD); if (m) return 'def:' + (m[2] || '') + m[3]
|
|
87
|
-
m = text.match(CONT); if (m) return m[1] + ':' + (m[2] || 'self')
|
|
88
|
-
return 'stmt:' + text.split('\n')[0].trim().replace(/\s+/g, ' ')
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Units over a region: a def/class/module head at keyword-depth 0 owns everything
|
|
92
|
-
// until its matching `end`; other depth-0 lines are statements (joined while
|
|
93
|
-
// brackets stay open).
|
|
94
|
-
function blocks(src) {
|
|
95
|
-
const L = codeLines(src)
|
|
96
|
-
const units = []
|
|
97
|
-
let i = 0
|
|
98
|
-
while (i < L.length) {
|
|
99
|
-
while (i < L.length && L[i].code.trim() === '') i++
|
|
100
|
-
if (i >= L.length) break
|
|
101
|
-
const start = L[i].start
|
|
102
|
-
const head = L[i].code
|
|
103
|
-
if (/^\s*(def|class|module)\b/.test(head)) {
|
|
104
|
-
let depth = 0, brackets = 0
|
|
105
|
-
do {
|
|
106
|
-
depth += keywordDelta(L[i].code)
|
|
107
|
-
brackets += L[i].brackets
|
|
108
|
-
i++
|
|
109
|
-
} while (i < L.length && (depth > 0 || brackets > 0))
|
|
110
|
-
units.push({ start, end: L[i - 1].end })
|
|
111
|
-
} else {
|
|
112
|
-
let brackets = L[i].brackets, depth = keywordDelta(L[i].code)
|
|
113
|
-
i++
|
|
114
|
-
while (i < L.length && (brackets > 0 || depth > 0)) { brackets += L[i].brackets; depth += keywordDelta(L[i].code); i++ }
|
|
115
|
-
units.push({ start, end: L[i - 1].end })
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return units
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function keyed(raw, src) {
|
|
122
|
-
const seen = new Map(), out = []
|
|
123
|
-
for (const u of raw) {
|
|
124
|
-
const text = src.slice(u.start, u.end)
|
|
125
|
-
let k = keyOf(text)
|
|
126
|
-
const c = (seen.get(k) || 0) + 1; seen.set(k, c)
|
|
127
|
-
if (c > 1) k += '#' + c
|
|
128
|
-
out.push({ key: k, text, start: u.start, end: u.end })
|
|
129
|
-
}
|
|
130
|
-
return out
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Approximate parse: brackets balanced AND keyword nesting closes exactly
|
|
134
|
-
// (never negative, ends at zero) AND no unterminated block comment.
|
|
135
|
-
function wellNested(src) {
|
|
136
|
-
const L = codeLines(src)
|
|
137
|
-
let depth = 0, brackets = 0
|
|
138
|
-
for (const l of L) {
|
|
139
|
-
depth += keywordDelta(l.code)
|
|
140
|
-
brackets += l.brackets
|
|
141
|
-
if (depth < 0 || brackets < 0) return false
|
|
142
|
-
}
|
|
143
|
-
return depth === 0 && brackets === 0
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Lexer for the intent layer + token merge (comments in gaps, strings one token).
|
|
147
|
-
function lexTokens(src) {
|
|
148
|
-
const at = scannerAt(src)
|
|
149
|
-
const out = []
|
|
150
|
-
const n = src.length
|
|
151
|
-
let i = 0
|
|
152
|
-
while (i < n) {
|
|
153
|
-
const c = src[i]
|
|
154
|
-
if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { i++; continue }
|
|
155
|
-
const j = at(i)
|
|
156
|
-
if (j > i) {
|
|
157
|
-
if (c === '#') { i = j; continue }
|
|
158
|
-
out.push({ start: i, end: j, k: src.slice(i, j) }); i = j; continue
|
|
159
|
-
}
|
|
160
|
-
if (/[A-Za-z_]/.test(c)) { let e = i + 1; while (e < n && /\w/.test(src[e])) e++; if (src[e] === '?' || src[e] === '!') e++; out.push({ start: i, end: e, k: src.slice(i, e) }); i = e; continue }
|
|
161
|
-
if (/[0-9]/.test(c)) { let e = i + 1; while (e < n && /[\w.]/.test(src[e])) e++; out.push({ start: i, end: e, k: src.slice(i, e) }); i = e; continue }
|
|
162
|
-
out.push({ start: i, end: i + 1, k: c }); i++
|
|
163
|
-
}
|
|
164
|
-
return out
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Reference position: not `.name` (method call on receiver) — `:name` symbols DO
|
|
168
|
-
// count (send(:helper) is a real reference; over-inclusion is the cautious direction).
|
|
169
|
-
function isRefToken(src, t) {
|
|
170
|
-
if (!/^[A-Za-z_]\w*[?!]?$/.test(t.k)) return false
|
|
171
|
-
let p = t.start - 1
|
|
172
|
-
while (p >= 0 && (src[p] === ' ' || src[p] === '\t')) p--
|
|
173
|
-
return !(p >= 0 && src[p] === '.')
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export const ruby = {
|
|
177
|
-
id: 'ruby',
|
|
178
|
-
exts: ['.rb', '.rake'],
|
|
179
|
-
parses: (src) => wellNested(String(src == null ? '' : src)),
|
|
180
|
-
parsesUnit: (src) => wellNested(String(src)),
|
|
181
|
-
units: (src) => keyed(blocks(String(src)), String(src)),
|
|
182
|
-
declaredNames: (src) => {
|
|
183
|
-
const names = new Set()
|
|
184
|
-
for (const u of keyed(blocks(String(src)), String(src))) {
|
|
185
|
-
const m = u.key.match(/^(?:def|class|module):(?:self\.)?([A-Za-z_][\w.?!]*)/)
|
|
186
|
-
if (m) names.add(m[1])
|
|
187
|
-
}
|
|
188
|
-
return names
|
|
189
|
-
},
|
|
190
|
-
usedIdentifiers: (src) => {
|
|
191
|
-
src = String(src)
|
|
192
|
-
const used = new Set()
|
|
193
|
-
for (const t of lexTokens(src)) if (isRefToken(src, t)) used.add(t.k)
|
|
194
|
-
return used
|
|
195
|
-
},
|
|
196
|
-
declBody: (src, name) => {
|
|
197
|
-
src = String(src)
|
|
198
|
-
for (const u of keyed(blocks(src), src)) {
|
|
199
|
-
const m = u.key.match(/^(?:def|class|module):(?:self\.)?([A-Za-z_][\w.?!]*)/)
|
|
200
|
-
if (!m || m[1] !== name) continue
|
|
201
|
-
return u.text.replace(name, '').trimEnd()
|
|
202
|
-
}
|
|
203
|
-
return null
|
|
204
|
-
},
|
|
205
|
-
renameRefs: (src, oldName, newName) => {
|
|
206
|
-
src = String(src)
|
|
207
|
-
const spots = []
|
|
208
|
-
for (const t of lexTokens(src)) if (t.k === oldName && isRefToken(src, t)) spots.push(t)
|
|
209
|
-
let out = src
|
|
210
|
-
for (const t of spots.reverse()) out = out.slice(0, t.start) + newName + out.slice(t.end)
|
|
211
|
-
return out
|
|
212
|
-
},
|
|
213
|
-
tokenize: (src) => lexTokens(String(src)),
|
|
214
|
-
// Descend into a class/module so two agents editing DIFFERENT methods merge.
|
|
215
|
-
splitUnit: (text) => {
|
|
216
|
-
const src = String(text)
|
|
217
|
-
if (!/^\s*(class|module)\b/.test(src)) return null
|
|
218
|
-
const L = codeLines(src)
|
|
219
|
-
if (!L.length) return null
|
|
220
|
-
const sigEnd = L[0].end
|
|
221
|
-
// body = everything between the header line and the final matching `end` line
|
|
222
|
-
let last = L.length - 1
|
|
223
|
-
while (last > 0 && L[last].code.trim() === '') last--
|
|
224
|
-
if (!/^\s*end\b/.test(L[last].code)) return null
|
|
225
|
-
const bodyStart = sigEnd, bodyEnd = L[last].start
|
|
226
|
-
const body = src.slice(bodyStart, bodyEnd)
|
|
227
|
-
const inner = keyed(blocks(body), body)
|
|
228
|
-
return { sig: src.slice(0, sigEnd), open: '', close: src.slice(bodyEnd), body, units: inner, join: '', spliceable: true }
|
|
229
|
-
},
|
|
230
|
-
}
|
|
1
|
+
// Ruby language provider for ICR — keyword-nesting structural splitter (def/class/
|
|
2
|
+
// module … end). Same honest-heuristic contract as the brace/python providers: not a
|
|
3
|
+
// full parser, but conservative — anything it can't confidently structure falls back
|
|
4
|
+
// to the engine's safe line tier, so output is never MORE broken than the inputs.
|
|
5
|
+
//
|
|
6
|
+
// Structure model: a line-oriented scan tracking strings, comments (# and =begin/=end),
|
|
7
|
+
// bracket depth, and KEYWORD depth (def/class/module/if/while/… open; `end` closes).
|
|
8
|
+
// Top-level def/class/module blocks become keyed units; the intent layer (dangling
|
|
9
|
+
// refs, rename + call-site rewrite, token merge) rides on a lexer like Python's.
|
|
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
|
+
if (src[i] === '"' || src[i] === "'") {
|
|
16
|
+
const q = src[i]; let j = i + 1
|
|
17
|
+
while (j < n) { if (src[j] === '\\') { j += 2; continue } if (src[j] === q) { j++; break } j++ }
|
|
18
|
+
return j
|
|
19
|
+
}
|
|
20
|
+
return -1
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Per physical line: the line's CODE text (strings/comments blanked), its bracket
|
|
25
|
+
// delta, and byte range. =begin/=end block comments are whole-line constructs.
|
|
26
|
+
function codeLines(src) {
|
|
27
|
+
const at = scannerAt(src)
|
|
28
|
+
const out = []
|
|
29
|
+
let i = 0, inBlockComment = false
|
|
30
|
+
const n = src.length
|
|
31
|
+
while (i < n) {
|
|
32
|
+
let e = src.indexOf('\n', i); e = e < 0 ? n : e + 1
|
|
33
|
+
const raw = src.slice(i, e)
|
|
34
|
+
if (inBlockComment) {
|
|
35
|
+
if (/^=end\b/.test(raw)) inBlockComment = false
|
|
36
|
+
out.push({ start: i, end: e, code: '', brackets: 0 })
|
|
37
|
+
i = e; continue
|
|
38
|
+
}
|
|
39
|
+
if (/^=begin\b/.test(raw)) { inBlockComment = true; out.push({ start: i, end: e, code: '', brackets: 0 }); i = e; continue }
|
|
40
|
+
let code = '', k = i, brackets = 0
|
|
41
|
+
while (k < e) {
|
|
42
|
+
const j = at(k)
|
|
43
|
+
if (j > k) { if (src[k] !== '#') code += ' '; k = Math.min(e, j); continue }
|
|
44
|
+
const c = src[k]
|
|
45
|
+
if (c === '(' || c === '[' || c === '{') brackets++
|
|
46
|
+
else if (c === ')' || c === ']' || c === '}') brackets--
|
|
47
|
+
code += c; k++
|
|
48
|
+
}
|
|
49
|
+
out.push({ start: i, end: e, code, brackets })
|
|
50
|
+
i = e
|
|
51
|
+
}
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Keyword depth delta for one line of CODE. Conservative rules for the ambiguous
|
|
56
|
+
// keywords (modifier `x if y` does not open; `while … do` opens once, not twice;
|
|
57
|
+
// endless `def foo = expr` does not open).
|
|
58
|
+
const OPENERS = new Set(['if', 'unless', 'while', 'until', 'case', 'begin', 'for'])
|
|
59
|
+
function keywordDelta(code) {
|
|
60
|
+
const words = code.match(/[A-Za-z_]\w*[?!]?/g) || []
|
|
61
|
+
let d = 0
|
|
62
|
+
const trimmed = code.trim()
|
|
63
|
+
const endless = /\bdef\s+[A-Za-z_][\w.?!]*(\([^)]*\))?\s*=(?!=)/.test(code)
|
|
64
|
+
const hasLoop = /\b(while|until|for)\b/.test(code)
|
|
65
|
+
for (let w = 0; w < words.length; w++) {
|
|
66
|
+
const word = words[w]
|
|
67
|
+
if (word === 'def') { if (!endless) d++ }
|
|
68
|
+
else if (word === 'class' || word === 'module') d++
|
|
69
|
+
else if (OPENERS.has(word)) {
|
|
70
|
+
// statement position only: first word of the line, or right after an assignment
|
|
71
|
+
const isFirst = trimmed.startsWith(word)
|
|
72
|
+
const afterAssign = new RegExp('=\\s*' + word + '\\b').test(code)
|
|
73
|
+
if (word === 'while' || word === 'until' || word === 'for') { if (isFirst) d++ }
|
|
74
|
+
else if (isFirst || afterAssign) d++
|
|
75
|
+
}
|
|
76
|
+
else if (word === 'do') { if (!hasLoop) d++ }
|
|
77
|
+
else if (word === 'end') d--
|
|
78
|
+
}
|
|
79
|
+
return d
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const HEAD = /^\s*(def)\s+(self\.)?([A-Za-z_][\w.?!]*)/
|
|
83
|
+
const CONT = /^\s*(class|module)\s+(?:<<\s*self|([A-Z]\w*(?:::[A-Z]\w*)*))/
|
|
84
|
+
|
|
85
|
+
function keyOf(text) {
|
|
86
|
+
let m = text.match(HEAD); if (m) return 'def:' + (m[2] || '') + m[3]
|
|
87
|
+
m = text.match(CONT); if (m) return m[1] + ':' + (m[2] || 'self')
|
|
88
|
+
return 'stmt:' + text.split('\n')[0].trim().replace(/\s+/g, ' ')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Units over a region: a def/class/module head at keyword-depth 0 owns everything
|
|
92
|
+
// until its matching `end`; other depth-0 lines are statements (joined while
|
|
93
|
+
// brackets stay open).
|
|
94
|
+
function blocks(src) {
|
|
95
|
+
const L = codeLines(src)
|
|
96
|
+
const units = []
|
|
97
|
+
let i = 0
|
|
98
|
+
while (i < L.length) {
|
|
99
|
+
while (i < L.length && L[i].code.trim() === '') i++
|
|
100
|
+
if (i >= L.length) break
|
|
101
|
+
const start = L[i].start
|
|
102
|
+
const head = L[i].code
|
|
103
|
+
if (/^\s*(def|class|module)\b/.test(head)) {
|
|
104
|
+
let depth = 0, brackets = 0
|
|
105
|
+
do {
|
|
106
|
+
depth += keywordDelta(L[i].code)
|
|
107
|
+
brackets += L[i].brackets
|
|
108
|
+
i++
|
|
109
|
+
} while (i < L.length && (depth > 0 || brackets > 0))
|
|
110
|
+
units.push({ start, end: L[i - 1].end })
|
|
111
|
+
} else {
|
|
112
|
+
let brackets = L[i].brackets, depth = keywordDelta(L[i].code)
|
|
113
|
+
i++
|
|
114
|
+
while (i < L.length && (brackets > 0 || depth > 0)) { brackets += L[i].brackets; depth += keywordDelta(L[i].code); i++ }
|
|
115
|
+
units.push({ start, end: L[i - 1].end })
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return units
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function keyed(raw, src) {
|
|
122
|
+
const seen = new Map(), out = []
|
|
123
|
+
for (const u of raw) {
|
|
124
|
+
const text = src.slice(u.start, u.end)
|
|
125
|
+
let k = keyOf(text)
|
|
126
|
+
const c = (seen.get(k) || 0) + 1; seen.set(k, c)
|
|
127
|
+
if (c > 1) k += '#' + c
|
|
128
|
+
out.push({ key: k, text, start: u.start, end: u.end })
|
|
129
|
+
}
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Approximate parse: brackets balanced AND keyword nesting closes exactly
|
|
134
|
+
// (never negative, ends at zero) AND no unterminated block comment.
|
|
135
|
+
function wellNested(src) {
|
|
136
|
+
const L = codeLines(src)
|
|
137
|
+
let depth = 0, brackets = 0
|
|
138
|
+
for (const l of L) {
|
|
139
|
+
depth += keywordDelta(l.code)
|
|
140
|
+
brackets += l.brackets
|
|
141
|
+
if (depth < 0 || brackets < 0) return false
|
|
142
|
+
}
|
|
143
|
+
return depth === 0 && brackets === 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Lexer for the intent layer + token merge (comments in gaps, strings one token).
|
|
147
|
+
function lexTokens(src) {
|
|
148
|
+
const at = scannerAt(src)
|
|
149
|
+
const out = []
|
|
150
|
+
const n = src.length
|
|
151
|
+
let i = 0
|
|
152
|
+
while (i < n) {
|
|
153
|
+
const c = src[i]
|
|
154
|
+
if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { i++; continue }
|
|
155
|
+
const j = at(i)
|
|
156
|
+
if (j > i) {
|
|
157
|
+
if (c === '#') { i = j; continue }
|
|
158
|
+
out.push({ start: i, end: j, k: src.slice(i, j) }); i = j; continue
|
|
159
|
+
}
|
|
160
|
+
if (/[A-Za-z_]/.test(c)) { let e = i + 1; while (e < n && /\w/.test(src[e])) e++; if (src[e] === '?' || src[e] === '!') e++; out.push({ start: i, end: e, k: src.slice(i, e) }); i = e; continue }
|
|
161
|
+
if (/[0-9]/.test(c)) { let e = i + 1; while (e < n && /[\w.]/.test(src[e])) e++; out.push({ start: i, end: e, k: src.slice(i, e) }); i = e; continue }
|
|
162
|
+
out.push({ start: i, end: i + 1, k: c }); i++
|
|
163
|
+
}
|
|
164
|
+
return out
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Reference position: not `.name` (method call on receiver) — `:name` symbols DO
|
|
168
|
+
// count (send(:helper) is a real reference; over-inclusion is the cautious direction).
|
|
169
|
+
function isRefToken(src, t) {
|
|
170
|
+
if (!/^[A-Za-z_]\w*[?!]?$/.test(t.k)) return false
|
|
171
|
+
let p = t.start - 1
|
|
172
|
+
while (p >= 0 && (src[p] === ' ' || src[p] === '\t')) p--
|
|
173
|
+
return !(p >= 0 && src[p] === '.')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const ruby = {
|
|
177
|
+
id: 'ruby',
|
|
178
|
+
exts: ['.rb', '.rake'],
|
|
179
|
+
parses: (src) => wellNested(String(src == null ? '' : src)),
|
|
180
|
+
parsesUnit: (src) => wellNested(String(src)),
|
|
181
|
+
units: (src) => keyed(blocks(String(src)), String(src)),
|
|
182
|
+
declaredNames: (src) => {
|
|
183
|
+
const names = new Set()
|
|
184
|
+
for (const u of keyed(blocks(String(src)), String(src))) {
|
|
185
|
+
const m = u.key.match(/^(?:def|class|module):(?:self\.)?([A-Za-z_][\w.?!]*)/)
|
|
186
|
+
if (m) names.add(m[1])
|
|
187
|
+
}
|
|
188
|
+
return names
|
|
189
|
+
},
|
|
190
|
+
usedIdentifiers: (src) => {
|
|
191
|
+
src = String(src)
|
|
192
|
+
const used = new Set()
|
|
193
|
+
for (const t of lexTokens(src)) if (isRefToken(src, t)) used.add(t.k)
|
|
194
|
+
return used
|
|
195
|
+
},
|
|
196
|
+
declBody: (src, name) => {
|
|
197
|
+
src = String(src)
|
|
198
|
+
for (const u of keyed(blocks(src), src)) {
|
|
199
|
+
const m = u.key.match(/^(?:def|class|module):(?:self\.)?([A-Za-z_][\w.?!]*)/)
|
|
200
|
+
if (!m || m[1] !== name) continue
|
|
201
|
+
return u.text.replace(name, '').trimEnd()
|
|
202
|
+
}
|
|
203
|
+
return null
|
|
204
|
+
},
|
|
205
|
+
renameRefs: (src, oldName, newName) => {
|
|
206
|
+
src = String(src)
|
|
207
|
+
const spots = []
|
|
208
|
+
for (const t of lexTokens(src)) if (t.k === oldName && isRefToken(src, t)) spots.push(t)
|
|
209
|
+
let out = src
|
|
210
|
+
for (const t of spots.reverse()) out = out.slice(0, t.start) + newName + out.slice(t.end)
|
|
211
|
+
return out
|
|
212
|
+
},
|
|
213
|
+
tokenize: (src) => lexTokens(String(src)),
|
|
214
|
+
// Descend into a class/module so two agents editing DIFFERENT methods merge.
|
|
215
|
+
splitUnit: (text) => {
|
|
216
|
+
const src = String(text)
|
|
217
|
+
if (!/^\s*(class|module)\b/.test(src)) return null
|
|
218
|
+
const L = codeLines(src)
|
|
219
|
+
if (!L.length) return null
|
|
220
|
+
const sigEnd = L[0].end
|
|
221
|
+
// body = everything between the header line and the final matching `end` line
|
|
222
|
+
let last = L.length - 1
|
|
223
|
+
while (last > 0 && L[last].code.trim() === '') last--
|
|
224
|
+
if (!/^\s*end\b/.test(L[last].code)) return null
|
|
225
|
+
const bodyStart = sigEnd, bodyEnd = L[last].start
|
|
226
|
+
const body = src.slice(bodyStart, bodyEnd)
|
|
227
|
+
const inner = keyed(blocks(body), body)
|
|
228
|
+
return { sig: src.slice(0, sigEnd), open: '', close: src.slice(bodyEnd), body, units: inner, join: '', spliceable: true }
|
|
229
|
+
},
|
|
230
|
+
}
|