@wundr.io/cli 1.0.11 → 1.0.12
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/bin/wundr.js +8 -4
- package/package.json +23 -23
- package/src/ai/ai-service.ts +16 -17
- package/src/ai/claude-client.ts +16 -16
- package/src/ai/conversation-manager.ts +29 -29
- package/src/cli.ts +4 -4
- package/src/commands/ai.ts +246 -78
- package/src/commands/alignment.ts +74 -74
- package/src/commands/analyze-optimized.ts +111 -78
- package/src/commands/analyze.ts +14 -14
- package/src/commands/batch.ts +179 -42
- package/src/commands/chat.ts +37 -30
- package/src/commands/claude-init.ts +41 -45
- package/src/commands/claude-setup.ts +204 -119
- package/src/commands/computer-setup.ts +85 -43
- package/src/commands/create-command.ts +4 -4
- package/src/commands/create.ts +27 -27
- package/src/commands/dashboard.ts +24 -24
- package/src/commands/govern.ts +25 -25
- package/src/commands/governance.ts +34 -34
- package/src/commands/guardian.ts +56 -56
- package/src/commands/init.ts +25 -22
- package/src/commands/orchestrator.ts +68 -41
- package/src/commands/performance-optimizer.ts +34 -35
- package/src/commands/plugins.ts +27 -27
- package/src/commands/project-update.ts +175 -72
- package/src/commands/rag.ts +185 -78
- package/src/commands/session.ts +35 -35
- package/src/commands/setup.ts +40 -344
- package/src/commands/test-init.ts +3 -3
- package/src/commands/test.ts +4 -4
- package/src/commands/watch.ts +28 -29
- package/src/commands/worktree.ts +49 -49
- package/src/context/context-manager.ts +10 -10
- package/src/context/session-manager.ts +41 -41
- package/src/framework/command-interface.ts +520 -0
- package/src/framework/command-registry.ts +942 -0
- package/src/framework/completion-exporter.ts +383 -0
- package/src/framework/debug-logger.ts +519 -0
- package/src/framework/error-handler.ts +867 -0
- package/src/framework/help-generator.ts +540 -0
- package/src/framework/index.ts +169 -0
- package/src/framework/interactive-repl.ts +703 -0
- package/src/framework/output-formatter.ts +834 -0
- package/src/framework/progress-manager.ts +539 -0
- package/src/index.ts +4 -4
- package/src/interactive/interactive-mode.ts +16 -16
- package/src/lib/conflict-resolution.ts +799 -9
- package/src/lib/merge-strategy.ts +529 -7
- package/src/lib/safety-mechanisms.ts +422 -18
- package/src/lib/state-detection.ts +1015 -13
- package/src/nlp/command-mapper.ts +29 -29
- package/src/nlp/command-parser.ts +17 -17
- package/src/nlp/intent-classifier.ts +7 -7
- package/src/nlp/intent-parser.ts +54 -52
- package/src/plugins/plugin-manager.ts +61 -39
- package/src/tests/computer-setup-integration.test.ts +46 -15
- package/src/types/modules.d.ts +424 -1
- package/src/utils/backup-rollback-manager.ts +11 -8
- package/src/utils/config-manager.ts +3 -3
- package/src/utils/error-handler.ts +2 -2
- package/src/utils/logger.ts +22 -22
- package/templates/batch/ci-cd.yaml +7 -7
- package/test-suites/api/health.spec.ts +20 -23
- package/test-suites/helpers/test-config.ts +14 -13
- package/test-suites/ui/accessibility.spec.ts +27 -22
- package/test-suites/ui/smoke.spec.ts +26 -21
- package/LICENSE +0 -21
- package/dist/ai/ai-service.d.ts +0 -152
- package/dist/ai/ai-service.d.ts.map +0 -1
- package/dist/ai/ai-service.js +0 -430
- package/dist/ai/ai-service.js.map +0 -1
- package/dist/ai/claude-client.d.ts +0 -130
- package/dist/ai/claude-client.d.ts.map +0 -1
- package/dist/ai/claude-client.js +0 -340
- package/dist/ai/claude-client.js.map +0 -1
- package/dist/ai/conversation-manager.d.ts +0 -164
- package/dist/ai/conversation-manager.d.ts.map +0 -1
- package/dist/ai/conversation-manager.js +0 -614
- package/dist/ai/conversation-manager.js.map +0 -1
- package/dist/ai/index.d.ts +0 -5
- package/dist/ai/index.d.ts.map +0 -1
- package/dist/ai/index.js +0 -8
- package/dist/ai/index.js.map +0 -1
- package/dist/cli.d.ts +0 -36
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -192
- package/dist/cli.js.map +0 -1
- package/dist/commands/ai.d.ts +0 -89
- package/dist/commands/ai.d.ts.map +0 -1
- package/dist/commands/ai.js +0 -799
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/alignment.d.ts +0 -78
- package/dist/commands/alignment.d.ts.map +0 -1
- package/dist/commands/alignment.js +0 -817
- package/dist/commands/alignment.js.map +0 -1
- package/dist/commands/analyze-optimized.d.ts +0 -14
- package/dist/commands/analyze-optimized.d.ts.map +0 -1
- package/dist/commands/analyze-optimized.js +0 -600
- package/dist/commands/analyze-optimized.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -65
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -435
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/batch.d.ts +0 -71
- package/dist/commands/batch.d.ts.map +0 -1
- package/dist/commands/batch.js +0 -738
- package/dist/commands/batch.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -71
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -674
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/claude-init.d.ts +0 -28
- package/dist/commands/claude-init.d.ts.map +0 -1
- package/dist/commands/claude-init.js +0 -591
- package/dist/commands/claude-init.js.map +0 -1
- package/dist/commands/claude-setup.d.ts +0 -119
- package/dist/commands/claude-setup.d.ts.map +0 -1
- package/dist/commands/claude-setup.js +0 -1073
- package/dist/commands/claude-setup.js.map +0 -1
- package/dist/commands/computer-setup-commands.d.ts +0 -53
- package/dist/commands/computer-setup-commands.d.ts.map +0 -1
- package/dist/commands/computer-setup-commands.js +0 -705
- package/dist/commands/computer-setup-commands.js.map +0 -1
- package/dist/commands/computer-setup.d.ts +0 -7
- package/dist/commands/computer-setup.d.ts.map +0 -1
- package/dist/commands/computer-setup.js +0 -849
- package/dist/commands/computer-setup.js.map +0 -1
- package/dist/commands/create-command.d.ts +0 -7
- package/dist/commands/create-command.d.ts.map +0 -1
- package/dist/commands/create-command.js +0 -158
- package/dist/commands/create-command.js.map +0 -1
- package/dist/commands/create.d.ts +0 -74
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -556
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dashboard.d.ts +0 -91
- package/dist/commands/dashboard.d.ts.map +0 -1
- package/dist/commands/dashboard.js +0 -538
- package/dist/commands/dashboard.js.map +0 -1
- package/dist/commands/govern.d.ts +0 -70
- package/dist/commands/govern.d.ts.map +0 -1
- package/dist/commands/govern.js +0 -481
- package/dist/commands/govern.js.map +0 -1
- package/dist/commands/governance.d.ts +0 -17
- package/dist/commands/governance.d.ts.map +0 -1
- package/dist/commands/governance.js +0 -703
- package/dist/commands/governance.js.map +0 -1
- package/dist/commands/guardian.d.ts +0 -20
- package/dist/commands/guardian.d.ts.map +0 -1
- package/dist/commands/guardian.js +0 -597
- package/dist/commands/guardian.js.map +0 -1
- package/dist/commands/init.d.ts +0 -59
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -650
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/orchestrator.d.ts +0 -7
- package/dist/commands/orchestrator.d.ts.map +0 -1
- package/dist/commands/orchestrator.js +0 -571
- package/dist/commands/orchestrator.js.map +0 -1
- package/dist/commands/performance-optimizer.d.ts +0 -30
- package/dist/commands/performance-optimizer.d.ts.map +0 -1
- package/dist/commands/performance-optimizer.js +0 -650
- package/dist/commands/performance-optimizer.js.map +0 -1
- package/dist/commands/plugins.d.ts +0 -87
- package/dist/commands/plugins.d.ts.map +0 -1
- package/dist/commands/plugins.js +0 -685
- package/dist/commands/plugins.js.map +0 -1
- package/dist/commands/rag.d.ts +0 -7
- package/dist/commands/rag.d.ts.map +0 -1
- package/dist/commands/rag.js +0 -748
- package/dist/commands/rag.js.map +0 -1
- package/dist/commands/session.d.ts +0 -41
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/session.js +0 -441
- package/dist/commands/session.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -397
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/test-init.d.ts +0 -9
- package/dist/commands/test-init.d.ts.map +0 -1
- package/dist/commands/test-init.js +0 -222
- package/dist/commands/test-init.js.map +0 -1
- package/dist/commands/test.d.ts +0 -25
- package/dist/commands/test.d.ts.map +0 -1
- package/dist/commands/test.js +0 -217
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/vp.d.ts +0 -7
- package/dist/commands/vp.d.ts.map +0 -1
- package/dist/commands/vp.js +0 -571
- package/dist/commands/vp.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -76
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js +0 -613
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/worktree.d.ts +0 -63
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/commands/worktree.js +0 -774
- package/dist/commands/worktree.js.map +0 -1
- package/dist/context/context-manager.d.ts +0 -155
- package/dist/context/context-manager.d.ts.map +0 -1
- package/dist/context/context-manager.js +0 -383
- package/dist/context/context-manager.js.map +0 -1
- package/dist/context/index.d.ts +0 -3
- package/dist/context/index.d.ts.map +0 -1
- package/dist/context/index.js +0 -6
- package/dist/context/index.js.map +0 -1
- package/dist/context/session-manager.d.ts +0 -207
- package/dist/context/session-manager.d.ts.map +0 -1
- package/dist/context/session-manager.js +0 -686
- package/dist/context/session-manager.js.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/interactive/interactive-mode.d.ts +0 -76
- package/dist/interactive/interactive-mode.d.ts.map +0 -1
- package/dist/interactive/interactive-mode.js +0 -732
- package/dist/interactive/interactive-mode.js.map +0 -1
- package/dist/nlp/command-mapper.d.ts +0 -174
- package/dist/nlp/command-mapper.d.ts.map +0 -1
- package/dist/nlp/command-mapper.js +0 -624
- package/dist/nlp/command-mapper.js.map +0 -1
- package/dist/nlp/command-parser.d.ts +0 -106
- package/dist/nlp/command-parser.d.ts.map +0 -1
- package/dist/nlp/command-parser.js +0 -417
- package/dist/nlp/command-parser.js.map +0 -1
- package/dist/nlp/index.d.ts +0 -5
- package/dist/nlp/index.d.ts.map +0 -1
- package/dist/nlp/index.js +0 -8
- package/dist/nlp/index.js.map +0 -1
- package/dist/nlp/intent-classifier.d.ts +0 -59
- package/dist/nlp/intent-classifier.d.ts.map +0 -1
- package/dist/nlp/intent-classifier.js +0 -384
- package/dist/nlp/intent-classifier.js.map +0 -1
- package/dist/nlp/intent-parser.d.ts +0 -152
- package/dist/nlp/intent-parser.d.ts.map +0 -1
- package/dist/nlp/intent-parser.js +0 -744
- package/dist/nlp/intent-parser.js.map +0 -1
- package/dist/plugins/plugin-manager.d.ts +0 -120
- package/dist/plugins/plugin-manager.d.ts.map +0 -1
- package/dist/plugins/plugin-manager.js +0 -595
- package/dist/plugins/plugin-manager.js.map +0 -1
- package/dist/types/index.d.ts +0 -224
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/backup-rollback-manager.d.ts +0 -72
- package/dist/utils/backup-rollback-manager.d.ts.map +0 -1
- package/dist/utils/backup-rollback-manager.js +0 -289
- package/dist/utils/backup-rollback-manager.js.map +0 -1
- package/dist/utils/claude-config-installer.d.ts +0 -98
- package/dist/utils/claude-config-installer.d.ts.map +0 -1
- package/dist/utils/claude-config-installer.js +0 -678
- package/dist/utils/claude-config-installer.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -73
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -339
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/error-handler.d.ts +0 -46
- package/dist/utils/error-handler.d.ts.map +0 -1
- package/dist/utils/error-handler.js +0 -169
- package/dist/utils/error-handler.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -25
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -105
- package/dist/utils/logger.js.map +0 -1
- package/src/commands/computer-setup-commands.ts +0 -872
|
@@ -1,28 +1,818 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Conflict Resolution
|
|
3
|
-
*
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
+
}
|