claude-autopm 1.30.0 → 1.31.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/autopm/.claude/scripts/github/dependency-tracker.js +554 -0
- package/autopm/.claude/scripts/github/dependency-validator.js +545 -0
- package/autopm/.claude/scripts/github/dependency-visualizer.js +477 -0
- package/autopm/.claude/scripts/pm/lib/epic-discovery.js +119 -0
- package/autopm/.claude/scripts/pm/next.js +56 -58
- package/autopm/.claude/scripts/pm/prd-new.js +33 -6
- package/autopm/.claude/scripts/pm/template-list.js +25 -3
- package/autopm/.claude/scripts/pm/template-new.js +25 -3
- package/autopm/lib/README-FILTER-SEARCH.md +285 -0
- package/autopm/lib/analytics-engine.js +689 -0
- package/autopm/lib/batch-processor-integration.js +366 -0
- package/autopm/lib/batch-processor.js +278 -0
- package/autopm/lib/burndown-chart.js +415 -0
- package/autopm/lib/conflict-history.js +316 -0
- package/autopm/lib/conflict-resolver.js +330 -0
- package/autopm/lib/dependency-analyzer.js +466 -0
- package/autopm/lib/filter-engine.js +414 -0
- package/autopm/lib/guide/interactive-guide.js +756 -0
- package/autopm/lib/guide/manager.js +663 -0
- package/autopm/lib/query-parser.js +322 -0
- package/autopm/lib/template-engine.js +347 -0
- package/autopm/lib/visual-diff.js +297 -0
- package/bin/autopm-poc.js +216 -0
- package/install/install.js +2 -1
- package/lib/ai-providers/ClaudeProvider.js +112 -0
- package/lib/ai-providers/base-provider.js +110 -0
- package/lib/services/PRDService.js +178 -0
- package/package.json +5 -2
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict History Manager
|
|
3
|
+
*
|
|
4
|
+
* Logs conflict resolutions with timestamps and provides undo/replay functionality.
|
|
5
|
+
* Supports both in-memory and file-based storage.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const ConflictHistory = require('./lib/conflict-history');
|
|
9
|
+
*
|
|
10
|
+
* const history = new ConflictHistory({
|
|
11
|
+
* storage: 'memory'
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // Log a conflict resolution
|
|
15
|
+
* const logId = history.log(conflict, resolution);
|
|
16
|
+
*
|
|
17
|
+
* // Retrieve history with filters
|
|
18
|
+
* const recentConflicts = history.getHistory({ strategy: 'newest' });
|
|
19
|
+
*
|
|
20
|
+
* // Undo a resolution
|
|
21
|
+
* const undone = history.undo(logId);
|
|
22
|
+
*
|
|
23
|
+
* // Replay with different strategy
|
|
24
|
+
* const replayed = history.replay(logId, 'local');
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
class ConflictHistory {
|
|
32
|
+
/**
|
|
33
|
+
* Create a new ConflictHistory instance
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} options - Configuration options
|
|
36
|
+
* @param {string} options.storage - Storage type ('memory' or 'file') (default: 'memory')
|
|
37
|
+
* @param {string} options.storagePath - Path to storage file (default: '.claude/.conflict-history.json')
|
|
38
|
+
*/
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
// Validate and sanitize storagePath to prevent path traversal
|
|
41
|
+
let storagePath = options.storagePath || '.claude/.conflict-history.json';
|
|
42
|
+
|
|
43
|
+
// Resolve path and check for path traversal attempts using a trusted base directory
|
|
44
|
+
const baseDir = process.cwd();
|
|
45
|
+
const resolvedPath = path.resolve(baseDir, storagePath);
|
|
46
|
+
if (!resolvedPath.startsWith(baseDir + path.sep)) {
|
|
47
|
+
throw new Error('storagePath must be within the current working directory (security: path traversal prevention)');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.options = {
|
|
51
|
+
storage: options.storage || 'memory',
|
|
52
|
+
storagePath: resolvedPath
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// In-memory storage
|
|
56
|
+
this.conflicts = new Map();
|
|
57
|
+
|
|
58
|
+
// Load from file if using file storage
|
|
59
|
+
if (this.options.storage === 'file') {
|
|
60
|
+
this._loadFromFile();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Log a conflict resolution
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} conflict - Conflict object
|
|
68
|
+
* @param {Object} resolution - Resolution object
|
|
69
|
+
* @param {string} resolution.strategy - Strategy used
|
|
70
|
+
* @param {string} resolution.chosenContent - Resolved content
|
|
71
|
+
* @param {Date} resolution.timestamp - Resolution timestamp (optional)
|
|
72
|
+
* @returns {string} Unique log ID
|
|
73
|
+
*/
|
|
74
|
+
log(conflict, resolution) {
|
|
75
|
+
// Generate unique ID
|
|
76
|
+
const logId = this._generateId();
|
|
77
|
+
|
|
78
|
+
// Add timestamp if not provided
|
|
79
|
+
const timestamp = resolution.timestamp || new Date();
|
|
80
|
+
|
|
81
|
+
// Store conflict and resolution
|
|
82
|
+
const entry = {
|
|
83
|
+
id: logId,
|
|
84
|
+
conflict: { ...conflict },
|
|
85
|
+
resolution: {
|
|
86
|
+
...resolution,
|
|
87
|
+
timestamp
|
|
88
|
+
},
|
|
89
|
+
createdAt: new Date()
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this.conflicts.set(logId, entry);
|
|
93
|
+
|
|
94
|
+
// Persist to file if using file storage
|
|
95
|
+
if (this.options.storage === 'file') {
|
|
96
|
+
this._saveToFile();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return logId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Retrieve conflict history with optional filters
|
|
104
|
+
*
|
|
105
|
+
* @param {Object} filters - Filter criteria
|
|
106
|
+
* @param {string} filters.strategy - Filter by resolution strategy
|
|
107
|
+
* @param {string} filters.filePath - Filter by file path
|
|
108
|
+
* @param {Date} filters.after - Filter by date (after)
|
|
109
|
+
* @param {Date} filters.before - Filter by date (before)
|
|
110
|
+
* @returns {Array<Object>} Filtered conflict history entries
|
|
111
|
+
*/
|
|
112
|
+
getHistory(filters = {}) {
|
|
113
|
+
let results = Array.from(this.conflicts.values());
|
|
114
|
+
|
|
115
|
+
// Apply filters
|
|
116
|
+
if (filters.strategy) {
|
|
117
|
+
results = results.filter(entry =>
|
|
118
|
+
entry.resolution.strategy === filters.strategy
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (filters.filePath) {
|
|
123
|
+
results = results.filter(entry =>
|
|
124
|
+
entry.conflict.filePath === filters.filePath
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (filters.after) {
|
|
129
|
+
results = results.filter(entry =>
|
|
130
|
+
entry.createdAt >= filters.after
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (filters.before) {
|
|
135
|
+
results = results.filter(entry =>
|
|
136
|
+
entry.createdAt <= filters.before
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Sort by creation date (newest first)
|
|
141
|
+
results.sort((a, b) => b.createdAt - a.createdAt);
|
|
142
|
+
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Undo a conflict resolution
|
|
148
|
+
*
|
|
149
|
+
* Retrieves the original conflict state before resolution.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} logId - Log ID to undo
|
|
152
|
+
* @returns {Object} Original conflict and resolution
|
|
153
|
+
*/
|
|
154
|
+
undo(logId) {
|
|
155
|
+
const entry = this.conflicts.get(logId);
|
|
156
|
+
|
|
157
|
+
if (!entry) {
|
|
158
|
+
throw new Error(`Conflict log not found: ${logId}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
conflict: entry.conflict,
|
|
163
|
+
resolution: entry.resolution
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Replay a conflict resolution with a different strategy
|
|
169
|
+
*
|
|
170
|
+
* @param {string} logId - Log ID to replay
|
|
171
|
+
* @param {string} newStrategy - New resolution strategy
|
|
172
|
+
* @returns {Object} Replayed resolution result
|
|
173
|
+
*/
|
|
174
|
+
replay(logId, newStrategy) {
|
|
175
|
+
const entry = this.conflicts.get(logId);
|
|
176
|
+
|
|
177
|
+
if (!entry) {
|
|
178
|
+
throw new Error(`Conflict log not found: ${logId}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Determine new content based on strategy
|
|
182
|
+
let newContent;
|
|
183
|
+
switch (newStrategy) {
|
|
184
|
+
case 'local':
|
|
185
|
+
newContent = entry.conflict.localContent;
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case 'remote':
|
|
189
|
+
newContent = entry.conflict.remoteContent;
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
case 'newest':
|
|
193
|
+
// Use timestamp comparison
|
|
194
|
+
if (entry.conflict.localTimestamp && entry.conflict.remoteTimestamp) {
|
|
195
|
+
const localTime = new Date(entry.conflict.localTimestamp).getTime();
|
|
196
|
+
const remoteTime = new Date(entry.conflict.remoteTimestamp).getTime();
|
|
197
|
+
newContent = remoteTime > localTime ?
|
|
198
|
+
entry.conflict.remoteContent :
|
|
199
|
+
entry.conflict.localContent;
|
|
200
|
+
} else {
|
|
201
|
+
newContent = entry.conflict.localContent;
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
default:
|
|
206
|
+
throw new Error(`Unknown strategy: ${newStrategy}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
originalResolution: entry.resolution,
|
|
211
|
+
newResolution: {
|
|
212
|
+
strategy: newStrategy,
|
|
213
|
+
chosenContent: newContent,
|
|
214
|
+
timestamp: new Date()
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Clear all conflict history
|
|
221
|
+
*/
|
|
222
|
+
clear() {
|
|
223
|
+
this.conflicts.clear();
|
|
224
|
+
|
|
225
|
+
if (this.options.storage === 'file') {
|
|
226
|
+
this._saveToFile();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get total number of logged conflicts
|
|
232
|
+
*
|
|
233
|
+
* @returns {number} Total conflicts
|
|
234
|
+
*/
|
|
235
|
+
count() {
|
|
236
|
+
return this.conflicts.size;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Generate unique ID for conflict log
|
|
241
|
+
*
|
|
242
|
+
* @private
|
|
243
|
+
* @returns {string} Unique ID
|
|
244
|
+
*/
|
|
245
|
+
_generateId() {
|
|
246
|
+
return crypto.randomBytes(16).toString('hex');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Load conflict history from file
|
|
251
|
+
*
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
_loadFromFile() {
|
|
255
|
+
try {
|
|
256
|
+
if (fs.existsSync(this.options.storagePath)) {
|
|
257
|
+
const data = fs.readFileSync(this.options.storagePath, 'utf8');
|
|
258
|
+
|
|
259
|
+
// Validate non-empty file
|
|
260
|
+
if (!data.trim()) {
|
|
261
|
+
this.conflicts.clear();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const parsed = JSON.parse(data);
|
|
266
|
+
|
|
267
|
+
// Validate structure
|
|
268
|
+
if (!Array.isArray(parsed)) {
|
|
269
|
+
throw new Error('Invalid conflict history format: expected array');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.conflicts.clear();
|
|
273
|
+
for (const entry of parsed) {
|
|
274
|
+
// Validate entry structure
|
|
275
|
+
if (!entry.id || !entry.conflict || !entry.resolution) {
|
|
276
|
+
console.warn(`Skipping invalid entry: ${JSON.stringify(entry).substring(0, 100)}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Restore Date objects
|
|
281
|
+
entry.createdAt = new Date(entry.createdAt);
|
|
282
|
+
entry.resolution.timestamp = new Date(entry.resolution.timestamp);
|
|
283
|
+
|
|
284
|
+
this.conflicts.set(entry.id, entry);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// Re-throw with context for better debugging
|
|
289
|
+
throw new Error(`Failed to load conflict history from ${this.options.storagePath}: ${error.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Save conflict history to file
|
|
295
|
+
*
|
|
296
|
+
* @private
|
|
297
|
+
*/
|
|
298
|
+
_saveToFile() {
|
|
299
|
+
try {
|
|
300
|
+
// Ensure directory exists
|
|
301
|
+
const dir = path.dirname(this.options.storagePath);
|
|
302
|
+
if (!fs.existsSync(dir)) {
|
|
303
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Convert Map to Array for JSON serialization
|
|
307
|
+
const data = Array.from(this.conflicts.values());
|
|
308
|
+
|
|
309
|
+
fs.writeFileSync(this.options.storagePath, JSON.stringify(data, null, 2), 'utf8');
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error('Failed to save conflict history:', error.message);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = ConflictHistory;
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Resolver for GitHub Sync Operations
|
|
3
|
+
*
|
|
4
|
+
* Handles three-way merge conflicts when syncing markdown files between
|
|
5
|
+
* local filesystem and GitHub. Supports multiple resolution strategies:
|
|
6
|
+
* - newest: Keep version with newest timestamp
|
|
7
|
+
* - local: Always prefer local version
|
|
8
|
+
* - remote: Always prefer remote version
|
|
9
|
+
* - rules-based: Apply custom resolution rules
|
|
10
|
+
* - interactive: Prompt user for resolution
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const ConflictResolver = require('./lib/conflict-resolver');
|
|
14
|
+
*
|
|
15
|
+
* const resolver = new ConflictResolver({
|
|
16
|
+
* strategy: 'newest',
|
|
17
|
+
* contextLines: 3
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* const result = resolver.threeWayMerge(localContent, remoteContent, baseContent);
|
|
21
|
+
*
|
|
22
|
+
* if (result.hasConflicts) {
|
|
23
|
+
* for (const conflict of result.conflicts) {
|
|
24
|
+
* const resolved = resolver.resolveConflict(conflict, 'newest');
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
class ConflictResolver {
|
|
30
|
+
/**
|
|
31
|
+
* Create a new ConflictResolver instance
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} options - Configuration options
|
|
34
|
+
* @param {string} options.strategy - Default resolution strategy (default: 'manual')
|
|
35
|
+
* @param {number} options.contextLines - Number of context lines around conflicts (default: 3)
|
|
36
|
+
* @param {string} options.markerPrefix - Prefix for conflict markers (default: 'LOCAL/REMOTE')
|
|
37
|
+
* @param {boolean} options.normalizeLineEndings - Normalize line endings during merge (default: true)
|
|
38
|
+
*/
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
// Validate configuration
|
|
41
|
+
if (options.contextLines !== undefined) {
|
|
42
|
+
if (typeof options.contextLines !== 'number' || options.contextLines < 0) {
|
|
43
|
+
throw new Error('contextLines must be a non-negative number');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.options = {
|
|
48
|
+
strategy: options.strategy || 'manual',
|
|
49
|
+
contextLines: options.contextLines !== undefined ? options.contextLines : 3,
|
|
50
|
+
markerPrefix: options.markerPrefix || 'LOCAL',
|
|
51
|
+
normalizeLineEndings: options.normalizeLineEndings !== false
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Perform three-way merge on text content
|
|
57
|
+
*
|
|
58
|
+
* Compares local, remote, and base versions to detect and merge changes.
|
|
59
|
+
* Uses a line-by-line diff algorithm to identify conflicts.
|
|
60
|
+
*
|
|
61
|
+
* IMPORTANT LIMITATIONS:
|
|
62
|
+
* - This is a simplified line-based merge that does NOT detect:
|
|
63
|
+
* • Moved code blocks (treats as delete + add = conflict)
|
|
64
|
+
* • Reordered functions (multiple conflicts)
|
|
65
|
+
* • Semantic equivalence (different formatting, same logic)
|
|
66
|
+
* - For complex refactoring, use manual conflict resolution strategy
|
|
67
|
+
* - Does not use Longest Common Subsequence (LCS) algorithm
|
|
68
|
+
* - No semantic/AST-based merging for code
|
|
69
|
+
*
|
|
70
|
+
* PERFORMANCE NOTES:
|
|
71
|
+
* - Files >1MB may cause memory pressure (splits into line arrays)
|
|
72
|
+
* - For very large files, consider splitting into sections
|
|
73
|
+
* - Tested up to 1MB files (~1000 lines)
|
|
74
|
+
*
|
|
75
|
+
* @param {string} local - Local version content
|
|
76
|
+
* @param {string} remote - Remote version content
|
|
77
|
+
* @param {string} base - Base version content (last synced)
|
|
78
|
+
* @returns {Object} Merge result with conflicts and merged content
|
|
79
|
+
* @returns {string} result.merged - Merged content with conflict markers
|
|
80
|
+
* @returns {Array} result.conflicts - Array of detected conflicts
|
|
81
|
+
* @returns {boolean} result.hasConflicts - True if conflicts were detected
|
|
82
|
+
*/
|
|
83
|
+
threeWayMerge(local, remote, base) {
|
|
84
|
+
// Validate inputs
|
|
85
|
+
if (local === null || local === undefined ||
|
|
86
|
+
remote === null || remote === undefined ||
|
|
87
|
+
base === null || base === undefined) {
|
|
88
|
+
throw new Error('All parameters (local, remote, base) are required and cannot be null or undefined');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Normalize line endings if enabled
|
|
92
|
+
if (this.options.normalizeLineEndings) {
|
|
93
|
+
local = this._normalizeLineEndings(local);
|
|
94
|
+
remote = this._normalizeLineEndings(remote);
|
|
95
|
+
base = this._normalizeLineEndings(base);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Split into lines for comparison
|
|
99
|
+
const localLines = local.split('\n');
|
|
100
|
+
const remoteLines = remote.split('\n');
|
|
101
|
+
const baseLines = base.split('\n');
|
|
102
|
+
|
|
103
|
+
// Perform three-way merge
|
|
104
|
+
const mergeResult = this._performMerge(localLines, remoteLines, baseLines);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
merged: mergeResult.merged.join('\n'),
|
|
108
|
+
conflicts: mergeResult.conflicts,
|
|
109
|
+
hasConflicts: mergeResult.conflicts.length > 0
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Perform the actual merge algorithm
|
|
115
|
+
*
|
|
116
|
+
* Uses a simplified three-way merge that handles most common cases:
|
|
117
|
+
* - Non-overlapping changes are merged
|
|
118
|
+
* - Identical changes are merged
|
|
119
|
+
* - Conflicting changes are marked
|
|
120
|
+
*
|
|
121
|
+
* @private
|
|
122
|
+
* @param {Array<string>} localLines - Local lines
|
|
123
|
+
* @param {Array<string>} remoteLines - Remote lines
|
|
124
|
+
* @param {Array<string>} baseLines - Base lines
|
|
125
|
+
* @returns {Object} Merge result
|
|
126
|
+
*/
|
|
127
|
+
_performMerge(localLines, remoteLines, baseLines) {
|
|
128
|
+
const merged = [];
|
|
129
|
+
const conflicts = [];
|
|
130
|
+
|
|
131
|
+
const maxLen = Math.max(localLines.length, remoteLines.length, baseLines.length);
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < maxLen; i++) {
|
|
134
|
+
const baseLine = baseLines[i];
|
|
135
|
+
const localLine = localLines[i];
|
|
136
|
+
const remoteLine = remoteLines[i];
|
|
137
|
+
|
|
138
|
+
// Case 1: All three are identical (or all undefined)
|
|
139
|
+
if (baseLine === localLine && baseLine === remoteLine) {
|
|
140
|
+
if (baseLine !== undefined) {
|
|
141
|
+
merged.push(baseLine);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Case 2: Base and local match, remote different - accept remote change
|
|
147
|
+
if (baseLine === localLine && remoteLine !== baseLine) {
|
|
148
|
+
if (remoteLine !== undefined) {
|
|
149
|
+
merged.push(remoteLine);
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Case 3: Base and remote match, local different - accept local change
|
|
155
|
+
if (baseLine === remoteLine && localLine !== baseLine) {
|
|
156
|
+
if (localLine !== undefined) {
|
|
157
|
+
merged.push(localLine);
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Case 4: Local and remote match (but differ from base) - accept the change
|
|
163
|
+
if (localLine === remoteLine) {
|
|
164
|
+
if (localLine !== undefined) {
|
|
165
|
+
merged.push(localLine);
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Case 5: All three differ - CONFLICT
|
|
171
|
+
conflicts.push({
|
|
172
|
+
line: merged.length + 1,
|
|
173
|
+
localContent: localLine || '',
|
|
174
|
+
remoteContent: remoteLine || '',
|
|
175
|
+
baseContent: baseLine || '',
|
|
176
|
+
section: this._detectSection(i, localLines)
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
merged.push('<<<<<<< LOCAL');
|
|
180
|
+
merged.push(localLine || '');
|
|
181
|
+
merged.push('=======');
|
|
182
|
+
merged.push(remoteLine || '');
|
|
183
|
+
merged.push('>>>>>>> REMOTE');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { merged, conflicts };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Detect which section of the file a line belongs to
|
|
191
|
+
*
|
|
192
|
+
* @private
|
|
193
|
+
* @param {number} lineIndex - Line index
|
|
194
|
+
* @param {Array<string>} lines - File lines
|
|
195
|
+
* @returns {string} Section name ('frontmatter', 'body', etc.)
|
|
196
|
+
*/
|
|
197
|
+
_detectSection(lineIndex, lines) {
|
|
198
|
+
// Detect frontmatter (YAML between --- markers)
|
|
199
|
+
if (lineIndex < 10) {
|
|
200
|
+
const firstLines = lines.slice(0, Math.min(10, lines.length));
|
|
201
|
+
if (firstLines[0] === '---') {
|
|
202
|
+
const endIndex = firstLines.findIndex((line, idx) => idx > 0 && line === '---');
|
|
203
|
+
if (endIndex > 0 && lineIndex <= endIndex) {
|
|
204
|
+
return 'frontmatter';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return 'body';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Normalize line endings to LF
|
|
214
|
+
*
|
|
215
|
+
* @private
|
|
216
|
+
* @param {string} text - Text to normalize
|
|
217
|
+
* @returns {string} Normalized text
|
|
218
|
+
*/
|
|
219
|
+
_normalizeLineEndings(text) {
|
|
220
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolve a conflict using the specified strategy
|
|
225
|
+
*
|
|
226
|
+
* @param {Object} conflict - Conflict object from threeWayMerge
|
|
227
|
+
* @param {string} strategy - Resolution strategy ('newest', 'local', 'remote', 'rules-based')
|
|
228
|
+
* @param {Object} rules - Optional rules for rules-based strategy
|
|
229
|
+
* @returns {string} Resolved content
|
|
230
|
+
*/
|
|
231
|
+
resolveConflict(conflict, strategy, rules = {}) {
|
|
232
|
+
switch (strategy) {
|
|
233
|
+
case 'newest':
|
|
234
|
+
return this._resolveNewest(conflict);
|
|
235
|
+
|
|
236
|
+
case 'local':
|
|
237
|
+
return conflict.localContent;
|
|
238
|
+
|
|
239
|
+
case 'remote':
|
|
240
|
+
return conflict.remoteContent;
|
|
241
|
+
|
|
242
|
+
case 'rules-based':
|
|
243
|
+
return this._resolveRulesBased(conflict, rules);
|
|
244
|
+
|
|
245
|
+
case 'manual':
|
|
246
|
+
// Return conflict markers for manual resolution
|
|
247
|
+
return `<<<<<<< LOCAL\n${conflict.localContent}\n=======\n${conflict.remoteContent}\n>>>>>>> REMOTE`;
|
|
248
|
+
|
|
249
|
+
default:
|
|
250
|
+
throw new Error(`Unknown resolution strategy: ${strategy}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve conflict using newest timestamp
|
|
256
|
+
*
|
|
257
|
+
* @private
|
|
258
|
+
* @param {Object} conflict - Conflict with timestamp metadata
|
|
259
|
+
* @returns {string} Content from newest version
|
|
260
|
+
*/
|
|
261
|
+
_resolveNewest(conflict) {
|
|
262
|
+
if (!conflict.localTimestamp || !conflict.remoteTimestamp) {
|
|
263
|
+
// If no timestamps, prefer local by default
|
|
264
|
+
return conflict.localContent;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const localTime = new Date(conflict.localTimestamp).getTime();
|
|
268
|
+
const remoteTime = new Date(conflict.remoteTimestamp).getTime();
|
|
269
|
+
|
|
270
|
+
// Validate timestamps (Invalid Date returns NaN)
|
|
271
|
+
if (isNaN(localTime) || isNaN(remoteTime)) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Invalid timestamps detected in conflict resolution. ` +
|
|
274
|
+
`localTimestamp: "${conflict.localTimestamp}", remoteTimestamp: "${conflict.remoteTimestamp}". ` +
|
|
275
|
+
`Expected format: a valid date string (e.g., ISO 8601).`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return remoteTime > localTime ? conflict.remoteContent : conflict.localContent;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Resolve conflict using custom rules
|
|
283
|
+
*
|
|
284
|
+
* @private
|
|
285
|
+
* @param {Object} conflict - Conflict object
|
|
286
|
+
* @param {Object} rules - Resolution rules
|
|
287
|
+
* @returns {string} Resolved content
|
|
288
|
+
*/
|
|
289
|
+
_resolveRulesBased(conflict, rules) {
|
|
290
|
+
const section = conflict.section || 'body';
|
|
291
|
+
|
|
292
|
+
// Check for section-specific rules
|
|
293
|
+
if (rules[section]) {
|
|
294
|
+
const sectionRules = rules[section];
|
|
295
|
+
|
|
296
|
+
// Extract field name from content (e.g., "priority: high")
|
|
297
|
+
const localMatch = conflict.localContent.match(/^(\w+):\s*(.+)$/);
|
|
298
|
+
const remoteMatch = conflict.remoteContent.match(/^(\w+):\s*(.+)$/);
|
|
299
|
+
|
|
300
|
+
if (localMatch && remoteMatch && localMatch[1] === remoteMatch[1]) {
|
|
301
|
+
const field = localMatch[1];
|
|
302
|
+
const localValue = localMatch[2];
|
|
303
|
+
const remoteValue = remoteMatch[2];
|
|
304
|
+
|
|
305
|
+
// Apply field-specific rule
|
|
306
|
+
if (sectionRules[field] === 'prefer-highest') {
|
|
307
|
+
// For priority fields, prefer higher priority
|
|
308
|
+
const priorities = ['low', 'medium', 'high', 'critical'];
|
|
309
|
+
const localPriority = priorities.indexOf(localValue);
|
|
310
|
+
const remotePriority = priorities.indexOf(remoteValue);
|
|
311
|
+
|
|
312
|
+
return localPriority > remotePriority ? conflict.localContent : conflict.remoteContent;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (sectionRules[field] === 'prefer-local') {
|
|
316
|
+
return conflict.localContent;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (sectionRules[field] === 'prefer-remote') {
|
|
320
|
+
return conflict.remoteContent;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Default: prefer local if no matching rule
|
|
326
|
+
return conflict.localContent;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = ConflictResolver;
|