ultracode 5.3.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/analysis-tool-handlers-H2RXLDPX.js +817 -0
- package/dist/chunks/analysis-tool-handlers-RJZAR6VT.js +817 -0
- package/dist/chunks/analysis-tool-handlers-Z2RF24T7.js +13 -0
- package/dist/chunks/autodoc-tool-handlers-CV5JEQUA.js +1112 -0
- package/dist/chunks/autodoc-tool-handlers-EHTNCH6I.js +1112 -0
- package/dist/chunks/autodoc-tool-handlers-MECXQJ2K.js +138 -0
- package/dist/chunks/chaos-CO7TOBOJ.js +18 -0
- package/dist/chunks/chaos-VM2PXERO.js +1573 -0
- package/dist/chunks/chaos-W3XRVJ7K.js +1564 -0
- package/dist/chunks/chunk-6K37BWK5.js +439 -0
- package/dist/chunks/chunk-EALTCYHZ.js +10 -0
- package/dist/chunks/chunk-FTBE7VMY.js +316 -0
- package/dist/chunks/chunk-KBW6LRQP.js +322 -0
- package/dist/chunks/chunk-NKUHX4CU.js +5 -0
- package/dist/chunks/chunk-NZFF4DQ4.js +3179 -0
- package/dist/chunks/chunk-RGP5UVQ7.js +3179 -0
- package/dist/chunks/chunk-RMZXFGQZ.js +322 -0
- package/dist/chunks/chunk-UG44F23Y.js +316 -0
- package/dist/chunks/chunk-V2SCB5H5.js +4403 -0
- package/dist/chunks/chunk-V6JAQNM3.js +1 -0
- package/dist/chunks/chunk-XFGXM4CR.js +4403 -0
- package/dist/chunks/dev-agent-JVIGBMHQ.js +1 -0
- package/dist/chunks/dev-agent-TRVP5U6N.js +1624 -0
- package/dist/chunks/dev-agent-Y5G5WKQ4.js +1624 -0
- package/dist/chunks/graph-storage-factory-AYZ57YSL.js +13 -0
- package/dist/chunks/graph-storage-factory-GTAIJEI5.js +1 -0
- package/dist/chunks/graph-storage-factory-T2WO5QVG.js +13 -0
- package/dist/chunks/incremental-updater-KDIQGAUU.js +14 -0
- package/dist/chunks/incremental-updater-OJRSTO3Q.js +1 -0
- package/dist/chunks/incremental-updater-SBEBH7KF.js +14 -0
- package/dist/chunks/indexer-agent-H3QIEL3Z.js +21 -0
- package/dist/chunks/indexer-agent-KHF5JMV7.js +21 -0
- package/dist/chunks/indexer-agent-SHJD6Z77.js +1 -0
- package/dist/chunks/indexing-pipeline-J6Z4BHKF.js +1 -0
- package/dist/chunks/indexing-pipeline-OY3337QN.js +249 -0
- package/dist/chunks/indexing-pipeline-WCXIDMAP.js +249 -0
- package/dist/chunks/merge-agent-LSUBDJB2.js +2481 -0
- package/dist/chunks/merge-agent-MJEW3HWU.js +2481 -0
- package/dist/chunks/merge-agent-O45OXF33.js +11 -0
- package/dist/chunks/merge-tool-handlers-BDSVNQVZ.js +277 -0
- package/dist/chunks/merge-tool-handlers-HP7DRBXJ.js +1 -0
- package/dist/chunks/merge-tool-handlers-RUJAKE3D.js +277 -0
- package/dist/chunks/pattern-tool-handlers-L62W3CXR.js +1549 -0
- package/dist/chunks/pattern-tool-handlers-SAHX2CVW.js +13 -0
- package/dist/chunks/query-agent-3TWDFIMT.js +191 -0
- package/dist/chunks/query-agent-HXQ3BMMF.js +191 -0
- package/dist/chunks/query-agent-USMC2GNG.js +1 -0
- package/dist/chunks/semantic-agent-MQCAWIAB.js +6381 -0
- package/dist/chunks/semantic-agent-NDGR3NAK.js +6381 -0
- package/dist/chunks/semantic-agent-S4ZL6GZC.js +137 -0
- package/dist/index.js +17 -17
- package/dist/roslyn-addon/.build-hash +1 -1
- package/dist/roslyn-addon/ILGPU.Algorithms.dll +0 -0
- package/dist/roslyn-addon/ILGPU.dll +0 -0
- package/dist/roslyn-addon/UltraCode.CSharp.deps.json +35 -0
- package/dist/roslyn-addon/UltraCode.CSharp.dll +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2481 @@
|
|
|
1
|
+
import { getGlobalContainer, getOrCreateAgent } from './chunk-KBW6LRQP.js';
|
|
2
|
+
import { BaseAgent } from './chunk-IGUCJL3R.js';
|
|
3
|
+
import { cosineSimilarity } from './chunk-QN237S7C.js';
|
|
4
|
+
import { init_fast_hash, hashText } from './chunk-HEMJHRHZ.js';
|
|
5
|
+
import './chunk-GMVGCSNU.js';
|
|
6
|
+
import { init_file_ops, readBytes } from './chunk-F7CKCMXI.js';
|
|
7
|
+
import './chunk-BMHPPH2B.js';
|
|
8
|
+
import { init_logging, log } from './chunk-VCCBEJQ5.js';
|
|
9
|
+
import { __export, __esm, __toCommonJS } from './chunk-NAQKA54E.js';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
// src/merge/indexing/content-normalizer.ts
|
|
15
|
+
var content_normalizer_exports = {};
|
|
16
|
+
__export(content_normalizer_exports, {
|
|
17
|
+
ContentNormalizer: () => ContentNormalizer
|
|
18
|
+
});
|
|
19
|
+
var ContentNormalizer;
|
|
20
|
+
var init_content_normalizer = __esm({
|
|
21
|
+
"src/merge/indexing/content-normalizer.ts"() {
|
|
22
|
+
init_fast_hash();
|
|
23
|
+
init_file_ops();
|
|
24
|
+
ContentNormalizer = class {
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a file.
|
|
27
|
+
*
|
|
28
|
+
* @param filePath - Absolute path to file
|
|
29
|
+
* @returns Normalized content with metadata
|
|
30
|
+
*/
|
|
31
|
+
async normalize(filePath) {
|
|
32
|
+
const rawBytes = Buffer.from(await readBytes(filePath));
|
|
33
|
+
const { encoding, hasBom } = this.detectEncoding(rawBytes);
|
|
34
|
+
let content = rawBytes.toString(encoding);
|
|
35
|
+
if (hasBom && content.charCodeAt(0) === 65279) {
|
|
36
|
+
content = content.slice(1);
|
|
37
|
+
}
|
|
38
|
+
content = this.normalizeLineEndings(content);
|
|
39
|
+
content = this.trimTrailingWhitespace(content);
|
|
40
|
+
content = content.replace(/\n+$/, "\n");
|
|
41
|
+
return {
|
|
42
|
+
content,
|
|
43
|
+
originalEncoding: encoding,
|
|
44
|
+
hadBom: hasBom
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compute SHA256 hash of content.
|
|
49
|
+
*
|
|
50
|
+
* Used for Fast Path Level 1 matching (exact content match).
|
|
51
|
+
*
|
|
52
|
+
* @param content - Normalized content string
|
|
53
|
+
* @returns Hex-encoded SHA256 hash
|
|
54
|
+
*/
|
|
55
|
+
computeContentHash(content) {
|
|
56
|
+
return hashText(content);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Detect encoding via BOM detection.
|
|
60
|
+
*
|
|
61
|
+
* @param bytes - Raw file bytes
|
|
62
|
+
* @returns Detected encoding and BOM presence
|
|
63
|
+
*/
|
|
64
|
+
detectEncoding(bytes) {
|
|
65
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
|
|
66
|
+
return { encoding: "utf8", hasBom: true };
|
|
67
|
+
}
|
|
68
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254) {
|
|
69
|
+
return { encoding: "utf16le", hasBom: true };
|
|
70
|
+
}
|
|
71
|
+
if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) {
|
|
72
|
+
return { encoding: "utf16le", hasBom: true };
|
|
73
|
+
}
|
|
74
|
+
return { encoding: "utf8", hasBom: false };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Normalize line endings (CRLF/CR → LF).
|
|
78
|
+
*
|
|
79
|
+
* @param content - Content with potentially mixed line endings
|
|
80
|
+
* @returns Content with Unix-style LF line endings
|
|
81
|
+
*/
|
|
82
|
+
normalizeLineEndings(content) {
|
|
83
|
+
content = content.replace(/\r\n/g, "\n");
|
|
84
|
+
content = content.replace(/\r/g, "\n");
|
|
85
|
+
return content;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Remove trailing whitespace on each line.
|
|
89
|
+
*
|
|
90
|
+
* @param content - Content to trim
|
|
91
|
+
* @returns Content with trimmed lines
|
|
92
|
+
*/
|
|
93
|
+
trimTrailingWhitespace(content) {
|
|
94
|
+
return content.split("\n").map((line) => line.replace(/[ \t]+$/, "")).join("\n");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// src/merge/models/code-unit.ts
|
|
101
|
+
var code_unit_exports = {};
|
|
102
|
+
__export(code_unit_exports, {
|
|
103
|
+
CodeUnitType: () => CodeUnitType
|
|
104
|
+
});
|
|
105
|
+
var CodeUnitType;
|
|
106
|
+
var init_code_unit = __esm({
|
|
107
|
+
"src/merge/models/code-unit.ts"() {
|
|
108
|
+
CodeUnitType = /* @__PURE__ */ ((CodeUnitType2) => {
|
|
109
|
+
CodeUnitType2["File"] = "file";
|
|
110
|
+
CodeUnitType2["Module"] = "module";
|
|
111
|
+
CodeUnitType2["Class"] = "class";
|
|
112
|
+
CodeUnitType2["Interface"] = "interface";
|
|
113
|
+
CodeUnitType2["Function"] = "function";
|
|
114
|
+
CodeUnitType2["Method"] = "method";
|
|
115
|
+
CodeUnitType2["Property"] = "property";
|
|
116
|
+
CodeUnitType2["Block"] = "block";
|
|
117
|
+
CodeUnitType2["Statement"] = "statement";
|
|
118
|
+
return CodeUnitType2;
|
|
119
|
+
})(CodeUnitType || {});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// src/agents/merge-agent.ts
|
|
124
|
+
init_logging();
|
|
125
|
+
|
|
126
|
+
// src/merge/engine/ai-conflict-resolver.ts
|
|
127
|
+
init_logging();
|
|
128
|
+
|
|
129
|
+
// src/merge/models/semantic-conflict.ts
|
|
130
|
+
function createOverlappingConflict(baseUnit, branchAUnit, branchBUnit) {
|
|
131
|
+
const id = `conflict-${branchAUnit.id}-${branchBUnit.id}-${Date.now()}`;
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
type: "OverlappingChanges" /* OverlappingChanges */,
|
|
135
|
+
severity: "Medium" /* Medium */,
|
|
136
|
+
baseUnit,
|
|
137
|
+
branchAUnit,
|
|
138
|
+
branchBUnit,
|
|
139
|
+
description: `Both branches modified ${branchAUnit.fullyQualifiedName}`,
|
|
140
|
+
conflictingRegions: [],
|
|
141
|
+
autoResolvable: false
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function createIncompatibleIntentsConflict(baseUnit, branchAUnit, branchBUnit, branchAIntent, branchBIntent) {
|
|
145
|
+
const id = `conflict-${branchAUnit.id}-${branchBUnit.id}-${Date.now()}`;
|
|
146
|
+
return {
|
|
147
|
+
id,
|
|
148
|
+
type: "IncompatibleIntents" /* IncompatibleIntents */,
|
|
149
|
+
severity: "High" /* High */,
|
|
150
|
+
baseUnit,
|
|
151
|
+
branchAUnit,
|
|
152
|
+
branchBUnit,
|
|
153
|
+
branchAIntent,
|
|
154
|
+
branchBIntent,
|
|
155
|
+
description: `Incompatible intents: ${branchAIntent.type} vs ${branchBIntent.type}`,
|
|
156
|
+
conflictingRegions: [],
|
|
157
|
+
autoResolvable: false
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function createAPIBreakingConflict(baseUnit, branchAUnit, branchBUnit) {
|
|
161
|
+
const id = `conflict-${branchAUnit.id}-${branchBUnit.id}-${Date.now()}`;
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
type: "APIBreakingChange" /* APIBreakingChange */,
|
|
165
|
+
severity: "Critical" /* Critical */,
|
|
166
|
+
baseUnit,
|
|
167
|
+
branchAUnit,
|
|
168
|
+
branchBUnit,
|
|
169
|
+
description: `API breaking change in ${branchAUnit.fullyQualifiedName}`,
|
|
170
|
+
conflictingRegions: [],
|
|
171
|
+
autoResolvable: false
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/merge/engine/ai-conflict-resolver.ts
|
|
176
|
+
var AIConflictResolver = class {
|
|
177
|
+
config;
|
|
178
|
+
embeddingCache = /* @__PURE__ */ new Map();
|
|
179
|
+
constructor(config) {
|
|
180
|
+
this.config = config;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Analyze a conflict using AI
|
|
184
|
+
*
|
|
185
|
+
* @param conflict - Conflict to analyze
|
|
186
|
+
* @returns AI analysis result
|
|
187
|
+
*/
|
|
188
|
+
async analyzeConflict(conflict) {
|
|
189
|
+
const { baseUnit, branchAUnit, branchBUnit } = conflict;
|
|
190
|
+
const [baseEmbedding, branchAEmbedding, branchBEmbedding] = await Promise.all([
|
|
191
|
+
this.getEmbedding(baseUnit),
|
|
192
|
+
this.getEmbedding(branchAUnit),
|
|
193
|
+
this.getEmbedding(branchBUnit)
|
|
194
|
+
]);
|
|
195
|
+
const similarity = this.cosineSimilarity(branchAEmbedding, branchBEmbedding);
|
|
196
|
+
const branchADistance = baseEmbedding && branchAEmbedding ? this.cosineDistance(baseEmbedding, branchAEmbedding) : 0;
|
|
197
|
+
const branchBDistance = baseEmbedding && branchBEmbedding ? this.cosineDistance(baseEmbedding, branchBEmbedding) : 0;
|
|
198
|
+
const { strategy, confidence, explanation, mergedCode } = this.determineStrategy(
|
|
199
|
+
conflict,
|
|
200
|
+
similarity,
|
|
201
|
+
branchADistance,
|
|
202
|
+
branchBDistance
|
|
203
|
+
);
|
|
204
|
+
return {
|
|
205
|
+
similarity,
|
|
206
|
+
branchADistance,
|
|
207
|
+
branchBDistance,
|
|
208
|
+
suggestedStrategy: strategy,
|
|
209
|
+
confidence,
|
|
210
|
+
explanation,
|
|
211
|
+
mergedCode
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get or generate embedding for a code unit
|
|
216
|
+
*/
|
|
217
|
+
async getEmbedding(unit) {
|
|
218
|
+
if (!unit) return null;
|
|
219
|
+
const cacheKey = this.getCacheKey(unit);
|
|
220
|
+
if (this.embeddingCache.has(cacheKey)) {
|
|
221
|
+
return this.embeddingCache.get(cacheKey);
|
|
222
|
+
}
|
|
223
|
+
if (unit.embedding) {
|
|
224
|
+
this.embeddingCache.set(cacheKey, unit.embedding);
|
|
225
|
+
return unit.embedding;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const text = this.prepareTextForEmbedding(unit);
|
|
229
|
+
const embedding = await this.config.embeddingGenerator.generateEmbedding(text);
|
|
230
|
+
this.embeddingCache.set(cacheKey, embedding);
|
|
231
|
+
return embedding;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
log.w("AICONFLICT", "embedding_fail", { id: unit.id, err: String(error) });
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Prepare text for embedding
|
|
239
|
+
*/
|
|
240
|
+
prepareTextForEmbedding(unit) {
|
|
241
|
+
const parts = [];
|
|
242
|
+
if (unit.fullyQualifiedName) {
|
|
243
|
+
parts.push(`Name: ${unit.fullyQualifiedName}`);
|
|
244
|
+
}
|
|
245
|
+
if (unit.signature) {
|
|
246
|
+
parts.push(`Signature: ${unit.signature}`);
|
|
247
|
+
}
|
|
248
|
+
if (unit.content) {
|
|
249
|
+
const normalized = unit.content.trim().replace(/\s+/g, " ");
|
|
250
|
+
parts.push(`Code: ${normalized}`);
|
|
251
|
+
}
|
|
252
|
+
return parts.join("\n");
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Determine resolution strategy based on similarity analysis
|
|
256
|
+
*/
|
|
257
|
+
determineStrategy(conflict, similarity, branchADistance, branchBDistance) {
|
|
258
|
+
const { branchAUnit, branchBUnit } = conflict;
|
|
259
|
+
if (similarity >= this.config.highSimilarityThreshold) {
|
|
260
|
+
const preferA = branchADistance <= branchBDistance;
|
|
261
|
+
return {
|
|
262
|
+
strategy: preferA ? "TakeBranchA" /* TakeBranchA */ : "TakeBranchB" /* TakeBranchB */,
|
|
263
|
+
confidence: 0.95,
|
|
264
|
+
explanation: `Both branches made semantically similar changes (similarity: ${similarity.toFixed(2)}). Choosing ${preferA ? "branchA" : "branchB"} as it's closer to base.`,
|
|
265
|
+
mergedCode: preferA ? branchAUnit.content : branchBUnit.content
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (similarity >= this.config.mediumSimilarityThreshold) {
|
|
269
|
+
const mergedCode = this.attemptIntelligentMerge(conflict, similarity);
|
|
270
|
+
if (mergedCode) {
|
|
271
|
+
return {
|
|
272
|
+
strategy: "MergeBoth" /* MergeBoth */,
|
|
273
|
+
confidence: 0.7 + similarity * 0.2,
|
|
274
|
+
// 0.7-0.9 range
|
|
275
|
+
explanation: `Changes are semantically compatible (similarity: ${similarity.toFixed(2)}). AI-generated merge proposed.`,
|
|
276
|
+
mergedCode
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
strategy: "ManualReview" /* ManualReview */,
|
|
281
|
+
confidence: 0.6,
|
|
282
|
+
explanation: `Changes are moderately similar (${similarity.toFixed(2)}) but automatic merge is uncertain. Manual review recommended.`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (similarity >= this.config.lowSimilarityThreshold) {
|
|
286
|
+
return {
|
|
287
|
+
strategy: "ManualReview" /* ManualReview */,
|
|
288
|
+
confidence: 0.4,
|
|
289
|
+
explanation: `Changes are semantically different (similarity: ${similarity.toFixed(2)}). Manual review required.`
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
strategy: "ManualReview" /* ManualReview */,
|
|
294
|
+
confidence: 0.2,
|
|
295
|
+
explanation: `Changes are semantically divergent (similarity: ${similarity.toFixed(2)}). Careful manual review strongly recommended.`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Attempt intelligent merge based on semantic analysis
|
|
300
|
+
*
|
|
301
|
+
* Heuristic: if changes are semantically close, try to combine them
|
|
302
|
+
*/
|
|
303
|
+
attemptIntelligentMerge(conflict, similarity) {
|
|
304
|
+
const { baseUnit, branchAUnit, branchBUnit } = conflict;
|
|
305
|
+
const baseLength = baseUnit?.content.length || 0;
|
|
306
|
+
const branchALength = branchAUnit.content.length;
|
|
307
|
+
const branchBLength = branchBUnit.content.length;
|
|
308
|
+
const branchAAdded = branchALength > baseLength;
|
|
309
|
+
const branchBAdded = branchBLength > baseLength;
|
|
310
|
+
if (branchAAdded && branchBAdded && similarity >= 0.7) {
|
|
311
|
+
return branchALength > branchBLength ? branchAUnit.content : branchBUnit.content;
|
|
312
|
+
}
|
|
313
|
+
const maxChange = Math.max(Math.abs(branchALength - baseLength), Math.abs(branchBLength - baseLength));
|
|
314
|
+
if (maxChange < 100 && similarity >= 0.8) {
|
|
315
|
+
return branchAUnit.content;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Cosine similarity between two vectors (0-1, where 1 = identical)
|
|
321
|
+
*/
|
|
322
|
+
cosineSimilarity(a, b) {
|
|
323
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
324
|
+
let dotProduct = 0;
|
|
325
|
+
let normA = 0;
|
|
326
|
+
let normB = 0;
|
|
327
|
+
for (let i = 0; i < a.length; i++) {
|
|
328
|
+
const aVal = a[i] ?? 0;
|
|
329
|
+
const bVal = b[i] ?? 0;
|
|
330
|
+
dotProduct += aVal * bVal;
|
|
331
|
+
normA += aVal * aVal;
|
|
332
|
+
normB += bVal * bVal;
|
|
333
|
+
}
|
|
334
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
335
|
+
if (denominator === 0) return 0;
|
|
336
|
+
return dotProduct / denominator;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Cosine distance (1 - similarity)
|
|
340
|
+
*/
|
|
341
|
+
cosineDistance(a, b) {
|
|
342
|
+
const similarity = this.cosineSimilarity(a, b);
|
|
343
|
+
return 1 - similarity;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Cache key for embedding
|
|
347
|
+
*/
|
|
348
|
+
getCacheKey(unit) {
|
|
349
|
+
return `${unit.id}-${unit.contentHash}`;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Create Resolution based on AI analysis
|
|
353
|
+
*/
|
|
354
|
+
createResolution(aiAnalysis) {
|
|
355
|
+
return {
|
|
356
|
+
strategy: aiAnalysis.suggestedStrategy,
|
|
357
|
+
confidence: aiAnalysis.confidence,
|
|
358
|
+
mergedCode: aiAnalysis.mergedCode || "",
|
|
359
|
+
explanation: aiAnalysis.explanation
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Clear cache
|
|
364
|
+
*/
|
|
365
|
+
clearCache() {
|
|
366
|
+
this.embeddingCache.clear();
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Get cache statistics
|
|
370
|
+
*/
|
|
371
|
+
getCacheStats() {
|
|
372
|
+
return {
|
|
373
|
+
size: this.embeddingCache.size,
|
|
374
|
+
maxSize: 1e3
|
|
375
|
+
// Can be made configurable
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/merge/engine/three-way-merger.ts
|
|
381
|
+
init_logging();
|
|
382
|
+
|
|
383
|
+
// src/merge/models/change-intent.ts
|
|
384
|
+
function createBugFixIntent(evidence) {
|
|
385
|
+
return {
|
|
386
|
+
type: "BugFix" /* BugFix */,
|
|
387
|
+
confidence: evidence.length > 0 ? Math.min(0.7 + evidence.length * 0.1, 1) : 0.5,
|
|
388
|
+
evidence,
|
|
389
|
+
description: "Code change appears to fix a bug"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function createRefactoringIntent(evidence) {
|
|
393
|
+
return {
|
|
394
|
+
type: "Refactoring" /* Refactoring */,
|
|
395
|
+
confidence: evidence.length > 0 ? Math.min(0.7 + evidence.length * 0.1, 1) : 0.5,
|
|
396
|
+
evidence,
|
|
397
|
+
description: "Code change is refactoring without logic changes"
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function createFeatureAdditionIntent(evidence) {
|
|
401
|
+
return {
|
|
402
|
+
type: "FeatureAddition" /* FeatureAddition */,
|
|
403
|
+
confidence: evidence.length > 0 ? Math.min(0.7 + evidence.length * 0.1, 1) : 0.5,
|
|
404
|
+
evidence,
|
|
405
|
+
description: "Code change adds new functionality"
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function createAPIChangeIntent(evidence) {
|
|
409
|
+
return {
|
|
410
|
+
type: "APIChange" /* APIChange */,
|
|
411
|
+
confidence: evidence.length > 0 ? Math.min(0.7 + evidence.length * 0.1, 1) : 0.5,
|
|
412
|
+
evidence,
|
|
413
|
+
description: "Code change modifies API signatures"
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function createUnknownIntent() {
|
|
417
|
+
return {
|
|
418
|
+
type: "Unknown" /* Unknown */,
|
|
419
|
+
confidence: 0,
|
|
420
|
+
evidence: [],
|
|
421
|
+
description: "Unable to classify change intent"
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/merge/analysis/conflict-detector.ts
|
|
426
|
+
var ConflictDetector = class {
|
|
427
|
+
/**
|
|
428
|
+
* Determine whether there is a conflict between two changes
|
|
429
|
+
*
|
|
430
|
+
* @param baseUnit - Unit in base (may be null)
|
|
431
|
+
* @param branchAUnit - Unit in branchA
|
|
432
|
+
* @param branchBUnit - Unit in branchB
|
|
433
|
+
* @param branchAIntent - Change intent in branchA (optional)
|
|
434
|
+
* @param branchBIntent - Change intent in branchB (optional)
|
|
435
|
+
* @returns SemanticConflict if there is a conflict, null if not
|
|
436
|
+
*/
|
|
437
|
+
detectConflict(baseUnit, branchAUnit, branchBUnit, branchAIntent, branchBIntent) {
|
|
438
|
+
if (branchAUnit.contentHash === branchBUnit.contentHash) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
const apiConflict = this.detectAPIConflict(baseUnit, branchAUnit, branchBUnit);
|
|
442
|
+
if (apiConflict) {
|
|
443
|
+
return apiConflict;
|
|
444
|
+
}
|
|
445
|
+
if (branchAIntent && branchBIntent) {
|
|
446
|
+
const intentConflict = this.detectIntentConflict(
|
|
447
|
+
baseUnit,
|
|
448
|
+
branchAUnit,
|
|
449
|
+
branchBUnit,
|
|
450
|
+
branchAIntent,
|
|
451
|
+
branchBIntent
|
|
452
|
+
);
|
|
453
|
+
if (intentConflict) {
|
|
454
|
+
return intentConflict;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const conflict = createOverlappingConflict(baseUnit, branchAUnit, branchBUnit);
|
|
458
|
+
conflict.severity = this.classifySeverity(baseUnit, branchAUnit, branchBUnit);
|
|
459
|
+
conflict.autoResolvable = this.isAutoResolvable(conflict, branchAIntent, branchBIntent);
|
|
460
|
+
return conflict;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Detect API breaking changes
|
|
464
|
+
*/
|
|
465
|
+
detectAPIConflict(baseUnit, branchAUnit, branchBUnit) {
|
|
466
|
+
if (!baseUnit) return null;
|
|
467
|
+
const branchASignatureChanged = baseUnit.signature !== branchAUnit.signature;
|
|
468
|
+
const branchBSignatureChanged = baseUnit.signature !== branchBUnit.signature;
|
|
469
|
+
if (branchASignatureChanged && branchBSignatureChanged) {
|
|
470
|
+
return createAPIBreakingConflict(baseUnit, branchAUnit, branchBUnit);
|
|
471
|
+
}
|
|
472
|
+
const branchAFQNChanged = baseUnit.fullyQualifiedName !== branchAUnit.fullyQualifiedName;
|
|
473
|
+
const branchBFQNChanged = baseUnit.fullyQualifiedName !== branchBUnit.fullyQualifiedName;
|
|
474
|
+
if (branchAFQNChanged && branchBFQNChanged) {
|
|
475
|
+
return createAPIBreakingConflict(baseUnit, branchAUnit, branchBUnit);
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Detect intent conflict
|
|
481
|
+
*/
|
|
482
|
+
detectIntentConflict(baseUnit, branchAUnit, branchBUnit, branchAIntent, branchBIntent) {
|
|
483
|
+
const compatible = this.areIntentsCompatible(branchAIntent.type, branchBIntent.type);
|
|
484
|
+
if (!compatible) {
|
|
485
|
+
return createIncompatibleIntentsConflict(baseUnit, branchAUnit, branchBUnit, branchAIntent, branchBIntent);
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Check intent compatibility
|
|
491
|
+
*/
|
|
492
|
+
areIntentsCompatible(intentA, intentB) {
|
|
493
|
+
const compatibilityMatrix = {
|
|
494
|
+
["BugFix" /* BugFix */]: /* @__PURE__ */ new Set([
|
|
495
|
+
"BugFix" /* BugFix */,
|
|
496
|
+
// Two bug fixes are usually compatible
|
|
497
|
+
"Refactoring" /* Refactoring */
|
|
498
|
+
// BugFix + Refactoring = OK
|
|
499
|
+
]),
|
|
500
|
+
["Refactoring" /* Refactoring */]: /* @__PURE__ */ new Set(["BugFix" /* BugFix */, "Refactoring" /* Refactoring */]),
|
|
501
|
+
["FeatureAddition" /* FeatureAddition */]: /* @__PURE__ */ new Set([
|
|
502
|
+
"FeatureAddition" /* FeatureAddition */
|
|
503
|
+
// Two feature additions can be compatible
|
|
504
|
+
]),
|
|
505
|
+
["APIChange" /* APIChange */]: /* @__PURE__ */ new Set([
|
|
506
|
+
// API changes are usually incompatible with other changes
|
|
507
|
+
]),
|
|
508
|
+
["Unknown" /* Unknown */]: /* @__PURE__ */ new Set(["Unknown" /* Unknown */])
|
|
509
|
+
};
|
|
510
|
+
const compatibleWith = compatibilityMatrix[intentA];
|
|
511
|
+
return compatibleWith ? compatibleWith.has(intentB) : false;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Classify conflict severity
|
|
515
|
+
*/
|
|
516
|
+
classifySeverity(baseUnit, branchAUnit, branchBUnit) {
|
|
517
|
+
if (!baseUnit) {
|
|
518
|
+
return "Medium" /* Medium */;
|
|
519
|
+
}
|
|
520
|
+
const branchADiff = this.computeDifference(baseUnit.content, branchAUnit.content);
|
|
521
|
+
const branchBDiff = this.computeDifference(baseUnit.content, branchBUnit.content);
|
|
522
|
+
if (branchADiff > 0.5 && branchBDiff > 0.5) {
|
|
523
|
+
return "High" /* High */;
|
|
524
|
+
}
|
|
525
|
+
if (branchADiff > 0.3 || branchBDiff > 0.3) {
|
|
526
|
+
return "Medium" /* Medium */;
|
|
527
|
+
}
|
|
528
|
+
return "Low" /* Low */;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Compute difference between two code versions (0.0-1.0)
|
|
532
|
+
*/
|
|
533
|
+
computeDifference(baseContent, changedContent) {
|
|
534
|
+
const maxLength = Math.max(baseContent.length, changedContent.length);
|
|
535
|
+
if (maxLength === 0) return 0;
|
|
536
|
+
const lengthDiff = Math.abs(baseContent.length - changedContent.length);
|
|
537
|
+
return Math.min(lengthDiff / maxLength, 1);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Check whether the conflict can be automatically resolved
|
|
541
|
+
*/
|
|
542
|
+
isAutoResolvable(conflict, branchAIntent, branchBIntent) {
|
|
543
|
+
if (conflict.severity === "Critical" /* Critical */) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
if (conflict.type === "APIBreakingChange" /* APIBreakingChange */) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
if (branchAIntent && branchBIntent && this.areIntentsCompatible(branchAIntent.type, branchBIntent.type) && conflict.severity === "Low" /* Low */) {
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Batch detection for multiple unit pairs
|
|
556
|
+
*/
|
|
557
|
+
detectConflicts(matches) {
|
|
558
|
+
const conflicts = [];
|
|
559
|
+
for (const match of matches) {
|
|
560
|
+
const conflict = this.detectConflict(
|
|
561
|
+
match.baseUnit,
|
|
562
|
+
match.branchAUnit,
|
|
563
|
+
match.branchBUnit,
|
|
564
|
+
match.branchAIntent,
|
|
565
|
+
match.branchBIntent
|
|
566
|
+
);
|
|
567
|
+
if (conflict) {
|
|
568
|
+
conflicts.push(conflict);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return conflicts;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// src/merge/analysis/intent-classifier.ts
|
|
576
|
+
var IntentClassifier = class {
|
|
577
|
+
/**
|
|
578
|
+
* Classify the change intent
|
|
579
|
+
*
|
|
580
|
+
* @param baseUnit - Unit before the change (may be null for new units)
|
|
581
|
+
* @param changedUnit - Unit after the change
|
|
582
|
+
* @returns ChangeIntent with type and confidence
|
|
583
|
+
*/
|
|
584
|
+
classifyIntent(baseUnit, changedUnit) {
|
|
585
|
+
if (!baseUnit) {
|
|
586
|
+
return this.classifyNewUnit(changedUnit);
|
|
587
|
+
}
|
|
588
|
+
const bugFixEvidence = this.collectBugFixEvidence(baseUnit, changedUnit);
|
|
589
|
+
const refactoringEvidence = this.collectRefactoringEvidence(baseUnit, changedUnit);
|
|
590
|
+
const featureEvidence = this.collectFeatureAdditionEvidence(baseUnit, changedUnit);
|
|
591
|
+
const apiChangeEvidence = this.collectAPIChangeEvidence(baseUnit, changedUnit);
|
|
592
|
+
const evidenceCounts = [
|
|
593
|
+
{ type: "BugFix" /* BugFix */, evidence: bugFixEvidence, count: bugFixEvidence.length },
|
|
594
|
+
{
|
|
595
|
+
type: "Refactoring" /* Refactoring */,
|
|
596
|
+
evidence: refactoringEvidence,
|
|
597
|
+
count: refactoringEvidence.length
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
type: "FeatureAddition" /* FeatureAddition */,
|
|
601
|
+
evidence: featureEvidence,
|
|
602
|
+
count: featureEvidence.length
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
type: "APIChange" /* APIChange */,
|
|
606
|
+
evidence: apiChangeEvidence,
|
|
607
|
+
count: apiChangeEvidence.length
|
|
608
|
+
}
|
|
609
|
+
];
|
|
610
|
+
evidenceCounts.sort((a, b) => b.count - a.count);
|
|
611
|
+
const winner = evidenceCounts[0];
|
|
612
|
+
if (!winner || winner.count === 0) {
|
|
613
|
+
return createUnknownIntent();
|
|
614
|
+
}
|
|
615
|
+
switch (winner.type) {
|
|
616
|
+
case "BugFix" /* BugFix */:
|
|
617
|
+
return createBugFixIntent(bugFixEvidence);
|
|
618
|
+
case "Refactoring" /* Refactoring */:
|
|
619
|
+
return createRefactoringIntent(refactoringEvidence);
|
|
620
|
+
case "FeatureAddition" /* FeatureAddition */:
|
|
621
|
+
return createFeatureAdditionIntent(featureEvidence);
|
|
622
|
+
case "APIChange" /* APIChange */:
|
|
623
|
+
return createAPIChangeIntent(apiChangeEvidence);
|
|
624
|
+
default:
|
|
625
|
+
return createUnknownIntent();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Classify a new unit (baseUnit === null)
|
|
630
|
+
*/
|
|
631
|
+
classifyNewUnit(changedUnit) {
|
|
632
|
+
const evidence = [];
|
|
633
|
+
if (changedUnit.type === "class") {
|
|
634
|
+
evidence.push({
|
|
635
|
+
type: "NewClass" /* NewClass */,
|
|
636
|
+
description: `Added new class: ${changedUnit.name}`,
|
|
637
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
638
|
+
});
|
|
639
|
+
} else if (changedUnit.type === "function" || changedUnit.type === "method") {
|
|
640
|
+
evidence.push({
|
|
641
|
+
type: "NewMethod" /* NewMethod */,
|
|
642
|
+
description: `Added new method: ${changedUnit.name}`,
|
|
643
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
644
|
+
});
|
|
645
|
+
} else if (changedUnit.type === "property") {
|
|
646
|
+
evidence.push({
|
|
647
|
+
type: "NewProperty" /* NewProperty */,
|
|
648
|
+
description: `Added new property: ${changedUnit.name}`,
|
|
649
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return createFeatureAdditionIntent(evidence);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Collect BugFix evidence
|
|
656
|
+
*/
|
|
657
|
+
collectBugFixEvidence(baseUnit, changedUnit) {
|
|
658
|
+
const evidence = [];
|
|
659
|
+
const baseContent = baseUnit.content.toLowerCase();
|
|
660
|
+
const changedContent = changedUnit.content.toLowerCase();
|
|
661
|
+
if (!baseContent.includes("try") && changedContent.includes("try")) {
|
|
662
|
+
evidence.push({
|
|
663
|
+
type: "AddedTryCatch" /* AddedTryCatch */,
|
|
664
|
+
description: "Added try-catch block for error handling",
|
|
665
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
const baseIfCount = (baseContent.match(/\bif\s*\(/g) || []).length;
|
|
669
|
+
const changedIfCount = (changedContent.match(/\bif\s*\(/g) || []).length;
|
|
670
|
+
if (changedIfCount > baseIfCount) {
|
|
671
|
+
evidence.push({
|
|
672
|
+
type: "AddedValidation" /* AddedValidation */,
|
|
673
|
+
description: `Added validation checks (+${changedIfCount - baseIfCount} if statements)`,
|
|
674
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const hasNullCheck = changedContent.includes("!= null") || changedContent.includes("!== null") || changedContent.includes("== null") || changedContent.includes("=== null");
|
|
678
|
+
if (hasNullCheck && !baseContent.includes("null")) {
|
|
679
|
+
evidence.push({
|
|
680
|
+
type: "AddedNullCheck" /* AddedNullCheck */,
|
|
681
|
+
description: "Added null/undefined check",
|
|
682
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return evidence;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Collect Refactoring evidence
|
|
689
|
+
*/
|
|
690
|
+
collectRefactoringEvidence(baseUnit, changedUnit) {
|
|
691
|
+
const evidence = [];
|
|
692
|
+
if (baseUnit.name !== changedUnit.name && baseUnit.structuralHash === changedUnit.structuralHash) {
|
|
693
|
+
evidence.push({
|
|
694
|
+
type: "RenamedVariable" /* RenamedVariable */,
|
|
695
|
+
description: `Renamed ${baseUnit.name} to ${changedUnit.name}`,
|
|
696
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
if (baseUnit.structuralHash === changedUnit.structuralHash && baseUnit.contentHash !== changedUnit.contentHash) {
|
|
700
|
+
evidence.push({
|
|
701
|
+
type: "CFGPreserved" /* CFGPreserved */,
|
|
702
|
+
description: "Code structure preserved (likely refactoring)",
|
|
703
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return evidence;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Collect FeatureAddition evidence
|
|
710
|
+
*/
|
|
711
|
+
collectFeatureAdditionEvidence(baseUnit, changedUnit) {
|
|
712
|
+
const evidence = [];
|
|
713
|
+
const newChildren = changedUnit.childIds.length - baseUnit.childIds.length;
|
|
714
|
+
if (newChildren > 0) {
|
|
715
|
+
evidence.push({
|
|
716
|
+
type: "NewMethod" /* NewMethod */,
|
|
717
|
+
description: `Added ${newChildren} new methods/properties`,
|
|
718
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
const sizeIncrease = (changedUnit.content.length - baseUnit.content.length) / baseUnit.content.length;
|
|
722
|
+
if (sizeIncrease > 0.3) {
|
|
723
|
+
evidence.push({
|
|
724
|
+
type: "NewMethod" /* NewMethod */,
|
|
725
|
+
description: `Code size increased by ${Math.round(sizeIncrease * 100)}%`,
|
|
726
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
return evidence;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Collect APIChange evidence
|
|
733
|
+
*/
|
|
734
|
+
collectAPIChangeEvidence(baseUnit, changedUnit) {
|
|
735
|
+
const evidence = [];
|
|
736
|
+
if (baseUnit.signature && changedUnit.signature && baseUnit.signature !== changedUnit.signature) {
|
|
737
|
+
evidence.push({
|
|
738
|
+
type: "SignatureChanged" /* SignatureChanged */,
|
|
739
|
+
description: `Signature changed: ${baseUnit.signature} \u2192 ${changedUnit.signature}`,
|
|
740
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (baseUnit.fullyQualifiedName !== changedUnit.fullyQualifiedName) {
|
|
744
|
+
evidence.push({
|
|
745
|
+
type: "SignatureChanged" /* SignatureChanged */,
|
|
746
|
+
description: `Fully qualified name changed: ${baseUnit.fullyQualifiedName} \u2192 ${changedUnit.fullyQualifiedName}`,
|
|
747
|
+
location: `${changedUnit.filePath}:${changedUnit.startLine}`
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
return evidence;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// src/merge/indexing/lazy-embedding-cache.ts
|
|
755
|
+
var LazyEmbeddingCache = class {
|
|
756
|
+
embeddingGenerator;
|
|
757
|
+
cache = /* @__PURE__ */ new Map();
|
|
758
|
+
// In-memory cache
|
|
759
|
+
batchSize;
|
|
760
|
+
constructor(embeddingGenerator, options = {}) {
|
|
761
|
+
this.embeddingGenerator = embeddingGenerator;
|
|
762
|
+
this.batchSize = options.batchSize ?? 32;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Generate embeddings for units in the index.
|
|
766
|
+
*
|
|
767
|
+
* Only for units without embeddings (lazy generation).
|
|
768
|
+
*
|
|
769
|
+
* @param index - Versioned index
|
|
770
|
+
* @param unitsNeedingEmbeddings - Units that need embeddings
|
|
771
|
+
* @returns Promise that resolves when done
|
|
772
|
+
*/
|
|
773
|
+
async generateEmbeddings(index, unitsNeedingEmbeddings) {
|
|
774
|
+
const unitsToProcess = unitsNeedingEmbeddings.filter((unit) => !unit.embedding);
|
|
775
|
+
if (unitsToProcess.length === 0) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
for (let i = 0; i < unitsToProcess.length; i += this.batchSize) {
|
|
779
|
+
const batch = unitsToProcess.slice(i, i + this.batchSize);
|
|
780
|
+
await Promise.all(
|
|
781
|
+
batch.map(async (unit) => {
|
|
782
|
+
const embedding = await this.getOrGenerateEmbedding(unit);
|
|
783
|
+
unit.embedding = embedding;
|
|
784
|
+
index.units.set(unit.id, unit);
|
|
785
|
+
})
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Get embedding from cache or generate a new one.
|
|
791
|
+
*/
|
|
792
|
+
async getOrGenerateEmbedding(unit) {
|
|
793
|
+
const cacheKey = this.computeCacheKey(unit);
|
|
794
|
+
const cached = this.cache.get(cacheKey);
|
|
795
|
+
if (cached) {
|
|
796
|
+
return cached;
|
|
797
|
+
}
|
|
798
|
+
const embedding = await this.embeddingGenerator(unit.content);
|
|
799
|
+
this.cache.set(cacheKey, embedding);
|
|
800
|
+
return embedding;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Compute cache key for a unit.
|
|
804
|
+
*
|
|
805
|
+
* Based on contentHash (same content = same embedding).
|
|
806
|
+
*/
|
|
807
|
+
computeCacheKey(unit) {
|
|
808
|
+
return unit.contentHash;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Clear in-memory cache.
|
|
812
|
+
*/
|
|
813
|
+
clearCache() {
|
|
814
|
+
this.cache.clear();
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Get cache statistics.
|
|
818
|
+
*/
|
|
819
|
+
getCacheStats() {
|
|
820
|
+
return {
|
|
821
|
+
size: this.cache.size,
|
|
822
|
+
memoryUsage: this.estimateMemoryUsage()
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Estimate cache memory usage (bytes).
|
|
827
|
+
*/
|
|
828
|
+
estimateMemoryUsage() {
|
|
829
|
+
const bytesPerEmbedding = 384 * 4 + 100;
|
|
830
|
+
return this.cache.size * bytesPerEmbedding;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// src/merge/indexing/multi-version-indexer.ts
|
|
835
|
+
init_logging();
|
|
836
|
+
var MultiVersionIndexer = class {
|
|
837
|
+
branchManager;
|
|
838
|
+
gitIntegration;
|
|
839
|
+
conductor;
|
|
840
|
+
constructor(branchManager, gitIntegration, conductor) {
|
|
841
|
+
this.branchManager = branchManager;
|
|
842
|
+
this.gitIntegration = gitIntegration;
|
|
843
|
+
this.conductor = conductor;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Index 3 branches for merge: base, branchA, branchB
|
|
847
|
+
*
|
|
848
|
+
* This is the main entry point for Phase 4.
|
|
849
|
+
*/
|
|
850
|
+
async indexThreeBranches(branchA, branchB, options = {}) {
|
|
851
|
+
const startTime = Date.now();
|
|
852
|
+
let cacheHits = 0;
|
|
853
|
+
log.i("MULTIVIDX", `[MultiVersionIndexer] Starting 3-way indexing: ${branchA} + ${branchB}`);
|
|
854
|
+
try {
|
|
855
|
+
const mergeBase = this.gitIntegration.getMergeBase(branchA, branchB);
|
|
856
|
+
if (!mergeBase) {
|
|
857
|
+
throw new Error(`No merge base found between ${branchA} and ${branchB}`);
|
|
858
|
+
}
|
|
859
|
+
log.i("MULTIVIDX", `[MultiVersionIndexer] Merge base: ${mergeBase.slice(0, 8)}`);
|
|
860
|
+
const baseIndex = await this.indexBranch(mergeBase, "base", options);
|
|
861
|
+
if (this.wasCacheHit(baseIndex)) cacheHits++;
|
|
862
|
+
const branchAIndex = await this.indexBranch(branchA, branchA, options);
|
|
863
|
+
if (this.wasCacheHit(branchAIndex)) cacheHits++;
|
|
864
|
+
const branchBIndex = await this.indexBranch(branchB, branchB, options);
|
|
865
|
+
if (this.wasCacheHit(branchBIndex)) cacheHits++;
|
|
866
|
+
await this.gitIntegration.restoreOriginalBranch();
|
|
867
|
+
const totalUnits = baseIndex.stats.totalUnits + branchAIndex.stats.totalUnits + branchBIndex.stats.totalUnits;
|
|
868
|
+
const totalFiles = (baseIndex.stats.byFile?.size || 0) + (branchAIndex.stats.byFile?.size || 0) + (branchBIndex.stats.byFile?.size || 0);
|
|
869
|
+
const indexingTimeMs = Date.now() - startTime;
|
|
870
|
+
log.i("MULTIVIDX", `[MultiVersionIndexer] Completed in ${indexingTimeMs}ms`);
|
|
871
|
+
log.i("MULTIVIDX", `[MultiVersionIndexer] Total units: ${totalUnits}, Cache hits: ${cacheHits}/3`);
|
|
872
|
+
return {
|
|
873
|
+
base: baseIndex,
|
|
874
|
+
branchA: branchAIndex,
|
|
875
|
+
branchB: branchBIndex,
|
|
876
|
+
mergeBase,
|
|
877
|
+
stats: {
|
|
878
|
+
totalUnits,
|
|
879
|
+
totalFiles,
|
|
880
|
+
indexingTimeMs,
|
|
881
|
+
cacheHits
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
} catch (error) {
|
|
885
|
+
await this.gitIntegration.cleanup();
|
|
886
|
+
throw error;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Index a single branch
|
|
891
|
+
*
|
|
892
|
+
* Strategy:
|
|
893
|
+
* 1. Check if branch DB exists and is up-to-date
|
|
894
|
+
* 2. If yes → load from cache
|
|
895
|
+
* 3. If no → checkout branch, run full index, save to cache
|
|
896
|
+
*/
|
|
897
|
+
async indexBranch(branch, label, options) {
|
|
898
|
+
log.i("MULTIVIDX", `[MultiVersionIndexer] Indexing ${label}...`);
|
|
899
|
+
if (!options.fullScan && !options.reset) {
|
|
900
|
+
const cachedIndex = await this.loadCachedIndex(branch);
|
|
901
|
+
if (cachedIndex) {
|
|
902
|
+
log.i("MULTIVIDX", `[MultiVersionIndexer] Loaded ${label} from cache`);
|
|
903
|
+
return cachedIndex;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const startTime = Date.now();
|
|
907
|
+
await this.gitIntegration.checkoutBranch(branch);
|
|
908
|
+
if (!this.conductor) {
|
|
909
|
+
throw new Error("ConductorOrchestrator is required for fresh indexing");
|
|
910
|
+
}
|
|
911
|
+
const container = getGlobalContainer();
|
|
912
|
+
const devAgent = await getOrCreateAgent(container, this.conductor, "dev" /* DEV */);
|
|
913
|
+
const index = await this.runIndexing(devAgent, branch, options);
|
|
914
|
+
await this.saveCachedIndex(branch, index);
|
|
915
|
+
const elapsed = Date.now() - startTime;
|
|
916
|
+
log.i("MULTIVIDX", `[MultiVersionIndexer] Indexed ${label} in ${elapsed}ms (${index.stats.totalUnits} units)`);
|
|
917
|
+
return index;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Run actual indexing using DevAgent
|
|
921
|
+
*/
|
|
922
|
+
async runIndexing(devAgent, branch, options) {
|
|
923
|
+
const repoPath = this.gitIntegration.repoPath;
|
|
924
|
+
const result = await devAgent.execute?.({
|
|
925
|
+
task: "index_codebase",
|
|
926
|
+
params: {
|
|
927
|
+
directory: repoPath,
|
|
928
|
+
incremental: options.incremental ?? true,
|
|
929
|
+
fullScan: options.fullScan ?? false,
|
|
930
|
+
excludePatterns: options.excludePatterns || [],
|
|
931
|
+
reset: options.reset ?? false
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
const index = {
|
|
935
|
+
branch,
|
|
936
|
+
indexedAt: /* @__PURE__ */ new Date(),
|
|
937
|
+
units: /* @__PURE__ */ new Map(),
|
|
938
|
+
contentHashIndex: /* @__PURE__ */ new Map(),
|
|
939
|
+
structuralHashIndex: /* @__PURE__ */ new Map(),
|
|
940
|
+
signatureIndex: /* @__PURE__ */ new Map(),
|
|
941
|
+
filePathIndex: /* @__PURE__ */ new Map(),
|
|
942
|
+
stats: {
|
|
943
|
+
totalUnits: 0,
|
|
944
|
+
byType: /* @__PURE__ */ new Map(),
|
|
945
|
+
byLanguage: /* @__PURE__ */ new Map(),
|
|
946
|
+
byFile: /* @__PURE__ */ new Map()
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
if (result?.entities) {
|
|
950
|
+
for (const entity of result.entities) {
|
|
951
|
+
const codeUnit = this.entityToCodeUnit(entity);
|
|
952
|
+
this.addUnitToIndex(index, codeUnit);
|
|
953
|
+
}
|
|
954
|
+
} else {
|
|
955
|
+
await this.populateIndexFromStorage(index);
|
|
956
|
+
}
|
|
957
|
+
return index;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Convert entity from DevAgent to CodeUnit
|
|
961
|
+
*/
|
|
962
|
+
entityToCodeUnit(entity) {
|
|
963
|
+
const { ContentNormalizer: ContentNormalizer2 } = (init_content_normalizer(), __toCommonJS(content_normalizer_exports));
|
|
964
|
+
const normalizer = new ContentNormalizer2();
|
|
965
|
+
const content = entity.content || "";
|
|
966
|
+
const contentHash = normalizer.computeContentHash(content);
|
|
967
|
+
return {
|
|
968
|
+
id: entity.id,
|
|
969
|
+
type: this.mapEntityType(entity.type),
|
|
970
|
+
filePath: entity.filePath || "",
|
|
971
|
+
name: entity.name || "",
|
|
972
|
+
fullyQualifiedName: entity.fullyQualifiedName || entity.name || "",
|
|
973
|
+
startLine: entity.startLine || 1,
|
|
974
|
+
endLine: entity.endLine || 1,
|
|
975
|
+
content,
|
|
976
|
+
contentHash,
|
|
977
|
+
structuralHash: entity.structuralHash || contentHash,
|
|
978
|
+
signature: entity.signature,
|
|
979
|
+
language: entity.language || "unknown",
|
|
980
|
+
parentId: entity.parentId,
|
|
981
|
+
childIds: entity.childIds || [],
|
|
982
|
+
metadata: entity.metadata || {}
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Map entity type string to CodeUnitType
|
|
987
|
+
*/
|
|
988
|
+
mapEntityType(type) {
|
|
989
|
+
const { CodeUnitType: CodeUnitType2 } = (init_code_unit(), __toCommonJS(code_unit_exports));
|
|
990
|
+
const typeMap = {
|
|
991
|
+
file: CodeUnitType2.File,
|
|
992
|
+
module: CodeUnitType2.Module,
|
|
993
|
+
class: CodeUnitType2.Class,
|
|
994
|
+
interface: CodeUnitType2.Interface,
|
|
995
|
+
function: CodeUnitType2.Function,
|
|
996
|
+
method: CodeUnitType2.Method,
|
|
997
|
+
property: CodeUnitType2.Property
|
|
998
|
+
};
|
|
999
|
+
return typeMap[type?.toLowerCase()] || CodeUnitType2.Block;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Add a CodeUnit to the index with all hash indexes
|
|
1003
|
+
*/
|
|
1004
|
+
addUnitToIndex(index, unit) {
|
|
1005
|
+
index.units.set(unit.id, unit);
|
|
1006
|
+
if (!index.contentHashIndex.has(unit.contentHash)) {
|
|
1007
|
+
index.contentHashIndex.set(unit.contentHash, []);
|
|
1008
|
+
}
|
|
1009
|
+
index.contentHashIndex.get(unit.contentHash).push(unit.id);
|
|
1010
|
+
if (!index.structuralHashIndex.has(unit.structuralHash)) {
|
|
1011
|
+
index.structuralHashIndex.set(unit.structuralHash, []);
|
|
1012
|
+
}
|
|
1013
|
+
index.structuralHashIndex.get(unit.structuralHash).push(unit.id);
|
|
1014
|
+
if (unit.signature) {
|
|
1015
|
+
if (!index.signatureIndex.has(unit.signature)) {
|
|
1016
|
+
index.signatureIndex.set(unit.signature, []);
|
|
1017
|
+
}
|
|
1018
|
+
index.signatureIndex.get(unit.signature).push(unit.id);
|
|
1019
|
+
}
|
|
1020
|
+
if (!index.filePathIndex.has(unit.filePath)) {
|
|
1021
|
+
index.filePathIndex.set(unit.filePath, []);
|
|
1022
|
+
}
|
|
1023
|
+
index.filePathIndex.get(unit.filePath).push(unit.id);
|
|
1024
|
+
index.stats.totalUnits++;
|
|
1025
|
+
index.stats.byType.set(unit.type, (index.stats.byType.get(unit.type) || 0) + 1);
|
|
1026
|
+
index.stats.byLanguage.set(unit.language, (index.stats.byLanguage.get(unit.language) || 0) + 1);
|
|
1027
|
+
index.stats.byFile.set(unit.filePath, (index.stats.byFile.get(unit.filePath) || 0) + 1);
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Populate index from GraphStorage (fallback when DevAgent result is unavailable)
|
|
1031
|
+
*/
|
|
1032
|
+
async populateIndexFromStorage(index) {
|
|
1033
|
+
try {
|
|
1034
|
+
const storage = this.conductor.getGraphStorage?.();
|
|
1035
|
+
if (!storage) {
|
|
1036
|
+
log.w("MULTIVIDX", "[MultiVersionIndexer] GraphStorage not available for fallback");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const entities = await storage.getAllEntities?.();
|
|
1040
|
+
if (!entities || entities.length === 0) {
|
|
1041
|
+
log.w("MULTIVIDX", "[MultiVersionIndexer] No entities found in GraphStorage");
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
for (const entity of entities) {
|
|
1045
|
+
const codeUnit = this.entityToCodeUnit(entity);
|
|
1046
|
+
this.addUnitToIndex(index, codeUnit);
|
|
1047
|
+
}
|
|
1048
|
+
log.i("MULTIVIDX", "storage_loaded", { units: index.stats.totalUnits });
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
log.e("MULTIVIDX", "storage_populate_fail", { err: String(error) });
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Load cached index for a branch
|
|
1055
|
+
*/
|
|
1056
|
+
async loadCachedIndex(branch) {
|
|
1057
|
+
try {
|
|
1058
|
+
if (!this.branchManager.hasBranchDatabase(branch)) {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
const metadata = this.branchManager.getBranchMetadata(branch);
|
|
1062
|
+
if (!metadata) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
const currentCommit = this.gitIntegration.getCommitHash(branch);
|
|
1066
|
+
if (metadata.lastCommitHash !== currentCommit) {
|
|
1067
|
+
log.i(
|
|
1068
|
+
"MULTIVIDX",
|
|
1069
|
+
`[MultiVersionIndexer] Cache outdated for ${branch} (${metadata.lastCommitHash?.slice(0, 8)} vs ${currentCommit.slice(0, 8)})`
|
|
1070
|
+
);
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
const index = {
|
|
1074
|
+
branch,
|
|
1075
|
+
indexedAt: new Date(metadata.lastIndexedAt),
|
|
1076
|
+
units: /* @__PURE__ */ new Map(),
|
|
1077
|
+
contentHashIndex: /* @__PURE__ */ new Map(),
|
|
1078
|
+
structuralHashIndex: /* @__PURE__ */ new Map(),
|
|
1079
|
+
signatureIndex: /* @__PURE__ */ new Map(),
|
|
1080
|
+
filePathIndex: /* @__PURE__ */ new Map(),
|
|
1081
|
+
stats: {
|
|
1082
|
+
totalUnits: 0,
|
|
1083
|
+
byType: /* @__PURE__ */ new Map(),
|
|
1084
|
+
byLanguage: /* @__PURE__ */ new Map(),
|
|
1085
|
+
byFile: /* @__PURE__ */ new Map()
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
index.stats.totalUnits = metadata.entityCount;
|
|
1089
|
+
index._fromCache = true;
|
|
1090
|
+
return index;
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
log.e("MULTIVIDX", "cache_load_fail", { branch, err: String(error) });
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Save index to branch cache
|
|
1098
|
+
*/
|
|
1099
|
+
async saveCachedIndex(branch, index) {
|
|
1100
|
+
try {
|
|
1101
|
+
const repoPath = this.gitIntegration.repoPath;
|
|
1102
|
+
const commitHash = this.gitIntegration.getCommitHash(branch);
|
|
1103
|
+
const repoHash = this.branchManager.getRepositoryHash(repoPath);
|
|
1104
|
+
this.branchManager.updateBranchMetadata({
|
|
1105
|
+
branch,
|
|
1106
|
+
repositoryPath: repoPath,
|
|
1107
|
+
repositoryHash: repoHash,
|
|
1108
|
+
lastCommitHash: commitHash,
|
|
1109
|
+
lastIndexedAt: Date.now(),
|
|
1110
|
+
fileCount: index.stats.byFile?.size || 0,
|
|
1111
|
+
entityCount: index.stats.totalUnits,
|
|
1112
|
+
relationshipCount: 0,
|
|
1113
|
+
// TODO: Track relationships
|
|
1114
|
+
indexVersion: "1.0.0",
|
|
1115
|
+
accessedAt: Date.now()
|
|
1116
|
+
});
|
|
1117
|
+
log.i("MULTIVIDX", "cache_saved", { branch, commit: commitHash.slice(0, 8) });
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
log.e("MULTIVIDX", "cache_save_fail", { branch, err: String(error) });
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Check if index was loaded from cache
|
|
1124
|
+
*/
|
|
1125
|
+
wasCacheHit(index) {
|
|
1126
|
+
return index._fromCache === true;
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
// src/merge/matching/fast-path-matcher.ts
|
|
1131
|
+
var FastPathMatcher = class {
|
|
1132
|
+
/**
|
|
1133
|
+
* Perform bulk matching between two indices.
|
|
1134
|
+
*
|
|
1135
|
+
* @param baseIndex - Base version index
|
|
1136
|
+
* @param targetIndex - Target version index (branchA or branchB)
|
|
1137
|
+
* @returns Map of targetUnitId → Match result
|
|
1138
|
+
*/
|
|
1139
|
+
bulkMatch(baseIndex, targetIndex) {
|
|
1140
|
+
const results = /* @__PURE__ */ new Map();
|
|
1141
|
+
for (const [targetId, targetUnit] of targetIndex.units) {
|
|
1142
|
+
const match = this.findMatch(targetUnit, baseIndex);
|
|
1143
|
+
if (match) {
|
|
1144
|
+
results.set(targetId, match);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return results;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Find match for a single unit in the base index.
|
|
1151
|
+
*
|
|
1152
|
+
* Tries levels in order: Exact → Structural → Signature → ID
|
|
1153
|
+
*/
|
|
1154
|
+
findMatch(targetUnit, baseIndex) {
|
|
1155
|
+
const exactMatch = this.tryExactMatch(targetUnit, baseIndex);
|
|
1156
|
+
if (exactMatch) {
|
|
1157
|
+
return {
|
|
1158
|
+
baseUnitId: exactMatch.id,
|
|
1159
|
+
baseUnit: exactMatch,
|
|
1160
|
+
level: "exact_content" /* ExactContent */,
|
|
1161
|
+
confidence: 1
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
const structuralMatch = this.tryStructuralMatch(targetUnit, baseIndex);
|
|
1165
|
+
if (structuralMatch) {
|
|
1166
|
+
return {
|
|
1167
|
+
baseUnitId: structuralMatch.id,
|
|
1168
|
+
baseUnit: structuralMatch,
|
|
1169
|
+
level: "structural" /* Structural */,
|
|
1170
|
+
confidence: 0.95
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
if (targetUnit.signature) {
|
|
1174
|
+
const signatureMatch = this.trySignatureMatch(targetUnit, baseIndex);
|
|
1175
|
+
if (signatureMatch) {
|
|
1176
|
+
return {
|
|
1177
|
+
baseUnitId: signatureMatch.id,
|
|
1178
|
+
baseUnit: signatureMatch,
|
|
1179
|
+
level: "signature" /* Signature */,
|
|
1180
|
+
confidence: 0.85
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const idMatch = this.tryIdMatch(targetUnit, baseIndex);
|
|
1185
|
+
if (idMatch) {
|
|
1186
|
+
return {
|
|
1187
|
+
baseUnitId: idMatch.id,
|
|
1188
|
+
baseUnit: idMatch,
|
|
1189
|
+
level: "id" /* Id */,
|
|
1190
|
+
confidence: 0.7
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
return void 0;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Level 1: Exact Content Match.
|
|
1197
|
+
*
|
|
1198
|
+
* Matches if contentHash is identical (byte-for-byte same code).
|
|
1199
|
+
*/
|
|
1200
|
+
tryExactMatch(targetUnit, baseIndex) {
|
|
1201
|
+
const candidates = baseIndex.contentHashIndex.get(targetUnit.contentHash);
|
|
1202
|
+
if (!candidates || candidates.length === 0) {
|
|
1203
|
+
return void 0;
|
|
1204
|
+
}
|
|
1205
|
+
if (candidates.length === 1) {
|
|
1206
|
+
const firstId2 = candidates[0];
|
|
1207
|
+
return firstId2 ? baseIndex.units.get(firstId2) : void 0;
|
|
1208
|
+
}
|
|
1209
|
+
const sameFileCandidate = candidates.find((id) => {
|
|
1210
|
+
const unit = baseIndex.units.get(id);
|
|
1211
|
+
return unit?.filePath === targetUnit.filePath;
|
|
1212
|
+
});
|
|
1213
|
+
if (sameFileCandidate) {
|
|
1214
|
+
return baseIndex.units.get(sameFileCandidate);
|
|
1215
|
+
}
|
|
1216
|
+
const firstId = candidates[0];
|
|
1217
|
+
return firstId ? baseIndex.units.get(firstId) : void 0;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Level 2: Structural Match.
|
|
1221
|
+
*
|
|
1222
|
+
* Matches if structuralHash is identical (same AST structure).
|
|
1223
|
+
* Ignores whitespace, comments, formatting.
|
|
1224
|
+
*/
|
|
1225
|
+
tryStructuralMatch(targetUnit, baseIndex) {
|
|
1226
|
+
const candidates = baseIndex.structuralHashIndex.get(targetUnit.structuralHash);
|
|
1227
|
+
if (!candidates || candidates.length === 0) {
|
|
1228
|
+
return void 0;
|
|
1229
|
+
}
|
|
1230
|
+
const typedCandidates = candidates.filter((id) => {
|
|
1231
|
+
const unit = baseIndex.units.get(id);
|
|
1232
|
+
return unit?.type === targetUnit.type;
|
|
1233
|
+
});
|
|
1234
|
+
if (typedCandidates.length === 0) {
|
|
1235
|
+
return void 0;
|
|
1236
|
+
}
|
|
1237
|
+
const bestCandidate = typedCandidates.find((id) => {
|
|
1238
|
+
const unit = baseIndex.units.get(id);
|
|
1239
|
+
return unit?.filePath === targetUnit.filePath && unit?.name === targetUnit.name;
|
|
1240
|
+
});
|
|
1241
|
+
if (bestCandidate) {
|
|
1242
|
+
return baseIndex.units.get(bestCandidate);
|
|
1243
|
+
}
|
|
1244
|
+
const firstId = typedCandidates[0];
|
|
1245
|
+
return firstId ? baseIndex.units.get(firstId) : void 0;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Level 3: Signature Match.
|
|
1249
|
+
*
|
|
1250
|
+
* Matches if signature is identical (FQN + params for functions).
|
|
1251
|
+
* Useful for renamed/moved code with same signature.
|
|
1252
|
+
*/
|
|
1253
|
+
trySignatureMatch(targetUnit, baseIndex) {
|
|
1254
|
+
if (!targetUnit.signature) {
|
|
1255
|
+
return void 0;
|
|
1256
|
+
}
|
|
1257
|
+
const candidates = baseIndex.signatureIndex.get(targetUnit.signature);
|
|
1258
|
+
if (!candidates || candidates.length === 0) {
|
|
1259
|
+
return void 0;
|
|
1260
|
+
}
|
|
1261
|
+
const typedCandidates = candidates.filter((id) => {
|
|
1262
|
+
const unit = baseIndex.units.get(id);
|
|
1263
|
+
return unit?.type === targetUnit.type;
|
|
1264
|
+
});
|
|
1265
|
+
if (typedCandidates.length === 0) {
|
|
1266
|
+
return void 0;
|
|
1267
|
+
}
|
|
1268
|
+
const firstCandidate = typedCandidates[0];
|
|
1269
|
+
if (!firstCandidate) {
|
|
1270
|
+
return void 0;
|
|
1271
|
+
}
|
|
1272
|
+
return baseIndex.units.get(firstCandidate);
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Level 4: ID Match.
|
|
1276
|
+
*
|
|
1277
|
+
* Matches by stable ID (based on FQN).
|
|
1278
|
+
* Only used as last resort - code may have changed significantly.
|
|
1279
|
+
*/
|
|
1280
|
+
tryIdMatch(targetUnit, baseIndex) {
|
|
1281
|
+
const candidate = baseIndex.units.get(targetUnit.id);
|
|
1282
|
+
if (candidate && candidate.fullyQualifiedName === targetUnit.fullyQualifiedName) {
|
|
1283
|
+
return candidate;
|
|
1284
|
+
}
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Compute Fast Path coverage statistics.
|
|
1289
|
+
*/
|
|
1290
|
+
computeStatistics(matchResults, totalUnits) {
|
|
1291
|
+
const byLevel = /* @__PURE__ */ new Map();
|
|
1292
|
+
for (const result of matchResults.values()) {
|
|
1293
|
+
byLevel.set(result.level, (byLevel.get(result.level) || 0) + 1);
|
|
1294
|
+
}
|
|
1295
|
+
const totalMatched = matchResults.size;
|
|
1296
|
+
const coverage = totalUnits > 0 ? totalMatched / totalUnits : 0;
|
|
1297
|
+
return {
|
|
1298
|
+
totalUnits,
|
|
1299
|
+
totalMatched,
|
|
1300
|
+
coverage,
|
|
1301
|
+
exactContentMatches: byLevel.get("exact_content" /* ExactContent */) || 0,
|
|
1302
|
+
structuralMatches: byLevel.get("structural" /* Structural */) || 0,
|
|
1303
|
+
signatureMatches: byLevel.get("signature" /* Signature */) || 0,
|
|
1304
|
+
idMatches: byLevel.get("id" /* Id */) || 0
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
// src/merge/matching/semantic-matcher.ts
|
|
1310
|
+
var SemanticMatcher = class {
|
|
1311
|
+
/**
|
|
1312
|
+
* Find semantic match for a unit in the base index.
|
|
1313
|
+
*
|
|
1314
|
+
* @param targetUnit - Target unit to match (must have embedding)
|
|
1315
|
+
* @param baseIndex - Base version index (units must have embeddings)
|
|
1316
|
+
* @param threshold - Minimum similarity threshold (0.0-1.0), default 0.7
|
|
1317
|
+
* @returns Match result or undefined if no match above threshold
|
|
1318
|
+
*/
|
|
1319
|
+
findMatch(targetUnit, baseIndex, threshold = 0.7) {
|
|
1320
|
+
if (!targetUnit.embedding) {
|
|
1321
|
+
throw new Error(`Target unit ${targetUnit.id} missing embedding. Use LazyEmbeddingCache to generate.`);
|
|
1322
|
+
}
|
|
1323
|
+
const candidates = this.findCandidates(targetUnit, baseIndex);
|
|
1324
|
+
if (candidates.length === 0) {
|
|
1325
|
+
return void 0;
|
|
1326
|
+
}
|
|
1327
|
+
const similarities = candidates.map((candidate) => {
|
|
1328
|
+
const vectorSim = this.computeVectorSimilarity(targetUnit.embedding, candidate.embedding);
|
|
1329
|
+
const structuralSim = this.computeStructuralSimilarity(targetUnit, candidate);
|
|
1330
|
+
const combinedScore = vectorSim * 0.7 + structuralSim * 0.3;
|
|
1331
|
+
return {
|
|
1332
|
+
candidate,
|
|
1333
|
+
vectorSimilarity: vectorSim,
|
|
1334
|
+
structuralSimilarity: structuralSim,
|
|
1335
|
+
combinedScore
|
|
1336
|
+
};
|
|
1337
|
+
});
|
|
1338
|
+
similarities.sort((a, b) => b.combinedScore - a.combinedScore);
|
|
1339
|
+
const best = similarities[0];
|
|
1340
|
+
if (!best || best.combinedScore < threshold) {
|
|
1341
|
+
return void 0;
|
|
1342
|
+
}
|
|
1343
|
+
return {
|
|
1344
|
+
baseUnitId: best.candidate.id,
|
|
1345
|
+
baseUnit: best.candidate,
|
|
1346
|
+
vectorSimilarity: best.vectorSimilarity,
|
|
1347
|
+
structuralSimilarity: best.structuralSimilarity,
|
|
1348
|
+
combinedScore: best.combinedScore,
|
|
1349
|
+
confidence: this.scoreToConfidence(best.combinedScore)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Bulk semantic matching for multiple units.
|
|
1354
|
+
*
|
|
1355
|
+
* @param targetUnits - Units to match (with embeddings)
|
|
1356
|
+
* @param baseIndex - Base index (units with embeddings)
|
|
1357
|
+
* @param threshold - Minimum similarity threshold
|
|
1358
|
+
* @returns Map of targetUnitId → Match result
|
|
1359
|
+
*/
|
|
1360
|
+
bulkMatch(targetUnits, baseIndex, threshold = 0.7) {
|
|
1361
|
+
const results = /* @__PURE__ */ new Map();
|
|
1362
|
+
for (const targetUnit of targetUnits) {
|
|
1363
|
+
if (!targetUnit.embedding) {
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
const match = this.findMatch(targetUnit, baseIndex, threshold);
|
|
1367
|
+
if (match) {
|
|
1368
|
+
results.set(targetUnit.id, match);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return results;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Find candidates in the base index (units with embeddings of the same type).
|
|
1375
|
+
*/
|
|
1376
|
+
findCandidates(targetUnit, baseIndex) {
|
|
1377
|
+
const candidates = [];
|
|
1378
|
+
for (const [, baseUnit] of baseIndex.units) {
|
|
1379
|
+
if (baseUnit.type === targetUnit.type && baseUnit.embedding) {
|
|
1380
|
+
candidates.push(baseUnit);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return candidates;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Compute vector similarity with GPU acceleration.
|
|
1387
|
+
*
|
|
1388
|
+
* Uses SIMD/WASM cosine similarity.
|
|
1389
|
+
*/
|
|
1390
|
+
computeVectorSimilarity(embedding1, embedding2) {
|
|
1391
|
+
return cosineSimilarity(embedding1, embedding2);
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Compute structural similarity (based on hashes).
|
|
1395
|
+
*
|
|
1396
|
+
* Simple metric: matching structuralHash = 1.0, otherwise 0.0
|
|
1397
|
+
* (can be improved using edit distance)
|
|
1398
|
+
*/
|
|
1399
|
+
computeStructuralSimilarity(unit1, unit2) {
|
|
1400
|
+
if (unit1.structuralHash === unit2.structuralHash) {
|
|
1401
|
+
return 1;
|
|
1402
|
+
}
|
|
1403
|
+
return 0;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Convert combined score to confidence (0.0-1.0).
|
|
1407
|
+
*
|
|
1408
|
+
* Mapping:
|
|
1409
|
+
* - 0.9-1.0 → 0.9-1.0 (very high confidence)
|
|
1410
|
+
* - 0.8-0.9 → 0.75-0.9 (high confidence)
|
|
1411
|
+
* - 0.7-0.8 → 0.6-0.75 (medium confidence)
|
|
1412
|
+
*/
|
|
1413
|
+
scoreToConfidence(score) {
|
|
1414
|
+
if (score >= 0.9) {
|
|
1415
|
+
return score;
|
|
1416
|
+
}
|
|
1417
|
+
if (score >= 0.8) {
|
|
1418
|
+
return 0.75 + (score - 0.8) * 1.5;
|
|
1419
|
+
}
|
|
1420
|
+
return 0.6 + (score - 0.7) * 1.5;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Compute Semantic Path statistics.
|
|
1424
|
+
*/
|
|
1425
|
+
computeStatistics(matchResults, totalUnits) {
|
|
1426
|
+
const totalMatched = matchResults.size;
|
|
1427
|
+
const coverage = totalUnits > 0 ? totalMatched / totalUnits : 0;
|
|
1428
|
+
let sumVector = 0;
|
|
1429
|
+
let sumStructural = 0;
|
|
1430
|
+
let sumCombined = 0;
|
|
1431
|
+
for (const result of matchResults.values()) {
|
|
1432
|
+
sumVector += result.vectorSimilarity;
|
|
1433
|
+
sumStructural += result.structuralSimilarity;
|
|
1434
|
+
sumCombined += result.combinedScore;
|
|
1435
|
+
}
|
|
1436
|
+
const count = totalMatched || 1;
|
|
1437
|
+
return {
|
|
1438
|
+
totalUnits,
|
|
1439
|
+
totalMatched,
|
|
1440
|
+
coverage,
|
|
1441
|
+
avgVectorSimilarity: sumVector / count,
|
|
1442
|
+
avgStructuralSimilarity: sumStructural / count,
|
|
1443
|
+
avgCombinedScore: sumCombined / count
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
// src/merge/engine/three-way-merger.ts
|
|
1449
|
+
var ThreeWayMerger = class {
|
|
1450
|
+
branchManager;
|
|
1451
|
+
gitIntegration;
|
|
1452
|
+
conductor;
|
|
1453
|
+
config;
|
|
1454
|
+
// Components
|
|
1455
|
+
multiVersionIndexer;
|
|
1456
|
+
fastPathMatcher;
|
|
1457
|
+
semanticMatcher;
|
|
1458
|
+
intentClassifier;
|
|
1459
|
+
conflictDetector;
|
|
1460
|
+
constructor(_branchManager, _gitIntegration, _conductor, config = {}) {
|
|
1461
|
+
this.branchManager = _branchManager;
|
|
1462
|
+
this.gitIntegration = _gitIntegration;
|
|
1463
|
+
this.conductor = _conductor;
|
|
1464
|
+
this.config = {
|
|
1465
|
+
fastPathEnabled: true,
|
|
1466
|
+
semanticMatchingEnabled: true,
|
|
1467
|
+
semanticThreshold: 0.7,
|
|
1468
|
+
classifyIntents: true,
|
|
1469
|
+
detectConflicts: true,
|
|
1470
|
+
autoResolveConflicts: false,
|
|
1471
|
+
...config
|
|
1472
|
+
};
|
|
1473
|
+
this.multiVersionIndexer = new MultiVersionIndexer(this.branchManager, this.gitIntegration, this.conductor);
|
|
1474
|
+
this.fastPathMatcher = new FastPathMatcher();
|
|
1475
|
+
this.semanticMatcher = new SemanticMatcher();
|
|
1476
|
+
this.intentClassifier = new IntentClassifier();
|
|
1477
|
+
this.conflictDetector = new ConflictDetector();
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Perform 3-way merge
|
|
1481
|
+
*
|
|
1482
|
+
* @param branchA - Name of the first branch to merge
|
|
1483
|
+
* @param branchB - Name of the second branch to merge
|
|
1484
|
+
* @returns MergeResult with matched units, conflicts, and merge actions
|
|
1485
|
+
*/
|
|
1486
|
+
async performMerge(branchA, branchB) {
|
|
1487
|
+
log.i("3WAYMERGE", `[ThreeWayMerger] Starting 3-way merge: ${branchA} + ${branchB}`);
|
|
1488
|
+
const startTime = Date.now();
|
|
1489
|
+
log.i("3WAYMERGE", "[ThreeWayMerger] Phase 1: Indexing 3 branches...");
|
|
1490
|
+
const indexResult = await this.multiVersionIndexer.indexThreeBranches(branchA, branchB);
|
|
1491
|
+
const { base, branchA: indexA, branchB: indexB, mergeBase } = indexResult;
|
|
1492
|
+
log.i(
|
|
1493
|
+
"3WAYMERGE",
|
|
1494
|
+
`[ThreeWayMerger] Indexed: base=${base.stats.totalUnits}, A=${indexA.stats.totalUnits}, B=${indexB.stats.totalUnits}`
|
|
1495
|
+
);
|
|
1496
|
+
log.i("3WAYMERGE", "[ThreeWayMerger] Phase 1.5: Detecting file changes from git...");
|
|
1497
|
+
const changesA = await this.gitIntegration.getChangedFilesBetween(mergeBase, branchA);
|
|
1498
|
+
const changesB = await this.gitIntegration.getChangedFilesBetween(mergeBase, branchB);
|
|
1499
|
+
const renamedInA = changesA.filter((c) => c.status === "renamed");
|
|
1500
|
+
const renamedInB = changesB.filter((c) => c.status === "renamed");
|
|
1501
|
+
log.i(
|
|
1502
|
+
"3WAYMERGE",
|
|
1503
|
+
`[ThreeWayMerger] Git changes: A=${changesA.length} (${renamedInA.length} renamed), B=${changesB.length} (${renamedInB.length} renamed)`
|
|
1504
|
+
);
|
|
1505
|
+
log.i("3WAYMERGE", "[ThreeWayMerger] Phase 2: Fast Path matching...");
|
|
1506
|
+
const { matchedUnits, unmatchedA, unmatchedB } = await this.fastPathMatch(base, indexA, indexB);
|
|
1507
|
+
log.i(
|
|
1508
|
+
"3WAYMERGE",
|
|
1509
|
+
`[ThreeWayMerger] Fast Path: ${matchedUnits.length} matched, ${unmatchedA.length} unmapped in A, ${unmatchedB.length} unmapped in B`
|
|
1510
|
+
);
|
|
1511
|
+
log.i("3WAYMERGE", "[ThreeWayMerger] Phase 2.5: Detecting added/deleted units...");
|
|
1512
|
+
const { addedInA, addedInB, deletedUnits, renamedUnits } = this.detectAddedDeletedRenamed(
|
|
1513
|
+
base,
|
|
1514
|
+
indexA,
|
|
1515
|
+
indexB,
|
|
1516
|
+
unmatchedA,
|
|
1517
|
+
unmatchedB,
|
|
1518
|
+
renamedInA,
|
|
1519
|
+
renamedInB
|
|
1520
|
+
);
|
|
1521
|
+
log.i(
|
|
1522
|
+
"3WAYMERGE",
|
|
1523
|
+
`[ThreeWayMerger] Added: A=${addedInA.length}, B=${addedInB.length}, Deleted=${deletedUnits.length}, Renamed=${renamedUnits.length}`
|
|
1524
|
+
);
|
|
1525
|
+
const addedIds = /* @__PURE__ */ new Set([...addedInA.map((u) => u.id), ...addedInB.map((u) => u.id)]);
|
|
1526
|
+
const renamedIds = new Set(renamedUnits.map((r) => r.unit.id));
|
|
1527
|
+
const remainingUnmatchedA = unmatchedA.filter((u) => !addedIds.has(u.id) && !renamedIds.has(u.id));
|
|
1528
|
+
const remainingUnmatchedB = unmatchedB.filter((u) => !addedIds.has(u.id) && !renamedIds.has(u.id));
|
|
1529
|
+
let semanticMatches = [];
|
|
1530
|
+
if (this.config.semanticMatchingEnabled && (remainingUnmatchedA.length > 0 || remainingUnmatchedB.length > 0)) {
|
|
1531
|
+
log.i("3WAYMERGE", "[ThreeWayMerger] Phase 3: Semantic matching...");
|
|
1532
|
+
semanticMatches = await this.semanticMatch(base, remainingUnmatchedA, remainingUnmatchedB);
|
|
1533
|
+
log.i("3WAYMERGE", `[ThreeWayMerger] Semantic: ${semanticMatches.length} matched`);
|
|
1534
|
+
}
|
|
1535
|
+
const allMatches = [...matchedUnits, ...semanticMatches];
|
|
1536
|
+
let intents = /* @__PURE__ */ new Map();
|
|
1537
|
+
if (this.config.classifyIntents) {
|
|
1538
|
+
log.i("3WAYMERGE", "[ThreeWayMerger] Phase 4: Classifying intents...");
|
|
1539
|
+
intents = this.classifyIntents(allMatches);
|
|
1540
|
+
log.i("3WAYMERGE", `[ThreeWayMerger] Classified intents for ${intents.size} unit pairs`);
|
|
1541
|
+
}
|
|
1542
|
+
let conflicts = [];
|
|
1543
|
+
if (this.config.detectConflicts) {
|
|
1544
|
+
log.i("3WAYMERGE", "[ThreeWayMerger] Phase 5: Detecting conflicts...");
|
|
1545
|
+
conflicts = this.detectConflicts(allMatches, intents);
|
|
1546
|
+
const deleteModifyConflicts = this.detectDeleteModifyConflicts(deletedUnits);
|
|
1547
|
+
conflicts = [...conflicts, ...deleteModifyConflicts];
|
|
1548
|
+
log.i("3WAYMERGE", `[ThreeWayMerger] Detected ${conflicts.length} conflicts`);
|
|
1549
|
+
}
|
|
1550
|
+
const mergeActions = this.generateMergeActions(
|
|
1551
|
+
allMatches,
|
|
1552
|
+
conflicts,
|
|
1553
|
+
addedInA,
|
|
1554
|
+
addedInB,
|
|
1555
|
+
deletedUnits,
|
|
1556
|
+
renamedUnits
|
|
1557
|
+
);
|
|
1558
|
+
const stats = {
|
|
1559
|
+
totalUnitsInBase: base.stats.totalUnits,
|
|
1560
|
+
totalUnitsInA: indexA.stats.totalUnits,
|
|
1561
|
+
totalUnitsInB: indexB.stats.totalUnits,
|
|
1562
|
+
matchedCount: allMatches.length,
|
|
1563
|
+
conflictCount: conflicts.length,
|
|
1564
|
+
autoMergedCount: mergeActions.filter((a) => a.type === "auto-merge").length,
|
|
1565
|
+
manualReviewCount: mergeActions.filter((a) => a.type === "manual-review").length,
|
|
1566
|
+
addedFromACount: addedInA.length,
|
|
1567
|
+
addedFromBCount: addedInB.length,
|
|
1568
|
+
deletedCount: deletedUnits.length,
|
|
1569
|
+
renamedCount: renamedUnits.length,
|
|
1570
|
+
mergeTimeMs: Date.now() - startTime
|
|
1571
|
+
};
|
|
1572
|
+
const result = {
|
|
1573
|
+
branchA,
|
|
1574
|
+
branchB,
|
|
1575
|
+
mergeBase,
|
|
1576
|
+
baseIndex: base,
|
|
1577
|
+
branchAIndex: indexA,
|
|
1578
|
+
branchBIndex: indexB,
|
|
1579
|
+
matchedUnits: allMatches,
|
|
1580
|
+
addedInA,
|
|
1581
|
+
addedInB,
|
|
1582
|
+
deletedUnits,
|
|
1583
|
+
renamedUnits,
|
|
1584
|
+
conflicts,
|
|
1585
|
+
mergeActions,
|
|
1586
|
+
stats
|
|
1587
|
+
};
|
|
1588
|
+
log.i(
|
|
1589
|
+
"3WAYMERGE",
|
|
1590
|
+
`[ThreeWayMerger] Merge completed in ${stats.mergeTimeMs}ms: ${stats.autoMergedCount} auto-merged, ${stats.conflictCount} conflicts, ${stats.addedFromACount + stats.addedFromBCount} added, ${stats.deletedCount} deleted, ${stats.renamedCount} renamed`
|
|
1591
|
+
);
|
|
1592
|
+
return result;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Phase 2: Fast Path Matching
|
|
1596
|
+
*/
|
|
1597
|
+
async fastPathMatch(base, indexA, indexB) {
|
|
1598
|
+
const matchedUnits = [];
|
|
1599
|
+
const unmatchedA = [];
|
|
1600
|
+
const unmatchedB = [];
|
|
1601
|
+
const unitsA = Array.from(indexA.units.values());
|
|
1602
|
+
const unitsB = Array.from(indexB.units.values());
|
|
1603
|
+
for (const unitA of unitsA) {
|
|
1604
|
+
const matchResult = this.fastPathMatcher.findMatch(unitA, indexB);
|
|
1605
|
+
if (matchResult) {
|
|
1606
|
+
const baseUnit = base.units.get(unitA.id) || null;
|
|
1607
|
+
matchedUnits.push({
|
|
1608
|
+
baseUnit,
|
|
1609
|
+
branchAUnit: unitA,
|
|
1610
|
+
branchBUnit: matchResult.baseUnit
|
|
1611
|
+
});
|
|
1612
|
+
} else {
|
|
1613
|
+
unmatchedA.push(unitA);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const matchedBIds = new Set(matchedUnits.map((m) => m.branchBUnit.id));
|
|
1617
|
+
for (const unitB of unitsB) {
|
|
1618
|
+
if (!matchedBIds.has(unitB.id)) {
|
|
1619
|
+
unmatchedB.push(unitB);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return { matchedUnits, unmatchedA, unmatchedB };
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Phase 3: Semantic Matching
|
|
1626
|
+
*/
|
|
1627
|
+
async semanticMatch(base, unmatchedA, unmatchedB) {
|
|
1628
|
+
if (this.config.embeddingGenerator) {
|
|
1629
|
+
const cache = new LazyEmbeddingCache(this.config.embeddingGenerator);
|
|
1630
|
+
await cache.generateEmbeddings(base, unmatchedA);
|
|
1631
|
+
await cache.generateEmbeddings(base, unmatchedB);
|
|
1632
|
+
}
|
|
1633
|
+
const matches = [];
|
|
1634
|
+
const tempIndexB = {
|
|
1635
|
+
...base,
|
|
1636
|
+
units: new Map(unmatchedB.map((u) => [u.id, u]))
|
|
1637
|
+
};
|
|
1638
|
+
for (const unitA of unmatchedA) {
|
|
1639
|
+
const matchResult = this.semanticMatcher.findMatch(unitA, tempIndexB, this.config.semanticThreshold);
|
|
1640
|
+
if (matchResult) {
|
|
1641
|
+
const baseUnit = base.units.get(unitA.id) || null;
|
|
1642
|
+
matches.push({
|
|
1643
|
+
baseUnit,
|
|
1644
|
+
branchAUnit: unitA,
|
|
1645
|
+
branchBUnit: matchResult.baseUnit
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return matches;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Phase 4: Intent Classification
|
|
1653
|
+
*/
|
|
1654
|
+
classifyIntents(matches) {
|
|
1655
|
+
const intents = /* @__PURE__ */ new Map();
|
|
1656
|
+
for (const match of matches) {
|
|
1657
|
+
const branchAIntent = this.intentClassifier.classifyIntent(match.baseUnit, match.branchAUnit);
|
|
1658
|
+
const branchBIntent = this.intentClassifier.classifyIntent(match.baseUnit, match.branchBUnit);
|
|
1659
|
+
intents.set(match.branchAUnit.id, { branchAIntent, branchBIntent });
|
|
1660
|
+
}
|
|
1661
|
+
return intents;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Phase 5: Conflict Detection
|
|
1665
|
+
*/
|
|
1666
|
+
detectConflicts(matches, intents) {
|
|
1667
|
+
const conflictsToDetect = matches.map((match) => {
|
|
1668
|
+
const intentPair = intents.get(match.branchAUnit.id);
|
|
1669
|
+
return {
|
|
1670
|
+
...match,
|
|
1671
|
+
branchAIntent: intentPair?.branchAIntent,
|
|
1672
|
+
branchBIntent: intentPair?.branchBIntent
|
|
1673
|
+
};
|
|
1674
|
+
});
|
|
1675
|
+
return this.conflictDetector.detectConflicts(conflictsToDetect);
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Phase 2.5: Detect added, deleted, and renamed units
|
|
1679
|
+
*/
|
|
1680
|
+
detectAddedDeletedRenamed(base, indexA, indexB, unmatchedA, unmatchedB, renamedInA, renamedInB) {
|
|
1681
|
+
const addedInA = [];
|
|
1682
|
+
const addedInB = [];
|
|
1683
|
+
const deletedUnits = [];
|
|
1684
|
+
const renamedUnits = [];
|
|
1685
|
+
const renameMapA = new Map(renamedInA.map((r) => [r.oldPath, r.path]));
|
|
1686
|
+
const renameMapB = new Map(renamedInB.map((r) => [r.oldPath, r.path]));
|
|
1687
|
+
const renameNewPathsA = new Set(renamedInA.map((r) => r.path));
|
|
1688
|
+
const renameNewPathsB = new Set(renamedInB.map((r) => r.path));
|
|
1689
|
+
for (const unit of unmatchedA) {
|
|
1690
|
+
const existsInBase = base.units.has(unit.id) || unit.filePath && this.findUnitByPath(base, unit.filePath);
|
|
1691
|
+
if (!existsInBase) {
|
|
1692
|
+
const isRenameDestination = unit.filePath && renameNewPathsA.has(unit.filePath);
|
|
1693
|
+
if (isRenameDestination) {
|
|
1694
|
+
for (const [oldPath, newPath] of renameMapA) {
|
|
1695
|
+
if (newPath === unit.filePath && oldPath) {
|
|
1696
|
+
renamedUnits.push({ oldPath, newPath, unit, branch: "branchA" });
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
} else {
|
|
1701
|
+
addedInA.push(unit);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
for (const unit of unmatchedB) {
|
|
1706
|
+
const existsInBase = base.units.has(unit.id) || unit.filePath && this.findUnitByPath(base, unit.filePath);
|
|
1707
|
+
if (!existsInBase) {
|
|
1708
|
+
const isRenameDestination = unit.filePath && renameNewPathsB.has(unit.filePath);
|
|
1709
|
+
if (isRenameDestination) {
|
|
1710
|
+
for (const [oldPath, newPath] of renameMapB) {
|
|
1711
|
+
if (newPath === unit.filePath && oldPath) {
|
|
1712
|
+
renamedUnits.push({ oldPath, newPath, unit, branch: "branchB" });
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
} else {
|
|
1717
|
+
addedInB.push(unit);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
for (const [unitId, baseUnit] of base.units) {
|
|
1722
|
+
const existsInA = indexA.units.has(unitId) || this.findUnitByPath(indexA, baseUnit.filePath);
|
|
1723
|
+
const existsInB = indexB.units.has(unitId) || this.findUnitByPath(indexB, baseUnit.filePath);
|
|
1724
|
+
const isRenamedInA = renameMapA.has(baseUnit.filePath);
|
|
1725
|
+
const isRenamedInB = renameMapB.has(baseUnit.filePath);
|
|
1726
|
+
if (!existsInA && !isRenamedInA && existsInB) {
|
|
1727
|
+
const unitInB = indexB.units.get(unitId) || this.findUnitByPath(indexB, baseUnit.filePath);
|
|
1728
|
+
const modifiedInB = unitInB && unitInB.contentHash !== baseUnit.contentHash;
|
|
1729
|
+
deletedUnits.push({
|
|
1730
|
+
baseUnit,
|
|
1731
|
+
deletedIn: "branchA",
|
|
1732
|
+
modifiedIn: modifiedInB ? "branchB" : void 0
|
|
1733
|
+
});
|
|
1734
|
+
} else if (!existsInB && !isRenamedInB && existsInA) {
|
|
1735
|
+
const unitInA = indexA.units.get(unitId) || this.findUnitByPath(indexA, baseUnit.filePath);
|
|
1736
|
+
const modifiedInA = unitInA && unitInA.contentHash !== baseUnit.contentHash;
|
|
1737
|
+
deletedUnits.push({
|
|
1738
|
+
baseUnit,
|
|
1739
|
+
deletedIn: "branchB",
|
|
1740
|
+
modifiedIn: modifiedInA ? "branchA" : void 0
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return { addedInA, addedInB, deletedUnits, renamedUnits };
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Find unit by file path in an index
|
|
1748
|
+
*/
|
|
1749
|
+
findUnitByPath(index, filePath) {
|
|
1750
|
+
if (!filePath) return void 0;
|
|
1751
|
+
const unitIds = index.filePathIndex.get(filePath);
|
|
1752
|
+
if (unitIds && unitIds.length > 0 && unitIds[0]) {
|
|
1753
|
+
return index.units.get(unitIds[0]);
|
|
1754
|
+
}
|
|
1755
|
+
return void 0;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Detect delete-modify conflicts
|
|
1759
|
+
*/
|
|
1760
|
+
detectDeleteModifyConflicts(deletedUnits) {
|
|
1761
|
+
const conflicts = [];
|
|
1762
|
+
for (const deleted of deletedUnits) {
|
|
1763
|
+
if (deleted.modifiedIn) {
|
|
1764
|
+
conflicts.push({
|
|
1765
|
+
id: `conflict-delete-modify-${deleted.baseUnit.id}`,
|
|
1766
|
+
type: "DeleteModify" /* DeleteModify */,
|
|
1767
|
+
severity: "High" /* High */,
|
|
1768
|
+
description: `File deleted in ${deleted.deletedIn} but modified in ${deleted.modifiedIn}`,
|
|
1769
|
+
baseUnit: deleted.baseUnit,
|
|
1770
|
+
branchAUnit: deleted.baseUnit,
|
|
1771
|
+
// Use base as placeholder
|
|
1772
|
+
branchBUnit: deleted.baseUnit,
|
|
1773
|
+
conflictingRegions: [],
|
|
1774
|
+
autoResolvable: false
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
return conflicts;
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Generate Merge Actions
|
|
1782
|
+
*/
|
|
1783
|
+
generateMergeActions(matches, conflicts, addedInA, addedInB, deletedUnits, renamedUnits) {
|
|
1784
|
+
const actions = [];
|
|
1785
|
+
const conflictIds = new Set(conflicts.map((c) => c.branchAUnit.id));
|
|
1786
|
+
for (const match of matches) {
|
|
1787
|
+
if (conflictIds.has(match.branchAUnit.id)) {
|
|
1788
|
+
const conflict = conflicts.find((c) => c.branchAUnit.id === match.branchAUnit.id);
|
|
1789
|
+
actions.push({
|
|
1790
|
+
type: "manual-review",
|
|
1791
|
+
unitId: match.branchAUnit.id,
|
|
1792
|
+
description: `Conflict detected: ${conflict?.description}`,
|
|
1793
|
+
conflict
|
|
1794
|
+
});
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
if (match.branchAUnit.contentHash === match.branchBUnit.contentHash) {
|
|
1798
|
+
actions.push({
|
|
1799
|
+
type: "auto-merge",
|
|
1800
|
+
unitId: match.branchAUnit.id,
|
|
1801
|
+
description: "Identical changes in both branches",
|
|
1802
|
+
mergedUnit: match.branchAUnit
|
|
1803
|
+
// Use either (they're identical)
|
|
1804
|
+
});
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
actions.push({
|
|
1808
|
+
type: "manual-review",
|
|
1809
|
+
unitId: match.branchAUnit.id,
|
|
1810
|
+
description: "Different changes in both branches"
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
for (const unit of addedInA) {
|
|
1814
|
+
actions.push({
|
|
1815
|
+
type: "add-from-branchA",
|
|
1816
|
+
unitId: unit.id,
|
|
1817
|
+
description: `New file added in branchA: ${unit.filePath}`,
|
|
1818
|
+
mergedUnit: unit,
|
|
1819
|
+
sourceBranch: "branchA"
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
for (const unit of addedInB) {
|
|
1823
|
+
actions.push({
|
|
1824
|
+
type: "add-from-branchB",
|
|
1825
|
+
unitId: unit.id,
|
|
1826
|
+
description: `New file added in branchB: ${unit.filePath}`,
|
|
1827
|
+
mergedUnit: unit,
|
|
1828
|
+
sourceBranch: "branchB"
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
for (const deleted of deletedUnits) {
|
|
1832
|
+
if (deleted.modifiedIn) {
|
|
1833
|
+
actions.push({
|
|
1834
|
+
type: "conflict-delete-modify",
|
|
1835
|
+
unitId: deleted.baseUnit.id,
|
|
1836
|
+
description: `Conflict: deleted in ${deleted.deletedIn}, modified in ${deleted.modifiedIn}`
|
|
1837
|
+
});
|
|
1838
|
+
} else {
|
|
1839
|
+
actions.push({
|
|
1840
|
+
type: deleted.deletedIn === "branchA" ? "delete-from-branchA" : "delete-from-branchB",
|
|
1841
|
+
unitId: deleted.baseUnit.id,
|
|
1842
|
+
description: `File deleted in ${deleted.deletedIn}: ${deleted.baseUnit.filePath}`
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
for (const renamed of renamedUnits) {
|
|
1847
|
+
actions.push({
|
|
1848
|
+
type: "rename",
|
|
1849
|
+
unitId: renamed.unit.id,
|
|
1850
|
+
description: `File renamed in ${renamed.branch}: ${renamed.oldPath} -> ${renamed.newPath}`,
|
|
1851
|
+
mergedUnit: renamed.unit,
|
|
1852
|
+
renameInfo: {
|
|
1853
|
+
oldPath: renamed.oldPath,
|
|
1854
|
+
newPath: renamed.newPath,
|
|
1855
|
+
sourceBranch: renamed.branch
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
return actions;
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
// src/merge/integration/git-integration.ts
|
|
1864
|
+
init_logging();
|
|
1865
|
+
var GitIntegration = class {
|
|
1866
|
+
config;
|
|
1867
|
+
originalBranch = null;
|
|
1868
|
+
constructor(config) {
|
|
1869
|
+
this.config = {
|
|
1870
|
+
allowDetachedHead: false,
|
|
1871
|
+
restoreOnError: true,
|
|
1872
|
+
...config
|
|
1873
|
+
};
|
|
1874
|
+
if (!this.isGitRepository()) {
|
|
1875
|
+
throw new Error(`Not a git repository: ${this.config.repoPath}`);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Get repository path
|
|
1880
|
+
* Public getter to avoid intersection type issues with private config field
|
|
1881
|
+
*/
|
|
1882
|
+
get repoPath() {
|
|
1883
|
+
return this.config.repoPath;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Check if path is a git repository
|
|
1887
|
+
*/
|
|
1888
|
+
isGitRepository() {
|
|
1889
|
+
const gitDir = join(this.config.repoPath, ".git");
|
|
1890
|
+
return existsSync(gitDir);
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Get current branch info
|
|
1894
|
+
*/
|
|
1895
|
+
getCurrentBranch() {
|
|
1896
|
+
try {
|
|
1897
|
+
const branch = execSync("git symbolic-ref --short HEAD", {
|
|
1898
|
+
cwd: this.config.repoPath,
|
|
1899
|
+
encoding: "utf-8",
|
|
1900
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1901
|
+
windowsHide: true
|
|
1902
|
+
}).trim();
|
|
1903
|
+
const commitHash = this.getCommitHash("HEAD");
|
|
1904
|
+
return {
|
|
1905
|
+
name: branch,
|
|
1906
|
+
commitHash,
|
|
1907
|
+
shortHash: commitHash.slice(0, 8),
|
|
1908
|
+
isDetached: false
|
|
1909
|
+
};
|
|
1910
|
+
} catch {
|
|
1911
|
+
const commitHash = this.getCommitHash("HEAD");
|
|
1912
|
+
return {
|
|
1913
|
+
name: `detached-${commitHash.slice(0, 8)}`,
|
|
1914
|
+
commitHash,
|
|
1915
|
+
shortHash: commitHash.slice(0, 8),
|
|
1916
|
+
isDetached: true
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Get commit hash for a reference (branch, tag, commit)
|
|
1922
|
+
*/
|
|
1923
|
+
getCommitHash(ref) {
|
|
1924
|
+
try {
|
|
1925
|
+
const hash = execSync(`git rev-parse ${ref}`, {
|
|
1926
|
+
cwd: this.config.repoPath,
|
|
1927
|
+
encoding: "utf-8",
|
|
1928
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1929
|
+
windowsHide: true
|
|
1930
|
+
}).trim();
|
|
1931
|
+
return hash;
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
throw new Error(`Failed to get commit hash for ${ref}`, { cause: error });
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Check if a branch exists
|
|
1938
|
+
*/
|
|
1939
|
+
branchExists(branch) {
|
|
1940
|
+
try {
|
|
1941
|
+
execSync(`git rev-parse --verify ${branch}`, {
|
|
1942
|
+
cwd: this.config.repoPath,
|
|
1943
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1944
|
+
windowsHide: true
|
|
1945
|
+
});
|
|
1946
|
+
return true;
|
|
1947
|
+
} catch {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Safely checkout a branch
|
|
1953
|
+
*
|
|
1954
|
+
* Saves current branch, checks out target, handles errors.
|
|
1955
|
+
*/
|
|
1956
|
+
async checkoutBranch(branch) {
|
|
1957
|
+
if (!this.originalBranch) {
|
|
1958
|
+
this.originalBranch = this.getCurrentBranch().name;
|
|
1959
|
+
}
|
|
1960
|
+
if (!this.branchExists(branch)) {
|
|
1961
|
+
throw new Error(`Branch does not exist: ${branch}`);
|
|
1962
|
+
}
|
|
1963
|
+
if (this.hasUncommittedChanges()) {
|
|
1964
|
+
throw new Error("Cannot checkout branch: uncommitted changes detected. Please commit or stash changes first.");
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
execSync(`git checkout ${branch}`, {
|
|
1968
|
+
cwd: this.config.repoPath,
|
|
1969
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1970
|
+
windowsHide: true
|
|
1971
|
+
});
|
|
1972
|
+
log.i("GITINTEGR", `[GitIntegration] Checked out branch: ${branch}`);
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
if (this.config.restoreOnError && this.originalBranch) {
|
|
1975
|
+
await this.restoreOriginalBranch();
|
|
1976
|
+
}
|
|
1977
|
+
throw new Error(`Failed to checkout branch ${branch}: ${error}`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Restore original branch (before merge operations)
|
|
1982
|
+
*/
|
|
1983
|
+
async restoreOriginalBranch() {
|
|
1984
|
+
if (!this.originalBranch) {
|
|
1985
|
+
log.w("GITINTEGR", "[GitIntegration] No original branch to restore");
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
try {
|
|
1989
|
+
execSync(`git checkout ${this.originalBranch}`, {
|
|
1990
|
+
cwd: this.config.repoPath,
|
|
1991
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1992
|
+
windowsHide: true
|
|
1993
|
+
});
|
|
1994
|
+
log.i("GITINTEGR", `[GitIntegration] Restored original branch: ${this.originalBranch}`);
|
|
1995
|
+
this.originalBranch = null;
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
log.i("GITINTEGR", `[GitIntegration] Failed to restore branch: ${error}`);
|
|
1998
|
+
throw error;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Check if there are uncommitted changes
|
|
2003
|
+
*/
|
|
2004
|
+
hasUncommittedChanges() {
|
|
2005
|
+
try {
|
|
2006
|
+
const output = execSync("git status --porcelain", {
|
|
2007
|
+
cwd: this.config.repoPath,
|
|
2008
|
+
encoding: "utf-8",
|
|
2009
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
2010
|
+
windowsHide: true
|
|
2011
|
+
});
|
|
2012
|
+
return output.trim().length > 0;
|
|
2013
|
+
} catch {
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Get changed files between two branches/commits
|
|
2019
|
+
*/
|
|
2020
|
+
async getChangedFilesBetween(base, target) {
|
|
2021
|
+
try {
|
|
2022
|
+
const output = execSync(`git diff --name-status ${base}...${target}`, {
|
|
2023
|
+
cwd: this.config.repoPath,
|
|
2024
|
+
encoding: "utf-8",
|
|
2025
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
2026
|
+
windowsHide: true
|
|
2027
|
+
});
|
|
2028
|
+
const changes = [];
|
|
2029
|
+
const lines = output.trim().split("\n");
|
|
2030
|
+
for (const line of lines) {
|
|
2031
|
+
if (!line) continue;
|
|
2032
|
+
const parts = line.split(" ");
|
|
2033
|
+
const status = parts[0];
|
|
2034
|
+
if (!status) continue;
|
|
2035
|
+
let change;
|
|
2036
|
+
switch (status[0]) {
|
|
2037
|
+
case "A":
|
|
2038
|
+
change = { path: parts[1], status: "added" };
|
|
2039
|
+
break;
|
|
2040
|
+
case "M":
|
|
2041
|
+
change = { path: parts[1], status: "modified" };
|
|
2042
|
+
break;
|
|
2043
|
+
case "D":
|
|
2044
|
+
change = { path: parts[1], status: "deleted" };
|
|
2045
|
+
break;
|
|
2046
|
+
case "R":
|
|
2047
|
+
change = {
|
|
2048
|
+
path: parts[2],
|
|
2049
|
+
status: "renamed",
|
|
2050
|
+
oldPath: parts[1]
|
|
2051
|
+
};
|
|
2052
|
+
break;
|
|
2053
|
+
default:
|
|
2054
|
+
change = { path: parts[1], status: "modified" };
|
|
2055
|
+
}
|
|
2056
|
+
changes.push(change);
|
|
2057
|
+
}
|
|
2058
|
+
return changes;
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
log.i("GITINTEGR", `[GitIntegration] Failed to get changed files: ${error}`);
|
|
2061
|
+
return [];
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Get detailed diff statistics between branches
|
|
2066
|
+
*/
|
|
2067
|
+
async getDiffStats(base, target) {
|
|
2068
|
+
try {
|
|
2069
|
+
const files = await this.getChangedFilesBetween(base, target);
|
|
2070
|
+
const output = execSync(`git diff --shortstat ${base}...${target}`, {
|
|
2071
|
+
cwd: this.config.repoPath,
|
|
2072
|
+
encoding: "utf-8",
|
|
2073
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
2074
|
+
windowsHide: true
|
|
2075
|
+
});
|
|
2076
|
+
const match = output.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
|
|
2077
|
+
const filesChanged = match?.[1] ? Number.parseInt(match[1], 10) : 0;
|
|
2078
|
+
const insertions = match?.[2] ? Number.parseInt(match[2], 10) : 0;
|
|
2079
|
+
const deletions = match?.[3] ? Number.parseInt(match[3], 10) : 0;
|
|
2080
|
+
return {
|
|
2081
|
+
files,
|
|
2082
|
+
filesChanged,
|
|
2083
|
+
insertions,
|
|
2084
|
+
deletions
|
|
2085
|
+
};
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
log.i("GITINTEGR", `[GitIntegration] Failed to get diff stats: ${error}`);
|
|
2088
|
+
return {
|
|
2089
|
+
files: [],
|
|
2090
|
+
filesChanged: 0,
|
|
2091
|
+
insertions: 0,
|
|
2092
|
+
deletions: 0
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Get merge base between two branches
|
|
2098
|
+
*
|
|
2099
|
+
* The merge base is the common ancestor commit.
|
|
2100
|
+
*/
|
|
2101
|
+
getMergeBase(branchA, branchB) {
|
|
2102
|
+
try {
|
|
2103
|
+
const base = execSync(`git merge-base ${branchA} ${branchB}`, {
|
|
2104
|
+
cwd: this.config.repoPath,
|
|
2105
|
+
encoding: "utf-8",
|
|
2106
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
2107
|
+
windowsHide: true
|
|
2108
|
+
}).trim();
|
|
2109
|
+
return base;
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
log.i("GITINTEGR", `[GitIntegration] Failed to get merge base: ${error}`);
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Get file content at specific revision
|
|
2117
|
+
*
|
|
2118
|
+
* @param path - File path relative to repository root
|
|
2119
|
+
* @param ref - Git reference (branch, tag, commit hash)
|
|
2120
|
+
* @returns File content as string, or null if file doesn't exist at ref
|
|
2121
|
+
*/
|
|
2122
|
+
getFileContent(path, ref) {
|
|
2123
|
+
try {
|
|
2124
|
+
const content = execSync(`git show ${ref}:${path}`, {
|
|
2125
|
+
cwd: this.config.repoPath,
|
|
2126
|
+
encoding: "utf-8",
|
|
2127
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
2128
|
+
windowsHide: true
|
|
2129
|
+
});
|
|
2130
|
+
return content;
|
|
2131
|
+
} catch (_error) {
|
|
2132
|
+
return null;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Get list of changed file paths between two references
|
|
2137
|
+
*
|
|
2138
|
+
* Simplified version of getChangedFilesBetween that returns only paths.
|
|
2139
|
+
* Useful for quick file enumeration.
|
|
2140
|
+
*
|
|
2141
|
+
* @param fromRef - Starting reference (branch, tag, commit)
|
|
2142
|
+
* @param toRef - Target reference
|
|
2143
|
+
* @returns Array of file paths (without status information)
|
|
2144
|
+
*/
|
|
2145
|
+
async getChangedFiles(fromRef, toRef) {
|
|
2146
|
+
try {
|
|
2147
|
+
const output = execSync(`git diff --name-only ${fromRef}...${toRef}`, {
|
|
2148
|
+
cwd: this.config.repoPath,
|
|
2149
|
+
encoding: "utf-8",
|
|
2150
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
2151
|
+
windowsHide: true
|
|
2152
|
+
});
|
|
2153
|
+
return output.trim().split("\n").filter((path) => path.length > 0);
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
log.i("GITINTEGR", `[GitIntegration] Failed to get changed files: ${error}`);
|
|
2156
|
+
return [];
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Get list of all branches
|
|
2161
|
+
*/
|
|
2162
|
+
getAllBranches() {
|
|
2163
|
+
try {
|
|
2164
|
+
const output = execSync("git branch --format='%(refname:short)'", {
|
|
2165
|
+
cwd: this.config.repoPath,
|
|
2166
|
+
encoding: "utf-8",
|
|
2167
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
2168
|
+
windowsHide: true
|
|
2169
|
+
});
|
|
2170
|
+
return output.trim().split("\n").map((b) => b.trim()).filter((b) => b.length > 0);
|
|
2171
|
+
} catch {
|
|
2172
|
+
return [];
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Clean up - restore original branch if not already done
|
|
2177
|
+
*/
|
|
2178
|
+
async cleanup() {
|
|
2179
|
+
if (this.originalBranch) {
|
|
2180
|
+
await this.restoreOriginalBranch();
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
// src/agents/merge-agent.ts
|
|
2186
|
+
var MergeAgent = class extends BaseAgent {
|
|
2187
|
+
config;
|
|
2188
|
+
gitIntegration = null;
|
|
2189
|
+
threeWayMerger = null;
|
|
2190
|
+
aiResolver = null;
|
|
2191
|
+
branchManager = null;
|
|
2192
|
+
// Merge-specific metrics
|
|
2193
|
+
mergeMetrics = {
|
|
2194
|
+
mergesPerformed: 0,
|
|
2195
|
+
conflictsDetected: 0,
|
|
2196
|
+
conflictsAutoResolved: 0,
|
|
2197
|
+
totalMergeTimeMs: 0,
|
|
2198
|
+
lastMergeAt: 0
|
|
2199
|
+
};
|
|
2200
|
+
constructor(config) {
|
|
2201
|
+
super("merge" /* MERGE */, {
|
|
2202
|
+
maxConcurrency: config.maxConcurrency ?? 1,
|
|
2203
|
+
memoryLimit: 512,
|
|
2204
|
+
// MB
|
|
2205
|
+
priority: 5
|
|
2206
|
+
});
|
|
2207
|
+
this.config = {
|
|
2208
|
+
fastPathEnabled: true,
|
|
2209
|
+
semanticMatchingEnabled: true,
|
|
2210
|
+
semanticThreshold: 0.7,
|
|
2211
|
+
autoResolveConflicts: false,
|
|
2212
|
+
maxConcurrency: 1,
|
|
2213
|
+
...config
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
// =============================================================================
|
|
2217
|
+
// ABSTRACT METHOD IMPLEMENTATIONS
|
|
2218
|
+
// =============================================================================
|
|
2219
|
+
canProcessTask(task) {
|
|
2220
|
+
return task.type === "merge" || task.type === "merge:analyze" || task.type === "merge:perform" || task.type === "merge:suggestions";
|
|
2221
|
+
}
|
|
2222
|
+
async processTask(task) {
|
|
2223
|
+
log.d("MERGEAGENT", "proc_task", { id: task.id, type: task.type });
|
|
2224
|
+
const payload = task.payload;
|
|
2225
|
+
switch (task.type) {
|
|
2226
|
+
case "merge":
|
|
2227
|
+
case "merge:perform":
|
|
2228
|
+
return await this.performSemanticMerge({
|
|
2229
|
+
branchA: payload["branchA"],
|
|
2230
|
+
branchB: payload["branchB"],
|
|
2231
|
+
dryRun: payload["dryRun"],
|
|
2232
|
+
autoResolve: payload["autoResolve"],
|
|
2233
|
+
includeAISuggestions: payload["includeAISuggestions"]
|
|
2234
|
+
});
|
|
2235
|
+
case "merge:analyze":
|
|
2236
|
+
return await this.analyzeConflicts(payload["branchA"], payload["branchB"]);
|
|
2237
|
+
case "merge:suggestions":
|
|
2238
|
+
return await this.getSuggestions(payload["conflict"]);
|
|
2239
|
+
default:
|
|
2240
|
+
throw new Error(`Unknown task type: ${task.type}`);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
async handleMessage(message) {
|
|
2244
|
+
log.d("MERGEAGENT", "recv_msg", { from: message.from, type: message.type });
|
|
2245
|
+
}
|
|
2246
|
+
// =============================================================================
|
|
2247
|
+
// LIFECYCLE
|
|
2248
|
+
// =============================================================================
|
|
2249
|
+
async onInitialize() {
|
|
2250
|
+
log.i("MERGEAGENT", "init_start");
|
|
2251
|
+
this.gitIntegration = this.config.gitIntegration ?? new GitIntegration({
|
|
2252
|
+
repoPath: this.config.repoPath,
|
|
2253
|
+
allowDetachedHead: true,
|
|
2254
|
+
restoreOnError: true
|
|
2255
|
+
});
|
|
2256
|
+
this.branchManager = this.config.branchManager ?? null;
|
|
2257
|
+
const mergerConfig = {
|
|
2258
|
+
fastPathEnabled: this.config.fastPathEnabled,
|
|
2259
|
+
semanticMatchingEnabled: this.config.semanticMatchingEnabled,
|
|
2260
|
+
semanticThreshold: this.config.semanticThreshold,
|
|
2261
|
+
autoResolveConflicts: this.config.autoResolveConflicts
|
|
2262
|
+
};
|
|
2263
|
+
if (this.branchManager && this.gitIntegration) {
|
|
2264
|
+
this.threeWayMerger = new ThreeWayMerger(
|
|
2265
|
+
this.branchManager,
|
|
2266
|
+
this.gitIntegration,
|
|
2267
|
+
null,
|
|
2268
|
+
// conductor - not needed for basic merge operations
|
|
2269
|
+
mergerConfig
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
this.aiResolver = null;
|
|
2273
|
+
log.i("MERGEAGENT", "init_done");
|
|
2274
|
+
}
|
|
2275
|
+
async onShutdown() {
|
|
2276
|
+
log.i("MERGEAGENT", "shutdown_start");
|
|
2277
|
+
if (this.gitIntegration) {
|
|
2278
|
+
await this.gitIntegration.cleanup();
|
|
2279
|
+
}
|
|
2280
|
+
this.threeWayMerger = null;
|
|
2281
|
+
this.gitIntegration = null;
|
|
2282
|
+
this.aiResolver = null;
|
|
2283
|
+
log.i("MERGEAGENT", "shutdown_done");
|
|
2284
|
+
}
|
|
2285
|
+
// =============================================================================
|
|
2286
|
+
// PUBLIC API
|
|
2287
|
+
// =============================================================================
|
|
2288
|
+
/**
|
|
2289
|
+
* Set dependencies after construction (for DI)
|
|
2290
|
+
*/
|
|
2291
|
+
setDependencies(branchManager, gitIntegration) {
|
|
2292
|
+
this.branchManager = branchManager;
|
|
2293
|
+
if (gitIntegration) {
|
|
2294
|
+
this.gitIntegration = gitIntegration;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Set AI resolver with embedding generator
|
|
2299
|
+
*/
|
|
2300
|
+
setAIResolver(config) {
|
|
2301
|
+
this.aiResolver = new AIConflictResolver(config);
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Perform semantic merge between two branches
|
|
2305
|
+
*/
|
|
2306
|
+
async performSemanticMerge(options) {
|
|
2307
|
+
const startTime = Date.now();
|
|
2308
|
+
if (!this.threeWayMerger) {
|
|
2309
|
+
return {
|
|
2310
|
+
success: false,
|
|
2311
|
+
error: "MergeAgent not properly initialized - missing ThreeWayMerger",
|
|
2312
|
+
stats: this.createEmptyStats(0)
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
try {
|
|
2316
|
+
log.i("MERGEAGENT", "merge_start", { from: options.branchA, to: options.branchB });
|
|
2317
|
+
const mergeResult = await this.threeWayMerger.performMerge(options.branchA, options.branchB);
|
|
2318
|
+
if (options.includeAISuggestions && mergeResult.conflicts.length > 0 && this.aiResolver) {
|
|
2319
|
+
await this.generateAISuggestions(mergeResult.conflicts);
|
|
2320
|
+
}
|
|
2321
|
+
let autoResolved = 0;
|
|
2322
|
+
if (options.autoResolve) {
|
|
2323
|
+
autoResolved = await this.autoResolveConflicts(mergeResult);
|
|
2324
|
+
}
|
|
2325
|
+
let appliedActions = [];
|
|
2326
|
+
if (!options.dryRun) {
|
|
2327
|
+
appliedActions = await this.applyMergeActions(mergeResult);
|
|
2328
|
+
}
|
|
2329
|
+
const mergeTimeMs = Date.now() - startTime;
|
|
2330
|
+
this.updateMergeMetrics(mergeResult, autoResolved, mergeTimeMs);
|
|
2331
|
+
return {
|
|
2332
|
+
success: true,
|
|
2333
|
+
mergeResult,
|
|
2334
|
+
appliedActions,
|
|
2335
|
+
stats: {
|
|
2336
|
+
totalUnitsAnalyzed: mergeResult.stats.totalUnitsInA + mergeResult.stats.totalUnitsInB,
|
|
2337
|
+
matchedUnits: mergeResult.stats.matchedCount,
|
|
2338
|
+
conflictsDetected: mergeResult.stats.conflictCount,
|
|
2339
|
+
autoResolved,
|
|
2340
|
+
manualReviewRequired: mergeResult.stats.manualReviewCount - autoResolved,
|
|
2341
|
+
mergeTimeMs
|
|
2342
|
+
}
|
|
2343
|
+
};
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2346
|
+
log.e("MERGEAGENT", "merge_fail", { err: errorMsg });
|
|
2347
|
+
return {
|
|
2348
|
+
success: false,
|
|
2349
|
+
error: errorMsg,
|
|
2350
|
+
stats: this.createEmptyStats(Date.now() - startTime)
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Analyze merge conflicts without performing merge
|
|
2356
|
+
*/
|
|
2357
|
+
async analyzeConflicts(branchA, branchB) {
|
|
2358
|
+
const result = await this.performSemanticMerge({
|
|
2359
|
+
branchA,
|
|
2360
|
+
branchB,
|
|
2361
|
+
dryRun: true,
|
|
2362
|
+
autoResolve: false
|
|
2363
|
+
});
|
|
2364
|
+
if (!result.mergeResult) {
|
|
2365
|
+
return {
|
|
2366
|
+
conflicts: [],
|
|
2367
|
+
stats: { totalConflicts: 0, bySeverity: {}, byType: {} }
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
const conflicts = result.mergeResult.conflicts;
|
|
2371
|
+
const bySeverity = {};
|
|
2372
|
+
const byType = {};
|
|
2373
|
+
for (const conflict of conflicts) {
|
|
2374
|
+
bySeverity[conflict.severity] = (bySeverity[conflict.severity] || 0) + 1;
|
|
2375
|
+
byType[conflict.type] = (byType[conflict.type] || 0) + 1;
|
|
2376
|
+
}
|
|
2377
|
+
return {
|
|
2378
|
+
conflicts,
|
|
2379
|
+
stats: {
|
|
2380
|
+
totalConflicts: conflicts.length,
|
|
2381
|
+
bySeverity,
|
|
2382
|
+
byType
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Get AI-generated suggestions for a specific conflict
|
|
2388
|
+
*/
|
|
2389
|
+
async getSuggestions(conflict) {
|
|
2390
|
+
if (!this.aiResolver) {
|
|
2391
|
+
return ["AI resolver not available - configure embedding generator first"];
|
|
2392
|
+
}
|
|
2393
|
+
try {
|
|
2394
|
+
const aiAnalysis = await this.aiResolver.analyzeConflict(conflict);
|
|
2395
|
+
const resolution = this.aiResolver.createResolution(aiAnalysis);
|
|
2396
|
+
return [
|
|
2397
|
+
`Strategy: ${resolution.strategy}`,
|
|
2398
|
+
`Confidence: ${(resolution.confidence * 100).toFixed(1)}%`,
|
|
2399
|
+
`Explanation: ${resolution.explanation}`,
|
|
2400
|
+
...resolution.mergedCode ? [`Suggested code:
|
|
2401
|
+
${resolution.mergedCode}`] : []
|
|
2402
|
+
];
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
log.e("MERGEAGENT", "ai_suggest_fail", { err: String(error) });
|
|
2405
|
+
return [];
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Get merge-specific metrics
|
|
2410
|
+
*/
|
|
2411
|
+
getMergeMetrics() {
|
|
2412
|
+
return { ...this.mergeMetrics };
|
|
2413
|
+
}
|
|
2414
|
+
// =============================================================================
|
|
2415
|
+
// PRIVATE METHODS
|
|
2416
|
+
// =============================================================================
|
|
2417
|
+
async generateAISuggestions(conflicts) {
|
|
2418
|
+
if (!this.aiResolver) return;
|
|
2419
|
+
log.d("MERGEAGENT", "gen_ai_suggest", { cnt: conflicts.length });
|
|
2420
|
+
for (const conflict of conflicts) {
|
|
2421
|
+
try {
|
|
2422
|
+
const aiAnalysis = await this.aiResolver.analyzeConflict(conflict);
|
|
2423
|
+
const resolution = this.aiResolver.createResolution(aiAnalysis);
|
|
2424
|
+
conflict.aiSuggestions = [resolution.explanation];
|
|
2425
|
+
conflict.aiConfidence = resolution.confidence;
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
log.w("MERGEAGENT", "ai_conflict_fail", { err: String(error) });
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
async autoResolveConflicts(mergeResult) {
|
|
2432
|
+
let resolved = 0;
|
|
2433
|
+
for (const action of mergeResult.mergeActions) {
|
|
2434
|
+
if (action.type === "manual-review" && action.conflict) {
|
|
2435
|
+
if (this.aiResolver) {
|
|
2436
|
+
try {
|
|
2437
|
+
const aiAnalysis = await this.aiResolver.analyzeConflict(action.conflict);
|
|
2438
|
+
if (aiAnalysis.confidence >= 0.9 && aiAnalysis.mergedCode) {
|
|
2439
|
+
action.type = "auto-merge";
|
|
2440
|
+
action.description = `Auto-resolved: ${aiAnalysis.explanation}`;
|
|
2441
|
+
resolved++;
|
|
2442
|
+
}
|
|
2443
|
+
} catch {
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
return resolved;
|
|
2449
|
+
}
|
|
2450
|
+
async applyMergeActions(mergeResult) {
|
|
2451
|
+
const applied = [];
|
|
2452
|
+
for (const action of mergeResult.mergeActions) {
|
|
2453
|
+
if (action.type === "auto-merge" && action.mergedUnit) {
|
|
2454
|
+
applied.push(action);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
log.i("MERGEAGENT", "actions_applied", { cnt: applied.length });
|
|
2458
|
+
return applied;
|
|
2459
|
+
}
|
|
2460
|
+
updateMergeMetrics(mergeResult, autoResolved, mergeTimeMs) {
|
|
2461
|
+
this.mergeMetrics.mergesPerformed++;
|
|
2462
|
+
this.mergeMetrics.conflictsDetected += mergeResult.conflicts.length;
|
|
2463
|
+
this.mergeMetrics.conflictsAutoResolved += autoResolved;
|
|
2464
|
+
this.mergeMetrics.totalMergeTimeMs += mergeTimeMs;
|
|
2465
|
+
this.mergeMetrics.lastMergeAt = Date.now();
|
|
2466
|
+
}
|
|
2467
|
+
createEmptyStats(mergeTimeMs) {
|
|
2468
|
+
return {
|
|
2469
|
+
totalUnitsAnalyzed: 0,
|
|
2470
|
+
matchedUnits: 0,
|
|
2471
|
+
conflictsDetected: 0,
|
|
2472
|
+
autoResolved: 0,
|
|
2473
|
+
manualReviewRequired: 0,
|
|
2474
|
+
mergeTimeMs
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
|
|
2479
|
+
export { MergeAgent };
|
|
2480
|
+
//# sourceMappingURL=merge-agent-MJEW3HWU.js.map
|
|
2481
|
+
//# sourceMappingURL=merge-agent-MJEW3HWU.js.map
|