@wundr.io/cli 1.0.11 → 1.0.13

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.
Files changed (180) hide show
  1. package/bin/wundr.js +8 -4
  2. package/dist/ai/ai-service.d.ts.map +1 -1
  3. package/dist/ai/ai-service.js.map +1 -1
  4. package/dist/ai/claude-client.js.map +1 -1
  5. package/dist/ai/conversation-manager.js.map +1 -1
  6. package/dist/commands/ai.d.ts.map +1 -1
  7. package/dist/commands/ai.js +179 -24
  8. package/dist/commands/ai.js.map +1 -1
  9. package/dist/commands/analyze-optimized.d.ts.map +1 -1
  10. package/dist/commands/analyze-optimized.js +15 -6
  11. package/dist/commands/analyze-optimized.js.map +1 -1
  12. package/dist/commands/batch.d.ts +22 -0
  13. package/dist/commands/batch.d.ts.map +1 -1
  14. package/dist/commands/batch.js +130 -14
  15. package/dist/commands/batch.js.map +1 -1
  16. package/dist/commands/chat.d.ts +1 -0
  17. package/dist/commands/chat.d.ts.map +1 -1
  18. package/dist/commands/chat.js +7 -3
  19. package/dist/commands/chat.js.map +1 -1
  20. package/dist/commands/claude-init.d.ts +1 -1
  21. package/dist/commands/claude-init.d.ts.map +1 -1
  22. package/dist/commands/claude-init.js +16 -16
  23. package/dist/commands/claude-init.js.map +1 -1
  24. package/dist/commands/claude-setup.d.ts +5 -5
  25. package/dist/commands/claude-setup.d.ts.map +1 -1
  26. package/dist/commands/claude-setup.js +65 -59
  27. package/dist/commands/claude-setup.js.map +1 -1
  28. package/dist/commands/computer-setup.d.ts +1 -0
  29. package/dist/commands/computer-setup.d.ts.map +1 -1
  30. package/dist/commands/computer-setup.js +35 -7
  31. package/dist/commands/computer-setup.js.map +1 -1
  32. package/dist/commands/dashboard.js.map +1 -1
  33. package/dist/commands/govern.js.map +1 -1
  34. package/dist/commands/init.d.ts.map +1 -1
  35. package/dist/commands/init.js +3 -3
  36. package/dist/commands/init.js.map +1 -1
  37. package/dist/commands/orchestrator.d.ts.map +1 -1
  38. package/dist/commands/orchestrator.js +11 -4
  39. package/dist/commands/orchestrator.js.map +1 -1
  40. package/dist/commands/performance-optimizer.d.ts.map +1 -1
  41. package/dist/commands/performance-optimizer.js.map +1 -1
  42. package/dist/commands/rag.d.ts.map +1 -1
  43. package/dist/commands/rag.js +9 -6
  44. package/dist/commands/rag.js.map +1 -1
  45. package/dist/commands/setup.d.ts +5 -10
  46. package/dist/commands/setup.d.ts.map +1 -1
  47. package/dist/commands/setup.js +35 -260
  48. package/dist/commands/setup.js.map +1 -1
  49. package/dist/commands/watch.d.ts.map +1 -1
  50. package/dist/commands/watch.js.map +1 -1
  51. package/dist/context/session-manager.js.map +1 -1
  52. package/dist/framework/command-interface.d.ts +349 -0
  53. package/dist/framework/command-interface.d.ts.map +1 -0
  54. package/dist/framework/command-interface.js +101 -0
  55. package/dist/framework/command-interface.js.map +1 -0
  56. package/dist/framework/command-registry.d.ts +173 -0
  57. package/dist/framework/command-registry.d.ts.map +1 -0
  58. package/dist/framework/command-registry.js +734 -0
  59. package/dist/framework/command-registry.js.map +1 -0
  60. package/dist/framework/completion-exporter.d.ts +79 -0
  61. package/dist/framework/completion-exporter.d.ts.map +1 -0
  62. package/dist/framework/completion-exporter.js +259 -0
  63. package/dist/framework/completion-exporter.js.map +1 -0
  64. package/dist/framework/debug-logger.d.ts +163 -0
  65. package/dist/framework/debug-logger.d.ts.map +1 -0
  66. package/dist/framework/debug-logger.js +373 -0
  67. package/dist/framework/debug-logger.js.map +1 -0
  68. package/dist/framework/error-handler.d.ts +196 -0
  69. package/dist/framework/error-handler.d.ts.map +1 -0
  70. package/dist/framework/error-handler.js +613 -0
  71. package/dist/framework/error-handler.js.map +1 -0
  72. package/dist/framework/help-generator.d.ts +78 -0
  73. package/dist/framework/help-generator.d.ts.map +1 -0
  74. package/dist/framework/help-generator.js +414 -0
  75. package/dist/framework/help-generator.js.map +1 -0
  76. package/dist/framework/index.d.ts +62 -0
  77. package/dist/framework/index.d.ts.map +1 -0
  78. package/dist/framework/index.js +95 -0
  79. package/dist/framework/index.js.map +1 -0
  80. package/dist/framework/interactive-repl.d.ts +138 -0
  81. package/dist/framework/interactive-repl.d.ts.map +1 -0
  82. package/dist/framework/interactive-repl.js +567 -0
  83. package/dist/framework/interactive-repl.js.map +1 -0
  84. package/dist/framework/output-formatter.d.ts +274 -0
  85. package/dist/framework/output-formatter.d.ts.map +1 -0
  86. package/dist/framework/output-formatter.js +545 -0
  87. package/dist/framework/output-formatter.js.map +1 -0
  88. package/dist/framework/progress-manager.d.ts +192 -0
  89. package/dist/framework/progress-manager.d.ts.map +1 -0
  90. package/dist/framework/progress-manager.js +408 -0
  91. package/dist/framework/progress-manager.js.map +1 -0
  92. package/dist/interactive/interactive-mode.js.map +1 -1
  93. package/dist/nlp/command-mapper.js.map +1 -1
  94. package/dist/nlp/command-parser.js.map +1 -1
  95. package/dist/nlp/intent-parser.d.ts.map +1 -1
  96. package/dist/nlp/intent-parser.js +4 -2
  97. package/dist/nlp/intent-parser.js.map +1 -1
  98. package/dist/plugins/plugin-manager.d.ts +2 -1
  99. package/dist/plugins/plugin-manager.d.ts.map +1 -1
  100. package/dist/plugins/plugin-manager.js +30 -19
  101. package/dist/plugins/plugin-manager.js.map +1 -1
  102. package/dist/utils/backup-rollback-manager.d.ts.map +1 -1
  103. package/dist/utils/backup-rollback-manager.js +1 -2
  104. package/dist/utils/backup-rollback-manager.js.map +1 -1
  105. package/dist/utils/logger.js.map +1 -1
  106. package/package.json +6 -6
  107. package/src/ai/ai-service.ts +16 -17
  108. package/src/ai/claude-client.ts +16 -16
  109. package/src/ai/conversation-manager.ts +29 -29
  110. package/src/cli.ts +4 -4
  111. package/src/commands/ai.ts +246 -78
  112. package/src/commands/alignment.ts +74 -74
  113. package/src/commands/analyze-optimized.ts +111 -78
  114. package/src/commands/analyze.ts +14 -14
  115. package/src/commands/batch.ts +179 -42
  116. package/src/commands/chat.ts +37 -30
  117. package/src/commands/claude-init.ts +41 -45
  118. package/src/commands/claude-setup.ts +204 -119
  119. package/src/commands/computer-setup.ts +85 -43
  120. package/src/commands/create-command.ts +4 -4
  121. package/src/commands/create.ts +27 -27
  122. package/src/commands/dashboard.ts +24 -24
  123. package/src/commands/govern.ts +25 -25
  124. package/src/commands/governance.ts +34 -34
  125. package/src/commands/guardian.ts +56 -56
  126. package/src/commands/init.ts +25 -22
  127. package/src/commands/orchestrator.ts +68 -41
  128. package/src/commands/performance-optimizer.ts +34 -35
  129. package/src/commands/plugins.ts +27 -27
  130. package/src/commands/project-update.ts +175 -72
  131. package/src/commands/rag.ts +185 -78
  132. package/src/commands/session.ts +35 -35
  133. package/src/commands/setup.ts +40 -344
  134. package/src/commands/test-init.ts +3 -3
  135. package/src/commands/test.ts +4 -4
  136. package/src/commands/watch.ts +28 -29
  137. package/src/commands/worktree.ts +49 -49
  138. package/src/context/context-manager.ts +10 -10
  139. package/src/context/session-manager.ts +41 -41
  140. package/src/framework/command-interface.ts +520 -0
  141. package/src/framework/command-registry.ts +942 -0
  142. package/src/framework/completion-exporter.ts +383 -0
  143. package/src/framework/debug-logger.ts +519 -0
  144. package/src/framework/error-handler.ts +867 -0
  145. package/src/framework/help-generator.ts +540 -0
  146. package/src/framework/index.ts +169 -0
  147. package/src/framework/interactive-repl.ts +703 -0
  148. package/src/framework/output-formatter.ts +834 -0
  149. package/src/framework/progress-manager.ts +539 -0
  150. package/src/index.ts +4 -4
  151. package/src/interactive/interactive-mode.ts +16 -16
  152. package/src/lib/conflict-resolution.ts +799 -9
  153. package/src/lib/merge-strategy.ts +529 -7
  154. package/src/lib/safety-mechanisms.ts +422 -18
  155. package/src/lib/state-detection.ts +1015 -13
  156. package/src/nlp/command-mapper.ts +29 -29
  157. package/src/nlp/command-parser.ts +17 -17
  158. package/src/nlp/intent-classifier.ts +7 -7
  159. package/src/nlp/intent-parser.ts +54 -52
  160. package/src/plugins/plugin-manager.ts +61 -39
  161. package/src/tests/computer-setup-integration.test.ts +46 -15
  162. package/src/types/modules.d.ts +424 -1
  163. package/src/utils/backup-rollback-manager.ts +11 -8
  164. package/src/utils/config-manager.ts +3 -3
  165. package/src/utils/error-handler.ts +2 -2
  166. package/src/utils/logger.ts +22 -22
  167. package/templates/batch/ci-cd.yaml +7 -7
  168. package/test-suites/api/health.spec.ts +20 -23
  169. package/test-suites/helpers/test-config.ts +14 -13
  170. package/test-suites/ui/accessibility.spec.ts +27 -22
  171. package/test-suites/ui/smoke.spec.ts +26 -21
  172. package/dist/commands/computer-setup-commands.d.ts +0 -53
  173. package/dist/commands/computer-setup-commands.d.ts.map +0 -1
  174. package/dist/commands/computer-setup-commands.js +0 -705
  175. package/dist/commands/computer-setup-commands.js.map +0 -1
  176. package/dist/commands/vp.d.ts +0 -7
  177. package/dist/commands/vp.d.ts.map +0 -1
  178. package/dist/commands/vp.js +0 -571
  179. package/dist/commands/vp.js.map +0 -1
  180. package/src/commands/computer-setup-commands.ts +0 -872
@@ -1,28 +1,818 @@
1
1
  /**
2
- * Conflict Resolution - Stub implementation
3
- * TODO: Implement full conflict resolution system
2
+ * Conflict Resolution Module
3
+ *
4
+ * Implements three-way merge conflict detection and resolution.
5
+ * Supports auto-resolution for non-conflicting changes and conflict markers
6
+ * for true line-level conflicts.
4
7
  */
5
8
 
6
- export interface ConflictResolver {
7
- resolve(conflict: UpdateConflict): Promise<ConflictResolutionResult>;
9
+ // ---------------------------------------------------------------------------
10
+ // Types & Interfaces
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export type ConflictSeverity = 'critical' | 'high' | 'medium' | 'low';
14
+ export type ConflictResolution =
15
+ | 'keep-user'
16
+ | 'take-target'
17
+ | 'keep-both'
18
+ | 'manual';
19
+ export type ConflictCategory =
20
+ | 'config'
21
+ | 'template'
22
+ | 'code'
23
+ | 'dependency'
24
+ | 'unknown';
25
+
26
+ export interface ResolutionSuggestion {
27
+ resolution: ConflictResolution;
28
+ confidence: number;
29
+ reasoning: string;
30
+ alternatives: ConflictResolution[];
8
31
  }
9
32
 
10
33
  export interface UpdateConflict {
11
- type: string;
34
+ /** Unique identifier for this conflict */
35
+ id: string;
36
+ /** Absolute path of the conflicting file */
37
+ filePath: string;
38
+ /** Human-readable description of the conflict */
12
39
  description: string;
40
+ /** Conflict category for grouping and auto-resolution */
41
+ category: ConflictCategory;
42
+ /** Whether this conflict can be automatically resolved */
43
+ autoResolvable: boolean;
44
+ /** Suggested resolution strategy */
45
+ suggestion: ResolutionSuggestion;
46
+ /** Severity level */
47
+ severity: ConflictSeverity;
48
+ /** Original base content (ancestor) */
49
+ baseContent: string;
50
+ /** User-modified content (ours) */
51
+ userContent: string;
52
+ /** Incoming target content (theirs) */
53
+ targetContent: string;
54
+ /** Generic local value (for non-textual conflicts) */
13
55
  localValue: unknown;
56
+ /** Generic remote value (for non-textual conflicts) */
14
57
  remoteValue: unknown;
58
+ /** Line number where the conflict originates, if known */
59
+ line?: number;
60
+ /** type field kept for backwards compatibility */
61
+ type: string;
15
62
  }
16
63
 
17
64
  export interface ConflictResolutionResult {
65
+ /** Whether the conflict was resolved */
18
66
  resolved: boolean;
67
+ /** The resolved value / merged content */
19
68
  value: unknown;
69
+ /** Which resolution strategy was applied */
70
+ strategy?: ConflictResolution;
71
+ /** Merged text content when applicable */
72
+ mergedContent?: string;
73
+ /** Conflict markers embedded in content (when strategy is 'manual') */
74
+ conflictMarkers?: string[];
75
+ }
76
+
77
+ export interface ConflictResolverOptions {
78
+ /** Prompt the user when a conflict cannot be auto-resolved */
79
+ interactive?: boolean;
80
+ /** Auto-resolve conflicts rated as 'low' severity */
81
+ autoResolveLow?: boolean;
82
+ /** Auto-resolve conflicts rated as 'medium' severity */
83
+ autoResolveMedium?: boolean;
84
+ }
85
+
86
+ export interface ConflictResolver {
87
+ resolve(conflict: UpdateConflict): Promise<ConflictResolutionResult>;
88
+ startSession(conflicts: UpdateConflict[]): void;
89
+ resolveAll(): Promise<ConflictResolutionResult[]>;
90
+ createUpdateConflict(
91
+ mergeConflict: { line: number; description: string },
92
+ filePath: string
93
+ ): UpdateConflict;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Diff helpers
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * Compute a line-level longest-common-subsequence between two string arrays.
102
+ * Returns an array of "edit" objects describing the differences.
103
+ */
104
+ type DiffOp = 'equal' | 'insert' | 'delete';
105
+
106
+ interface DiffEntry {
107
+ op: DiffOp;
108
+ line: string;
109
+ indexA: number;
110
+ indexB: number;
111
+ }
112
+
113
+ function lcsLength(a: string[], b: string[]): number[][] {
114
+ const m = a.length;
115
+ const n = b.length;
116
+ // Use a flat Int32Array for performance
117
+ const dp: number[] = new Array((m + 1) * (n + 1)).fill(0);
118
+ const idx = (i: number, j: number) => i * (n + 1) + j;
119
+ for (let i = 1; i <= m; i++) {
120
+ for (let j = 1; j <= n; j++) {
121
+ dp[idx(i, j)] =
122
+ a[i - 1] === b[j - 1]
123
+ ? dp[idx(i - 1, j - 1)] + 1
124
+ : Math.max(dp[idx(i - 1, j)], dp[idx(i, j - 1)]);
125
+ }
126
+ }
127
+ // Convert flat array back to 2D
128
+ const table: number[][] = [];
129
+ for (let i = 0; i <= m; i++) {
130
+ table.push(dp.slice(i * (n + 1), (i + 1) * (n + 1)));
131
+ }
132
+ return table;
133
+ }
134
+
135
+ function diff(a: string[], b: string[]): DiffEntry[] {
136
+ const table = lcsLength(a, b);
137
+ const result: DiffEntry[] = [];
138
+
139
+ let i = a.length;
140
+ let j = b.length;
141
+
142
+ while (i > 0 || j > 0) {
143
+ if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
144
+ result.unshift({
145
+ op: 'equal',
146
+ line: a[i - 1],
147
+ indexA: i - 1,
148
+ indexB: j - 1,
149
+ });
150
+ i--;
151
+ j--;
152
+ } else if (j > 0 && (i === 0 || table[i][j - 1] >= table[i - 1][j])) {
153
+ result.unshift({
154
+ op: 'insert',
155
+ line: b[j - 1],
156
+ indexA: i,
157
+ indexB: j - 1,
158
+ });
159
+ j--;
160
+ } else {
161
+ result.unshift({
162
+ op: 'delete',
163
+ line: a[i - 1],
164
+ indexA: i - 1,
165
+ indexB: j,
166
+ });
167
+ i--;
168
+ }
169
+ }
170
+
171
+ return result;
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Three-way merge core
176
+ // ---------------------------------------------------------------------------
177
+
178
+ export interface MergeHunk {
179
+ /** Lines shared by all three versions */
180
+ common: string[];
181
+ /** Lines that only appear in base (deleted in both ours and theirs) */
182
+ baseOnly: string[];
183
+ /** Lines modified/added in ours */
184
+ ours: string[];
185
+ /** Lines modified/added in theirs */
186
+ theirs: string[];
187
+ /** Whether both sides changed the same base lines differently */
188
+ isConflict: boolean;
189
+ /** Starting line number in the base (0-indexed) */
190
+ baseStartLine: number;
191
+ }
192
+
193
+ export interface ThreeWayMergeResult {
194
+ /** Merged content (may contain conflict markers for unresolved conflicts) */
195
+ mergedContent: string;
196
+ /** True when there are no conflict markers */
197
+ hasConflicts: boolean;
198
+ /** Individual conflict hunks */
199
+ conflictHunks: MergeHunk[];
200
+ /** Auto-resolved hunks (non-conflicting changes applied) */
201
+ resolvedHunks: number;
202
+ }
203
+
204
+ const CONFLICT_START = '<<<<<<< ours';
205
+ const CONFLICT_SEP = '=======';
206
+ const CONFLICT_END = '>>>>>>> theirs';
207
+
208
+ /**
209
+ * Perform a three-way merge of text content.
210
+ *
211
+ * Strategy:
212
+ * 1. Diff base -> ours to find what we changed.
213
+ * 2. Diff base -> theirs to find what they changed.
214
+ * 3. Walk through base lines. Where only one side changed, take that change
215
+ * (auto-resolve). Where both sides changed the same region differently,
216
+ * emit conflict markers.
217
+ */
218
+ export function threeWayMergeText(
219
+ base: string,
220
+ ours: string,
221
+ theirs: string
222
+ ): ThreeWayMergeResult {
223
+ const baseLines = base.split('\n');
224
+ const oursLines = ours.split('\n');
225
+ const theirsLines = theirs.split('\n');
226
+
227
+ // Build change maps: baseIndex -> replacement lines (null = deleted)
228
+ const oursChanges = buildChangeMap(baseLines, oursLines);
229
+ const theirsChanges = buildChangeMap(baseLines, theirsLines);
230
+
231
+ const mergedLines: string[] = [];
232
+ const conflictHunks: MergeHunk[] = [];
233
+ let resolvedHunks = 0;
234
+ let hasConflicts = false;
235
+
236
+ let baseIdx = 0;
237
+ while (baseIdx < baseLines.length) {
238
+ const oursChange = oursChanges.get(baseIdx);
239
+ const theirsChange = theirsChanges.get(baseIdx);
240
+
241
+ const oursModified = oursChange !== undefined;
242
+ const theirsModified = theirsChange !== undefined;
243
+
244
+ if (!oursModified && !theirsModified) {
245
+ // Both sides kept the line unchanged
246
+ mergedLines.push(baseLines[baseIdx]);
247
+ baseIdx++;
248
+ continue;
249
+ }
250
+
251
+ // Determine the extent of this change region on the base side
252
+ const regionEnd = findRegionEnd(
253
+ baseIdx,
254
+ oursChanges,
255
+ theirsChanges,
256
+ baseLines.length
257
+ );
258
+
259
+ // Collect all base lines in the region
260
+ const baseRegion: string[] = baseLines.slice(baseIdx, regionEnd);
261
+
262
+ // Collect ours replacement for the whole region
263
+ const oursRegion = collectRegionReplacement(
264
+ baseIdx,
265
+ regionEnd,
266
+ oursChanges,
267
+ baseLines
268
+ );
269
+ const theirsRegion = collectRegionReplacement(
270
+ baseIdx,
271
+ regionEnd,
272
+ theirsChanges,
273
+ baseLines
274
+ );
275
+
276
+ const oursChanged = !arraysEqual(baseRegion, oursRegion);
277
+ const theirsChanged = !arraysEqual(baseRegion, theirsRegion);
278
+
279
+ if (oursChanged && !theirsChanged) {
280
+ // Only we changed this region - take ours
281
+ mergedLines.push(...oursRegion);
282
+ resolvedHunks++;
283
+ } else if (!oursChanged && theirsChanged) {
284
+ // Only they changed this region - take theirs
285
+ mergedLines.push(...theirsRegion);
286
+ resolvedHunks++;
287
+ } else if (arraysEqual(oursRegion, theirsRegion)) {
288
+ // Both changed identically - take either (ours)
289
+ mergedLines.push(...oursRegion);
290
+ resolvedHunks++;
291
+ } else {
292
+ // True conflict - both sides changed the same region differently
293
+ hasConflicts = true;
294
+ mergedLines.push(CONFLICT_START);
295
+ mergedLines.push(...oursRegion);
296
+ mergedLines.push(CONFLICT_SEP);
297
+ mergedLines.push(...theirsRegion);
298
+ mergedLines.push(CONFLICT_END);
299
+
300
+ conflictHunks.push({
301
+ common: [],
302
+ baseOnly: baseRegion,
303
+ ours: oursRegion,
304
+ theirs: theirsRegion,
305
+ isConflict: true,
306
+ baseStartLine: baseIdx,
307
+ });
308
+ }
309
+
310
+ baseIdx = regionEnd;
311
+ }
312
+
313
+ return {
314
+ mergedContent: mergedLines.join('\n'),
315
+ hasConflicts,
316
+ conflictHunks,
317
+ resolvedHunks,
318
+ };
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // Change map builder
323
+ // ---------------------------------------------------------------------------
324
+
325
+ /**
326
+ * Represents what happened to a contiguous block of base lines.
327
+ * baseStartIndex -> { baseCount, replacementLines }
328
+ */
329
+ type ChangeMap = Map<number, { baseCount: number; lines: string[] }>;
330
+
331
+ function buildChangeMap(
332
+ baseLines: string[],
333
+ changedLines: string[]
334
+ ): ChangeMap {
335
+ const entries = diff(baseLines, changedLines);
336
+ const map: ChangeMap = new Map();
337
+
338
+ let baseIdx = 0;
339
+ let entryIdx = 0;
340
+
341
+ while (entryIdx < entries.length) {
342
+ const entry = entries[entryIdx];
343
+
344
+ if (entry.op === 'equal') {
345
+ baseIdx++;
346
+ entryIdx++;
347
+ continue;
348
+ }
349
+
350
+ // Start of a change region
351
+ const regionBaseStart = baseIdx;
352
+ const deletedLines: string[] = [];
353
+ const insertedLines: string[] = [];
354
+
355
+ // Consume all consecutive non-equal ops
356
+ while (entryIdx < entries.length && entries[entryIdx].op !== 'equal') {
357
+ const e = entries[entryIdx];
358
+ if (e.op === 'delete') {
359
+ deletedLines.push(e.line);
360
+ baseIdx++;
361
+ } else {
362
+ insertedLines.push(e.line);
363
+ }
364
+ entryIdx++;
365
+ }
366
+
367
+ map.set(regionBaseStart, {
368
+ baseCount: deletedLines.length,
369
+ lines: insertedLines,
370
+ });
371
+ }
372
+
373
+ return map;
374
+ }
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Region helpers
378
+ // ---------------------------------------------------------------------------
379
+
380
+ /**
381
+ * Find the end (exclusive) of the change region starting at baseStart.
382
+ * Expands to cover any overlapping changes from both sides.
383
+ */
384
+ function findRegionEnd(
385
+ baseStart: number,
386
+ oursChanges: ChangeMap,
387
+ theirsChanges: ChangeMap,
388
+ baseLength: number
389
+ ): number {
390
+ let end = baseStart + 1;
391
+
392
+ // Expand based on ours changes
393
+ const oursChange = oursChanges.get(baseStart);
394
+ if (oursChange) {
395
+ end = Math.max(end, baseStart + Math.max(oursChange.baseCount, 1));
396
+ }
397
+
398
+ // Expand based on theirs changes
399
+ const theirsChange = theirsChanges.get(baseStart);
400
+ if (theirsChange) {
401
+ end = Math.max(end, baseStart + Math.max(theirsChange.baseCount, 1));
402
+ }
403
+
404
+ return Math.min(end, baseLength);
20
405
  }
21
406
 
22
- export function createConflictResolver(): ConflictResolver {
407
+ /**
408
+ * Collect the replacement lines for the base region [start, end).
409
+ * Lines not covered by a change entry are taken as-is from base.
410
+ */
411
+ function collectRegionReplacement(
412
+ start: number,
413
+ end: number,
414
+ changes: ChangeMap,
415
+ baseLines: string[]
416
+ ): string[] {
417
+ const result: string[] = [];
418
+ let idx = start;
419
+
420
+ while (idx < end) {
421
+ const change = changes.get(idx);
422
+ if (change) {
423
+ result.push(...change.lines);
424
+ idx += Math.max(change.baseCount, 1);
425
+ } else {
426
+ result.push(baseLines[idx]);
427
+ idx++;
428
+ }
429
+ }
430
+
431
+ return result;
432
+ }
433
+
434
+ function arraysEqual(a: string[], b: string[]): boolean {
435
+ if (a.length !== b.length) return false;
436
+ for (let i = 0; i < a.length; i++) {
437
+ if (a[i] !== b[i]) return false;
438
+ }
439
+ return true;
440
+ }
441
+
442
+ // ---------------------------------------------------------------------------
443
+ // Conflict resolver implementation
444
+ // ---------------------------------------------------------------------------
445
+
446
+ let conflictIdCounter = 0;
447
+
448
+ function nextConflictId(): string {
449
+ conflictIdCounter++;
450
+ return `conflict-${Date.now()}-${conflictIdCounter}`;
451
+ }
452
+
453
+ function inferCategory(filePath: string): ConflictCategory {
454
+ const lower = filePath.toLowerCase();
455
+ if (lower.includes('package.json') || lower.includes('.npmrc'))
456
+ return 'dependency';
457
+ if (
458
+ lower.endsWith('.json') ||
459
+ lower.endsWith('.yaml') ||
460
+ lower.endsWith('.yml') ||
461
+ lower.endsWith('.toml') ||
462
+ lower.endsWith('.env')
463
+ )
464
+ return 'config';
465
+ if (
466
+ lower.endsWith('.ts') ||
467
+ lower.endsWith('.tsx') ||
468
+ lower.endsWith('.js') ||
469
+ lower.endsWith('.jsx')
470
+ )
471
+ return 'code';
472
+ if (
473
+ lower.endsWith('.md') ||
474
+ lower.endsWith('.txt') ||
475
+ lower.endsWith('.html') ||
476
+ lower.endsWith('.hbs')
477
+ )
478
+ return 'template';
479
+ return 'unknown';
480
+ }
481
+
482
+ function buildSuggestion(
483
+ category: ConflictCategory,
484
+ severity: ConflictSeverity
485
+ ): ResolutionSuggestion {
486
+ let resolution: ConflictResolution;
487
+ let confidence: number;
488
+ let reasoning: string;
489
+ const alternatives: ConflictResolution[] = [
490
+ 'keep-user',
491
+ 'take-target',
492
+ 'keep-both',
493
+ 'manual',
494
+ ];
495
+
496
+ switch (category) {
497
+ case 'config':
498
+ resolution = 'keep-both';
499
+ confidence = 0.7;
500
+ reasoning =
501
+ 'Config files often have user-specific values that should be preserved alongside new defaults.';
502
+ break;
503
+ case 'dependency':
504
+ resolution = 'take-target';
505
+ confidence = 0.8;
506
+ reasoning =
507
+ 'Dependency updates typically should take the newer version from the target to ensure compatibility.';
508
+ break;
509
+ case 'template':
510
+ resolution = 'keep-user';
511
+ confidence = 0.75;
512
+ reasoning =
513
+ 'Template customisations represent user intent and should be preserved by default.';
514
+ break;
515
+ case 'code':
516
+ if (severity === 'low') {
517
+ resolution = 'take-target';
518
+ confidence = 0.65;
519
+ reasoning =
520
+ 'Low-severity code conflicts can usually accept the incoming change safely.';
521
+ } else {
522
+ resolution = 'manual';
523
+ confidence = 0.5;
524
+ reasoning =
525
+ 'Code conflicts with significant changes require manual review to ensure correctness.';
526
+ }
527
+ break;
528
+ default:
529
+ resolution = 'manual';
530
+ confidence = 0.4;
531
+ reasoning =
532
+ 'Unable to determine a safe automatic resolution strategy for this file type.';
533
+ }
534
+
23
535
  return {
24
- async resolve(_conflict: UpdateConflict): Promise<ConflictResolutionResult> {
25
- throw new Error('Conflict resolution not yet implemented');
26
- },
536
+ resolution,
537
+ confidence,
538
+ reasoning,
539
+ alternatives: alternatives.filter(a => a !== resolution),
27
540
  };
28
541
  }
542
+
543
+ function computeSeverity(
544
+ baseContent: string,
545
+ userContent: string,
546
+ targetContent: string,
547
+ category: ConflictCategory
548
+ ): ConflictSeverity {
549
+ if (category === 'dependency') return 'medium';
550
+
551
+ const baseLinesCount = baseContent.split('\n').length;
552
+ const oursLinesDelta = Math.abs(
553
+ userContent.split('\n').length - baseLinesCount
554
+ );
555
+ const theirsLinesDelta = Math.abs(
556
+ targetContent.split('\n').length - baseLinesCount
557
+ );
558
+ const combinedDelta = oursLinesDelta + theirsLinesDelta;
559
+
560
+ if (category === 'code') {
561
+ if (combinedDelta > 50) return 'critical';
562
+ if (combinedDelta > 20) return 'high';
563
+ if (combinedDelta > 5) return 'medium';
564
+ return 'low';
565
+ }
566
+
567
+ if (combinedDelta > 30) return 'high';
568
+ if (combinedDelta > 10) return 'medium';
569
+ return 'low';
570
+ }
571
+
572
+ /**
573
+ * Resolve a single conflict using the three-way merge strategy.
574
+ *
575
+ * - Strings: perform text three-way merge.
576
+ * - Non-strings: compare localValue vs remoteValue and either auto-resolve
577
+ * or report a conflict.
578
+ */
579
+ async function resolveConflict(
580
+ conflict: UpdateConflict,
581
+ options: ConflictResolverOptions
582
+ ): Promise<ConflictResolutionResult> {
583
+ // Text-based resolution (when content fields are populated)
584
+ if (
585
+ typeof conflict.baseContent === 'string' &&
586
+ typeof conflict.userContent === 'string' &&
587
+ typeof conflict.targetContent === 'string' &&
588
+ (conflict.baseContent || conflict.userContent || conflict.targetContent)
589
+ ) {
590
+ return resolveTextConflict(conflict, options);
591
+ }
592
+
593
+ // Non-textual value conflict
594
+ return resolveValueConflict(conflict, options);
595
+ }
596
+
597
+ function resolveTextConflict(
598
+ conflict: UpdateConflict,
599
+ options: ConflictResolverOptions
600
+ ): ConflictResolutionResult {
601
+ const {
602
+ baseContent,
603
+ userContent,
604
+ targetContent,
605
+ severity,
606
+ suggestion,
607
+ autoResolvable,
608
+ } = conflict;
609
+
610
+ // If content is identical between user and target, no real conflict
611
+ if (userContent === targetContent) {
612
+ return {
613
+ resolved: true,
614
+ value: userContent,
615
+ strategy: 'keep-user',
616
+ mergedContent: userContent,
617
+ };
618
+ }
619
+
620
+ // If only the user changed it (base === theirs), keep user
621
+ if (baseContent === targetContent) {
622
+ return {
623
+ resolved: true,
624
+ value: userContent,
625
+ strategy: 'keep-user',
626
+ mergedContent: userContent,
627
+ };
628
+ }
629
+
630
+ // If only the target changed it (base === ours), take target
631
+ if (baseContent === userContent) {
632
+ return {
633
+ resolved: true,
634
+ value: targetContent,
635
+ strategy: 'take-target',
636
+ mergedContent: targetContent,
637
+ };
638
+ }
639
+
640
+ // Both sides changed - attempt three-way merge
641
+ const mergeResult = threeWayMergeText(
642
+ baseContent,
643
+ userContent,
644
+ targetContent
645
+ );
646
+
647
+ if (!mergeResult.hasConflicts) {
648
+ // Clean merge - auto-resolved
649
+ return {
650
+ resolved: true,
651
+ value: mergeResult.mergedContent,
652
+ strategy: 'keep-both',
653
+ mergedContent: mergeResult.mergedContent,
654
+ };
655
+ }
656
+
657
+ // Remaining conflicts - check if we can auto-resolve
658
+ const canAutoResolve =
659
+ autoResolvable ||
660
+ (severity === 'low' && options.autoResolveLow) ||
661
+ (severity === 'medium' && options.autoResolveMedium);
662
+
663
+ if (canAutoResolve) {
664
+ // Apply the suggested resolution strategy
665
+ const resolution = suggestion.resolution;
666
+ let resolved: string;
667
+
668
+ if (resolution === 'keep-user') {
669
+ resolved = userContent;
670
+ } else if (resolution === 'take-target') {
671
+ resolved = targetContent;
672
+ } else {
673
+ // keep-both: use the content with conflict markers so callers can see what merged
674
+ resolved = mergeResult.mergedContent;
675
+ }
676
+
677
+ return {
678
+ resolved: true,
679
+ value: resolved,
680
+ strategy: resolution,
681
+ mergedContent: resolved,
682
+ };
683
+ }
684
+
685
+ // Cannot auto-resolve: return conflict markers for manual intervention
686
+ return {
687
+ resolved: false,
688
+ value: mergeResult.mergedContent,
689
+ strategy: 'manual',
690
+ mergedContent: mergeResult.mergedContent,
691
+ conflictMarkers: mergeResult.conflictHunks.map(
692
+ h =>
693
+ `${CONFLICT_START}\n${h.ours.join('\n')}\n${CONFLICT_SEP}\n${h.theirs.join('\n')}\n${CONFLICT_END}`
694
+ ),
695
+ };
696
+ }
697
+
698
+ function resolveValueConflict(
699
+ conflict: UpdateConflict,
700
+ options: ConflictResolverOptions
701
+ ): ConflictResolutionResult {
702
+ const { localValue, remoteValue, severity, suggestion, autoResolvable } =
703
+ conflict;
704
+
705
+ if (localValue === remoteValue) {
706
+ return { resolved: true, value: localValue, strategy: 'keep-user' };
707
+ }
708
+
709
+ const canAutoResolve =
710
+ autoResolvable ||
711
+ (severity === 'low' && options.autoResolveLow) ||
712
+ (severity === 'medium' && options.autoResolveMedium);
713
+
714
+ if (canAutoResolve) {
715
+ const value =
716
+ suggestion.resolution === 'take-target' ? remoteValue : localValue;
717
+ return { resolved: true, value, strategy: suggestion.resolution };
718
+ }
719
+
720
+ return { resolved: false, value: null, strategy: 'manual' };
721
+ }
722
+
723
+ // ---------------------------------------------------------------------------
724
+ // Factory
725
+ // ---------------------------------------------------------------------------
726
+
727
+ class ConflictResolverImpl implements ConflictResolver {
728
+ private projectRoot: string;
729
+ private options: ConflictResolverOptions;
730
+ private sessionConflicts: UpdateConflict[] = [];
731
+
732
+ constructor(projectRoot: string, options: ConflictResolverOptions = {}) {
733
+ this.projectRoot = projectRoot;
734
+ this.options = {
735
+ interactive: false,
736
+ autoResolveLow: true,
737
+ autoResolveMedium: false,
738
+ ...options,
739
+ };
740
+ }
741
+
742
+ /**
743
+ * Resolve a single conflict.
744
+ */
745
+ async resolve(conflict: UpdateConflict): Promise<ConflictResolutionResult> {
746
+ return resolveConflict(conflict, this.options);
747
+ }
748
+
749
+ /**
750
+ * Begin a resolution session with a list of conflicts.
751
+ */
752
+ startSession(conflicts: UpdateConflict[]): void {
753
+ this.sessionConflicts = [...conflicts];
754
+ }
755
+
756
+ /**
757
+ * Resolve all conflicts registered with startSession().
758
+ */
759
+ async resolveAll(): Promise<ConflictResolutionResult[]> {
760
+ const results: ConflictResolutionResult[] = [];
761
+ for (const conflict of this.sessionConflicts) {
762
+ const result = await this.resolve(conflict);
763
+ results.push(result);
764
+ }
765
+ this.sessionConflicts = [];
766
+ return results;
767
+ }
768
+
769
+ /**
770
+ * Convert a raw merge conflict (line + description) into a full UpdateConflict.
771
+ * The merge-strategy module produces minimal conflict objects; this method
772
+ * enriches them with metadata for resolution.
773
+ */
774
+ createUpdateConflict(
775
+ mergeConflict: { line: number; description: string },
776
+ filePath: string
777
+ ): UpdateConflict {
778
+ const category = inferCategory(filePath);
779
+ const severity = 'medium' as ConflictSeverity;
780
+ const suggestion = buildSuggestion(category, severity);
781
+
782
+ return {
783
+ id: nextConflictId(),
784
+ filePath,
785
+ description: mergeConflict.description,
786
+ type: 'merge',
787
+ category,
788
+ autoResolvable: false,
789
+ suggestion,
790
+ severity,
791
+ baseContent: '',
792
+ userContent: '',
793
+ targetContent: '',
794
+ localValue: null,
795
+ remoteValue: null,
796
+ line: mergeConflict.line,
797
+ };
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Create a ConflictResolver instance.
803
+ *
804
+ * Accepts an optional projectRoot and options object so the factory
805
+ * signature matches the usage in project-update.ts:
806
+ *
807
+ * createConflictResolver(projectRoot, { interactive, autoResolveLow, autoResolveMedium })
808
+ *
809
+ * and also the original zero-argument form:
810
+ *
811
+ * createConflictResolver()
812
+ */
813
+ export function createConflictResolver(
814
+ projectRoot: string = process.cwd(),
815
+ options: ConflictResolverOptions = {}
816
+ ): ConflictResolver {
817
+ return new ConflictResolverImpl(projectRoot, options);
818
+ }