hivecode-mcp 0.11.6 → 0.12.1
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 +127 -127
- package/README.md +119 -69
- package/core.js +14 -146
- package/docs/MCP.md +19 -11
- package/docs/SELF_HOSTING.md +256 -0
- package/hive-activity.js +310 -0
- package/hive-mcp.js +447 -76
- package/hive-mux-proto.js +58 -0
- package/hive-mux.js +350 -0
- package/hive-pr-staleness.js +189 -0
- package/hive-privacy.js +288 -0
- package/hive-relay-health.js +225 -0
- package/hive-setup.js +445 -0
- package/hive-stdio.js +168 -0
- package/hive-update.js +58 -0
- package/hive-version.js +59 -0
- package/lang-js.js +11 -2
- package/limits.js +49 -1
- package/package.json +29 -7
- package/reconcile.cjs +477 -0
- package/sync.js +613 -43
package/reconcile.cjs
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// reconcile.cjs — Single source of truth for the 3-way reconcile & merge engine.
|
|
2
|
+
//
|
|
3
|
+
// CommonJS on purpose: sync.js is ESM and the VS Code extension is CJS.
|
|
4
|
+
// A .cjs file is the one shape both can load synchronously without duplication.
|
|
5
|
+
//
|
|
6
|
+
// Acceptance criteria (HIV-8), re-checked 21 Sep 2026 -- FOUR OF FIVE MET:
|
|
7
|
+
// 1. MET one module exports reconcile() and its dependencies (11 exports).
|
|
8
|
+
// 2. MET core.js re-exports from here and sync.js imports core.js. One copy.
|
|
9
|
+
// 3. NOT MET extension/extension.js does NOT require this file. It requires
|
|
10
|
+
// ./icr.cjs (the ICR bundle) and keeps its OWN reconcile() at
|
|
11
|
+
// extension/extension.js:1166. The second copy is still there.
|
|
12
|
+
// 4. MET node tests/test.js -- 95 passed, 0 failed.
|
|
13
|
+
// 5. MET node tests/conflict-test.mjs -- ALL 4 PASS.
|
|
14
|
+
//
|
|
15
|
+
// Criterion 3 read "No second copy anywhere" while a second copy existed, in a file
|
|
16
|
+
// that ships in package.json files[]. A comment asserting an invariant it does not
|
|
17
|
+
// hold is worse than no comment: the next reader trusts it and stops looking. Corrected
|
|
18
|
+
// to what is true rather than deleted, because the gap is the useful part.
|
|
19
|
+
//
|
|
20
|
+
// WHAT ACTUALLY GUARDS THE DRIFT, since it is not this file:
|
|
21
|
+
// tests/extension-parity-test.mjs the ICR bundle vs the engine, 24/24
|
|
22
|
+
// tests/extension-reconcile-characterisation-test.mjs pins the extension's own copy
|
|
23
|
+
// Those two measure that the copies AGREE. They do not make it one copy, and closing
|
|
24
|
+
// criterion 3 for real means the extension loading this module.
|
|
25
|
+
|
|
26
|
+
const fs = require('fs')
|
|
27
|
+
const path = require('path')
|
|
28
|
+
|
|
29
|
+
// Minimal text diff: replace only the changed middle, preserving the common
|
|
30
|
+
// prefix/suffix. Applied to a Y.Text so edits stay small (good for CRDT churn).
|
|
31
|
+
function applyDiff(ytext, next) {
|
|
32
|
+
const cur = ytext.toString()
|
|
33
|
+
if (cur === next) return false
|
|
34
|
+
let p = 0
|
|
35
|
+
while (p < cur.length && p < next.length && cur[p] === next[p]) p++
|
|
36
|
+
let s = 0
|
|
37
|
+
while (s < cur.length - p && s < next.length - p && cur[cur.length - 1 - s] === next[next.length - 1 - s]) s++
|
|
38
|
+
if (cur.length - p - s > 0) ytext.delete(p, cur.length - p - s)
|
|
39
|
+
const ins = next.slice(p, next.length - s)
|
|
40
|
+
if (ins) ytext.insert(p, ins)
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Detect REAL git-style conflict markers in a file. Line-anchored on purpose.
|
|
45
|
+
function hasConflictMarkers(text) {
|
|
46
|
+
return /^<<<<<<< /m.test(text || '') && /^>>>>>>> /m.test(text || '')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Strip and de-nest conflict markers to prevent exponential 2^N code ballooning
|
|
50
|
+
function stripConflictMarkers(text, prefer = 'local') {
|
|
51
|
+
if (!text || !hasConflictMarkers(text)) return text || ''
|
|
52
|
+
const lines = text.split('\n')
|
|
53
|
+
const out = []
|
|
54
|
+
let inConflict = false
|
|
55
|
+
let inTheirs = false
|
|
56
|
+
const localBlock = []
|
|
57
|
+
const theirsBlock = []
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const line = lines[i]
|
|
61
|
+
if (/^<<<<<<< /.test(line)) {
|
|
62
|
+
inConflict = true
|
|
63
|
+
inTheirs = false
|
|
64
|
+
localBlock.length = 0
|
|
65
|
+
theirsBlock.length = 0
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (/^=======$/.test(line) && inConflict) {
|
|
69
|
+
inTheirs = true
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
if (/^>>>>>>> /.test(line) && inConflict) {
|
|
73
|
+
inConflict = false
|
|
74
|
+
out.push(...(prefer === 'theirs' ? theirsBlock : localBlock))
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (inConflict) {
|
|
78
|
+
if (inTheirs) theirsBlock.push(line)
|
|
79
|
+
else localBlock.push(line)
|
|
80
|
+
} else {
|
|
81
|
+
out.push(line)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out.join('\n')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Which base line-range an edit replaced, and the replacement lines.
|
|
88
|
+
function changedRange(base, other) {
|
|
89
|
+
let p = 0
|
|
90
|
+
while (p < base.length && p < other.length && base[p] === other[p]) p++
|
|
91
|
+
let s = 0
|
|
92
|
+
while (s < base.length - p && s < other.length - p && base[base.length - 1 - s] === other[other.length - 1 - s]) s++
|
|
93
|
+
return { startBase: p, endBase: base.length - s, newLines: other.slice(p, other.length - s) }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Best-effort "what was defined here" across common languages — for the board.
|
|
97
|
+
function symbolsIn(text) {
|
|
98
|
+
const names = new Set()
|
|
99
|
+
const add = (re) => { let m; while ((m = re.exec(text)) && names.size < 8) names.add(m[1]) }
|
|
100
|
+
add(/\bfunction\s+(\w+)/g)
|
|
101
|
+
add(/\bclass\s+(\w+)/g)
|
|
102
|
+
add(/\bdef\s+(\w+)/g)
|
|
103
|
+
add(/\b(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/g)
|
|
104
|
+
return [...names].slice(0, 8)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Classify a change as a small PATCH vs wholesale REWRITE.
|
|
108
|
+
function summarizeChange(base, next) {
|
|
109
|
+
const b = (base || '').split('\n')
|
|
110
|
+
const n = (next || '').split('\n')
|
|
111
|
+
if (!base) return { isRewrite: false, changedLines: n.length, totalLines: n.length, symbols: symbolsIn(next || '') }
|
|
112
|
+
const r = changedRange(b, n)
|
|
113
|
+
const churn = Math.max(r.endBase - r.startBase, r.newLines.length)
|
|
114
|
+
const total = Math.max(b.length, n.length)
|
|
115
|
+
const isRewrite = total > 0 && churn >= 4 && churn / total >= 0.5
|
|
116
|
+
return { isRewrite, changedLines: churn, totalLines: total, symbols: symbolsIn(r.newLines.join('\n')) }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 3-way merge for the sync layer (disk <-> shared doc)
|
|
120
|
+
function merge3(base, mine, theirs) {
|
|
121
|
+
if (mine === theirs) return { text: mine, conflict: false }
|
|
122
|
+
if (base === theirs) return { text: mine, conflict: false }
|
|
123
|
+
if (base === mine) return { text: theirs, conflict: false }
|
|
124
|
+
|
|
125
|
+
const cleanBase = hasConflictMarkers(base) ? stripConflictMarkers(base, 'local') : (base || '')
|
|
126
|
+
const cleanMine = hasConflictMarkers(mine) ? stripConflictMarkers(mine, 'local') : (mine || '')
|
|
127
|
+
const cleanTheirs = hasConflictMarkers(theirs) ? stripConflictMarkers(theirs, 'theirs') : (theirs || '')
|
|
128
|
+
|
|
129
|
+
if (cleanMine === cleanTheirs) return { text: cleanMine, conflict: false }
|
|
130
|
+
if (cleanBase === cleanTheirs) return { text: cleanMine, conflict: false }
|
|
131
|
+
if (cleanBase === cleanMine) return { text: cleanTheirs, conflict: false }
|
|
132
|
+
|
|
133
|
+
const b = cleanBase.split('\n')
|
|
134
|
+
const mr = changedRange(b, cleanMine.split('\n'))
|
|
135
|
+
const tr = changedRange(b, cleanTheirs.split('\n'))
|
|
136
|
+
if (mr.endBase <= tr.startBase || tr.endBase <= mr.startBase) {
|
|
137
|
+
const out = b.slice()
|
|
138
|
+
for (const e of [mr, tr].sort((x, y) => y.startBase - x.startBase)) {
|
|
139
|
+
out.splice(e.startBase, e.endBase - e.startBase, ...e.newLines)
|
|
140
|
+
}
|
|
141
|
+
return { text: out.join('\n'), conflict: false }
|
|
142
|
+
}
|
|
143
|
+
const start = Math.min(mr.startBase, tr.startBase)
|
|
144
|
+
const end = Math.max(mr.endBase, tr.endBase)
|
|
145
|
+
const mineBlock = [...b.slice(start, mr.startBase), ...mr.newLines, ...b.slice(mr.endBase, end)]
|
|
146
|
+
const theirsBlock = [...b.slice(start, tr.startBase), ...tr.newLines, ...b.slice(tr.endBase, end)]
|
|
147
|
+
const out = [
|
|
148
|
+
...b.slice(0, start),
|
|
149
|
+
'<<<<<<< local (yours)',
|
|
150
|
+
...mineBlock,
|
|
151
|
+
'=======',
|
|
152
|
+
...theirsBlock,
|
|
153
|
+
'>>>>>>> incoming (theirs)',
|
|
154
|
+
...b.slice(end),
|
|
155
|
+
]
|
|
156
|
+
return { text: out.join('\n'), conflict: true }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function describeConflicts(cs) {
|
|
160
|
+
return (cs || []).map((c) =>
|
|
161
|
+
c.startsWith('ref:') ? `'${c.slice(4)}' was removed or renamed but is still used`
|
|
162
|
+
: c.startsWith('fn:') ? `both sides changed function ${c.slice(3)}`
|
|
163
|
+
: c.startsWith('class:') ? `both sides changed class ${c.slice(6)}`
|
|
164
|
+
: `both sides changed ${c.replace(/^\w+:/, '')}`).join('; ')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function icrMerge3(base, mine, theirs, relPath, icrlib) {
|
|
168
|
+
const lm = merge3(base, mine, theirs)
|
|
169
|
+
if (!icrlib || !icrlib.supports || !icrlib.supports(relPath)) return lm
|
|
170
|
+
let r
|
|
171
|
+
try { r = icrlib.structuralMerge(base, mine, theirs, { filename: relPath }) } catch (e) { return lm }
|
|
172
|
+
if (r.status === 'auto') {
|
|
173
|
+
const renamed = r.renames && r.renames.length
|
|
174
|
+
return { text: r.text, conflict: false, icr: renamed ? 'rename' : 'structural', icrWarning: renamed ? `auto-applied rename ${r.renames.join(', ')}` : undefined }
|
|
175
|
+
}
|
|
176
|
+
if (r.status === 'semantic-conflict') return Object.assign({}, lm, { icrWarning: describeConflicts(r.conflicts) })
|
|
177
|
+
return lm
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// createReconciler: binds to a session's maps, state, and callbacks, returning
|
|
181
|
+
// a reconcile(relPath, origin = 'local') function.
|
|
182
|
+
function createReconciler(ctx) {
|
|
183
|
+
const {
|
|
184
|
+
root,
|
|
185
|
+
name = 'anon',
|
|
186
|
+
SKIP = new Set(['HIVE_BOARD.md', 'HIVE_CHAT.md', 'HIVE_TASKS.md', 'HIVE_RULES.md', 'HIVE_MEMBERS.md', 'HIVE_FOR_AGENTS.md', '.hive.json']),
|
|
187
|
+
isIgnored = () => false,
|
|
188
|
+
canOpen = () => true,
|
|
189
|
+
canWrite = () => true,
|
|
190
|
+
readText = (full) => {
|
|
191
|
+
try {
|
|
192
|
+
const buf = fs.readFileSync(full)
|
|
193
|
+
if (buf.length > 1_000_000 || buf.includes(0)) return null
|
|
194
|
+
return buf.toString('utf8')
|
|
195
|
+
} catch { return null }
|
|
196
|
+
},
|
|
197
|
+
writeToDisk,
|
|
198
|
+
openFile,
|
|
199
|
+
touchFile,
|
|
200
|
+
fileDocs,
|
|
201
|
+
manifest,
|
|
202
|
+
bases,
|
|
203
|
+
forkBases,
|
|
204
|
+
known,
|
|
205
|
+
mtimes,
|
|
206
|
+
conflicted = new Set(),
|
|
207
|
+
icrWarned = new Map(),
|
|
208
|
+
snappedBase = new Set(),
|
|
209
|
+
preexisting = new Set(),
|
|
210
|
+
priorShadow,
|
|
211
|
+
shadow,
|
|
212
|
+
say = () => {},
|
|
213
|
+
log = () => {},
|
|
214
|
+
markEditing = () => {},
|
|
215
|
+
noteCoEditing = () => {},
|
|
216
|
+
noteIfRewrite = () => {},
|
|
217
|
+
captureSnapshot = () => {},
|
|
218
|
+
lastLocalEdit,
|
|
219
|
+
// Fork-gate & provenance
|
|
220
|
+
FORK_GATE = false,
|
|
221
|
+
captureLocal,
|
|
222
|
+
forkRecord,
|
|
223
|
+
forkBlock,
|
|
224
|
+
detectFork,
|
|
225
|
+
renderConflict,
|
|
226
|
+
adoptResolved,
|
|
227
|
+
stampProvenance,
|
|
228
|
+
attestHead,
|
|
229
|
+
fusionReason,
|
|
230
|
+
supports,
|
|
231
|
+
languageFor,
|
|
232
|
+
icrlib
|
|
233
|
+
} = ctx
|
|
234
|
+
|
|
235
|
+
return function reconcile(relPath, origin = 'local') {
|
|
236
|
+
if (SKIP.has(relPath) || isIgnored(relPath)) return
|
|
237
|
+
if (!canOpen(relPath)) return
|
|
238
|
+
const full = path.join(root, relPath)
|
|
239
|
+
const exists = fs.existsSync(full)
|
|
240
|
+
const disk = exists ? readText(full) : null
|
|
241
|
+
if (exists && disk === null) return
|
|
242
|
+
|
|
243
|
+
if (origin === 'local' && !canWrite(relPath)) {
|
|
244
|
+
const fe2 = fileDocs.get(relPath)
|
|
245
|
+
if (fe2 && fe2.synced) {
|
|
246
|
+
const dt = fe2.text.toString()
|
|
247
|
+
if (dt && dt !== disk) writeToDisk(relPath, dt)
|
|
248
|
+
}
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let fe = fileDocs.get(relPath)
|
|
253
|
+
if (!fe) { if (disk === null) return; openFile(relPath); return }
|
|
254
|
+
if (!fe.synced) return
|
|
255
|
+
if (touchFile) touchFile(relPath)
|
|
256
|
+
|
|
257
|
+
if (FORK_GATE && captureLocal) captureLocal(relPath)
|
|
258
|
+
|
|
259
|
+
if (FORK_GATE && fe.conflict && forkRecord) {
|
|
260
|
+
const rec = forkRecord(fe)
|
|
261
|
+
if (rec) {
|
|
262
|
+
const block = forkBlock(fe, rec)
|
|
263
|
+
if (origin === 'local') {
|
|
264
|
+
if (disk === block) return
|
|
265
|
+
const fork0 = forkBases.get(relPath)
|
|
266
|
+
const sawMarkers = fork0 === block || hasConflictMarkers(fork0 || '')
|
|
267
|
+
if (sawMarkers && disk !== null && !hasConflictMarkers(disk)) {
|
|
268
|
+
fe.doc.transact(() => {
|
|
269
|
+
applyDiff(fe.text, disk)
|
|
270
|
+
for (const v of rec.versions) fe.forkmark.set(v.hash, 1)
|
|
271
|
+
for (const key of [...fe.conflict.keys()]) fe.conflict.delete(key)
|
|
272
|
+
})
|
|
273
|
+
if (stampProvenance) stampProvenance(relPath, block || '', disk)
|
|
274
|
+
conflicted.delete(relPath)
|
|
275
|
+
known.add(relPath); bases.set(relPath, disk); forkBases.set(relPath, disk)
|
|
276
|
+
try { say(`✓ conflict in ${relPath} resolved by ${name}.`) } catch { }
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
if (!sawMarkers && disk !== null && disk !== fork0) {
|
|
280
|
+
if (detectFork) detectFork(relPath)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
if (renderConflict) renderConflict(relPath)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
if (renderConflict) renderConflict(relPath)
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
if (disk !== null && hasConflictMarkers(disk) && !hasConflictMarkers(fe.text.toString())) {
|
|
290
|
+
if (adoptResolved) { adoptResolved(relPath); return }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const docText = fe.text.toString()
|
|
295
|
+
const noHistory = !bases.has(relPath) && !(priorShadow && priorShadow.has(relPath))
|
|
296
|
+
if (docText === '' && (!manifest.has(relPath) || (disk && noHistory))) {
|
|
297
|
+
if (disk === null) return
|
|
298
|
+
fe.doc.transact(() => applyDiff(fe.text, disk))
|
|
299
|
+
if (stampProvenance) stampProvenance(relPath, '', disk)
|
|
300
|
+
manifest.set(relPath, 1)
|
|
301
|
+
known.add(relPath); bases.set(relPath, disk); forkBases.set(relPath, disk)
|
|
302
|
+
if (shadow) shadow.set(relPath, disk)
|
|
303
|
+
try { mtimes.set(relPath, fs.statSync(full).mtimeMs) } catch { }
|
|
304
|
+
captureSnapshot(relPath, disk, name, { force: true, kind: 'base', label: 'created' })
|
|
305
|
+
snappedBase.add(relPath)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const yt = fe.text
|
|
310
|
+
if (disk === null) {
|
|
311
|
+
writeToDisk(relPath, docText)
|
|
312
|
+
bases.set(relPath, docText); forkBases.set(relPath, docText)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (disk === docText) {
|
|
317
|
+
if (origin === 'local' && !known.has(relPath) && disk) markEditing(relPath)
|
|
318
|
+
known.add(relPath); bases.set(relPath, disk)
|
|
319
|
+
if (shadow) shadow.set(relPath, disk)
|
|
320
|
+
if (!forkBases.has(relPath) || origin === 'local') forkBases.set(relPath, disk)
|
|
321
|
+
if (!snappedBase.has(relPath) && disk) {
|
|
322
|
+
captureSnapshot(relPath, disk, name, { force: true, kind: 'base', label: 'baseline' })
|
|
323
|
+
snappedBase.add(relPath)
|
|
324
|
+
}
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// FIRST-CONTACT ADOPT
|
|
329
|
+
if (!bases.has(relPath) && docText !== '' && disk !== docText) {
|
|
330
|
+
if (!preexisting.has(relPath)) {
|
|
331
|
+
const nl = (s) => (s === '' || s.endsWith('\n') ? s : s + '\n')
|
|
332
|
+
const r0 = icrMerge3('', nl(disk), nl(docText), relPath, icrlib)
|
|
333
|
+
if (r0.text !== docText) fe.doc.transact(() => applyDiff(yt, r0.text))
|
|
334
|
+
if (r0.text !== disk) writeToDisk(relPath, r0.text)
|
|
335
|
+
if (r0.conflict) conflicted.add(relPath)
|
|
336
|
+
bases.set(relPath, r0.text); forkBases.set(relPath, r0.text)
|
|
337
|
+
if (shadow) shadow.set(relPath, r0.text)
|
|
338
|
+
if (!manifest.has(relPath)) manifest.set(relPath, 1)
|
|
339
|
+
try { say(`⚠ ${relPath} was created by two participants at once — both versions were merged${r0.conflict ? ', and CONFLICT markers need resolving' : ''}.`) } catch { }
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
const anc = priorShadow ? priorShadow.get(relPath) : undefined
|
|
343
|
+
if (anc !== undefined && disk !== null) {
|
|
344
|
+
if (disk === anc) {
|
|
345
|
+
writeToDisk(relPath, docText)
|
|
346
|
+
bases.set(relPath, docText); forkBases.set(relPath, docText)
|
|
347
|
+
if (!snappedBase.has(relPath)) {
|
|
348
|
+
captureSnapshot(relPath, docText, name, { force: true, kind: 'base', label: 'room copy adopted on rejoin (local was unchanged)' })
|
|
349
|
+
snappedBase.add(relPath)
|
|
350
|
+
}
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
captureSnapshot(relPath, disk, name, { force: true, kind: 'manual', label: 'local copy before rejoin merge' })
|
|
354
|
+
const rj = icrMerge3(anc, disk, docText, relPath, icrlib)
|
|
355
|
+
if (rj.text !== docText) fe.doc.transact(() => applyDiff(yt, rj.text))
|
|
356
|
+
if (rj.text !== disk) writeToDisk(relPath, rj.text)
|
|
357
|
+
if (rj.conflict) conflicted.add(relPath)
|
|
358
|
+
bases.set(relPath, rj.text); forkBases.set(relPath, rj.text)
|
|
359
|
+
if (shadow) shadow.set(relPath, rj.text)
|
|
360
|
+
if (!manifest.has(relPath)) manifest.set(relPath, 1)
|
|
361
|
+
try { say(`⚠ ${relPath} changed locally while ${name} was disconnected — the local edit and the room copy were merged${rj.conflict ? ', and CONFLICT markers need resolving' : ''}. Pre-merge local copy is in the file's history.`) } catch { }
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
captureSnapshot(relPath, disk, name, { force: true, kind: 'manual', label: 'local copy at join (room copy adopted)' })
|
|
365
|
+
writeToDisk(relPath, docText)
|
|
366
|
+
bases.set(relPath, docText); forkBases.set(relPath, docText)
|
|
367
|
+
if (!snappedBase.has(relPath)) {
|
|
368
|
+
captureSnapshot(relPath, docText, name, { force: true, kind: 'base', label: 'adopted from room at join' })
|
|
369
|
+
snappedBase.add(relPath)
|
|
370
|
+
}
|
|
371
|
+
try { say(`↓ ${relPath} already existed in this room — ${name} adopted the room's copy. The local version before joining is saved in the file's history.`) } catch { }
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const base = bases.has(relPath) ? bases.get(relPath) : disk
|
|
376
|
+
const fork = forkBases.has(relPath) ? forkBases.get(relPath) : base
|
|
377
|
+
if (origin === 'local') { noteCoEditing(relPath); markEditing(relPath) }
|
|
378
|
+
|
|
379
|
+
let res, reAdded = false
|
|
380
|
+
if (origin === 'local' && hasConflictMarkers(docText) && !hasConflictMarkers(disk)) {
|
|
381
|
+
res = { text: disk, conflict: false }
|
|
382
|
+
} else if (origin === 'local') {
|
|
383
|
+
noteIfRewrite(relPath, fork, disk)
|
|
384
|
+
const sc = summarizeChange(fork, disk)
|
|
385
|
+
const isCleanRewrite = sc.isRewrite && supports && supports(relPath) && languageFor && languageFor(relPath).parses(disk)
|
|
386
|
+
const theirsNew = changedRange(fork.split('\n'), docText.split('\n')).newLines.filter((l) => l.length)
|
|
387
|
+
const mineLines = new Set(disk.split('\n'))
|
|
388
|
+
const integrated = theirsNew.every((l) => mineLines.has(l))
|
|
389
|
+
if (integrated || (isCleanRewrite && !hasConflictMarkers(docText))) {
|
|
390
|
+
res = { text: disk, conflict: false }
|
|
391
|
+
} else {
|
|
392
|
+
res = icrMerge3(fork, disk, docText, relPath, icrlib)
|
|
393
|
+
reAdded = !res.conflict && res.text !== disk
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
res = icrMerge3(base, disk, docText, relPath, icrlib)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (res && !res.conflict && fusionReason) {
|
|
400
|
+
const why = fusionReason(res.text, disk, docText, (supports && supports(relPath)) ? languageFor(relPath) : null)
|
|
401
|
+
if (why) {
|
|
402
|
+
conflicted.add(relPath)
|
|
403
|
+
if (icrWarned.get(relPath) !== why) {
|
|
404
|
+
icrWarned.set(relPath, why)
|
|
405
|
+
log(`[${name}] !! REFUSED a fused merge of ${relPath}: ${why}`)
|
|
406
|
+
try {
|
|
407
|
+
say(`[!] @all ${relPath}: a merge was REFUSED because it fused rather than merged — ${why}. Nothing was written: the room's copy and ${name}'s local copy are both intact and unchanged. Someone must reconcile them deliberately; a plain file write will merge against the other version again, hive_edit replaces it outright.`)
|
|
408
|
+
} catch { }
|
|
409
|
+
}
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (origin === 'local' && res.text !== docText) {
|
|
415
|
+
const sc = summarizeChange(docText, res.text)
|
|
416
|
+
captureSnapshot(relPath, docText, name, { force: sc.isRewrite, churn: `${sc.changedLines}/${sc.totalLines} lines` })
|
|
417
|
+
if (typeof lastLocalEdit === 'function') lastLocalEdit(relPath)
|
|
418
|
+
}
|
|
419
|
+
fe.doc.transact(() => applyDiff(yt, res.text))
|
|
420
|
+
if (origin === 'local') {
|
|
421
|
+
if (FORK_GATE) { if (res.text !== docText && attestHead) attestHead(relPath, res.text, docText) }
|
|
422
|
+
else if (res.text !== docText && stampProvenance) stampProvenance(relPath, docText, res.text)
|
|
423
|
+
}
|
|
424
|
+
if (res.text !== disk) writeToDisk(relPath, res.text)
|
|
425
|
+
known.add(relPath); bases.set(relPath, res.text)
|
|
426
|
+
if (origin === 'local') forkBases.set(relPath, res.text)
|
|
427
|
+
else if (!forkBases.get(relPath)) forkBases.set(relPath, res.text)
|
|
428
|
+
|
|
429
|
+
const hasMarkers = res.conflict || hasConflictMarkers(res.text)
|
|
430
|
+
if (hasMarkers && !conflicted.has(relPath)) {
|
|
431
|
+
conflicted.add(relPath)
|
|
432
|
+
log(`[${name}] ⚠ merge conflict in ${relPath} — kept BOTH versions with <<<<<<< markers`)
|
|
433
|
+
try { say(`⚠ MERGE CONFLICT in ${relPath} — it has <<<<<<< markers. Whoever owns it: resolve before continuing.`) } catch { }
|
|
434
|
+
} else if (!hasMarkers && conflicted.has(relPath)) {
|
|
435
|
+
conflicted.delete(relPath)
|
|
436
|
+
try { say(`✓ conflict in ${relPath} resolved.`) } catch { }
|
|
437
|
+
} else if (reAdded) {
|
|
438
|
+
log(`[${name}] protected ${relPath}: your edit was based on an older version — re-added changes that arrived since (nobody's work lost)`)
|
|
439
|
+
try { say(`↺ ${relPath}: ${name}'s edit was based on an older copy — kept the changes that landed in between (nothing lost).`) } catch { }
|
|
440
|
+
} else if (!hasMarkers && res.icr === 'rename') {
|
|
441
|
+
log(`[${name}] ICR merged ${relPath} (detected a rename and updated the call sites)`)
|
|
442
|
+
} else if (!hasMarkers && res.icr === 'structural') {
|
|
443
|
+
log(`[${name}] ICR merged ${relPath} structurally (both edits kept, syntax guaranteed)`)
|
|
444
|
+
} else if (!hasMarkers) {
|
|
445
|
+
log(`[${name}] merged ${relPath} (both edits kept)`)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (res.icrWarning && icrWarned.get(relPath) !== res.icrWarning) {
|
|
449
|
+
icrWarned.set(relPath, res.icrWarning)
|
|
450
|
+
log(`[${name}] ⚠ ICR flag in ${relPath} — ${res.icrWarning}`)
|
|
451
|
+
try { say(`⚠ ICR flag in ${relPath}: ${res.icrWarning}. It merged, but review — this is a break a plain merge can't see.`) } catch { }
|
|
452
|
+
} else if (!res.icrWarning && icrWarned.has(relPath)) {
|
|
453
|
+
icrWarned.delete(relPath)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (detectFork) detectFork(relPath)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// reconcile direct call
|
|
461
|
+
function reconcile(ctx, relPath, origin = 'local') {
|
|
462
|
+
return createReconciler(ctx)(relPath, origin)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
reconcile,
|
|
467
|
+
createReconciler,
|
|
468
|
+
applyDiff,
|
|
469
|
+
hasConflictMarkers,
|
|
470
|
+
stripConflictMarkers,
|
|
471
|
+
changedRange,
|
|
472
|
+
symbolsIn,
|
|
473
|
+
summarizeChange,
|
|
474
|
+
merge3,
|
|
475
|
+
describeConflicts,
|
|
476
|
+
icrMerge3
|
|
477
|
+
}
|