@zoebuildsai/trace 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.gitignore +115 -0
  2. package/.trace/progress.json +22 -0
  3. package/README.md +466 -0
  4. package/RELEASE-NOTES-1.5.0.md +410 -0
  5. package/STATUS.md +245 -0
  6. package/dist/auto-commit.d.ts +66 -0
  7. package/dist/auto-commit.d.ts.map +1 -0
  8. package/dist/auto-commit.js +180 -0
  9. package/dist/auto-commit.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +246 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands.d.ts +46 -0
  15. package/dist/commands.d.ts.map +1 -0
  16. package/dist/commands.js +256 -0
  17. package/dist/commands.js.map +1 -0
  18. package/dist/diff.d.ts +23 -0
  19. package/dist/diff.d.ts.map +1 -0
  20. package/dist/diff.js +106 -0
  21. package/dist/diff.js.map +1 -0
  22. package/dist/github.d.ts.map +1 -0
  23. package/dist/github.js.map +1 -0
  24. package/dist/index-cache.d.ts +35 -0
  25. package/dist/index-cache.d.ts.map +1 -0
  26. package/dist/index-cache.js +114 -0
  27. package/dist/index-cache.js.map +1 -0
  28. package/dist/index.d.ts +15 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +25 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/storage.d.ts +45 -0
  33. package/dist/storage.d.ts.map +1 -0
  34. package/dist/storage.js +151 -0
  35. package/dist/storage.js.map +1 -0
  36. package/dist/sync.d.ts +60 -0
  37. package/dist/sync.js +184 -0
  38. package/dist/tags.d.ts +85 -0
  39. package/dist/tags.d.ts.map +1 -0
  40. package/dist/tags.js +219 -0
  41. package/dist/tags.js.map +1 -0
  42. package/dist/types.d.ts +102 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +6 -0
  45. package/dist/types.js.map +1 -0
  46. package/docs/.nojekyll +0 -0
  47. package/docs/README.md +73 -0
  48. package/docs/_config.yml +2 -0
  49. package/docs/index.html +960 -0
  50. package/docs-website/package.json +20 -0
  51. package/jest.config.js +21 -0
  52. package/package.json +50 -0
  53. package/scripts/init.ts +290 -0
  54. package/src/agent-audit.ts +270 -0
  55. package/src/agent-checkout.ts +227 -0
  56. package/src/agent-coordination.ts +318 -0
  57. package/src/async-queue.ts +203 -0
  58. package/src/auto-branching.ts +279 -0
  59. package/src/auto-commit.ts +166 -0
  60. package/src/cherry-pick.ts +252 -0
  61. package/src/chunked-upload.ts +224 -0
  62. package/src/cli-v2.ts +335 -0
  63. package/src/cli.ts +318 -0
  64. package/src/cliff-detection.ts +232 -0
  65. package/src/commands.ts +267 -0
  66. package/src/commit-hash-system.ts +351 -0
  67. package/src/compression.ts +176 -0
  68. package/src/conflict-resolution-ui.ts +277 -0
  69. package/src/conflict-visualization.ts +238 -0
  70. package/src/diff-formatter.ts +184 -0
  71. package/src/diff.ts +124 -0
  72. package/src/distributed-coordination.ts +273 -0
  73. package/src/git-interop.ts +316 -0
  74. package/src/index-cache.ts +88 -0
  75. package/src/index.ts +38 -0
  76. package/src/merge-engine.ts +143 -0
  77. package/src/message-search.ts +370 -0
  78. package/src/performance-monitoring.ts +236 -0
  79. package/src/rebase.ts +327 -0
  80. package/src/rollback.ts +215 -0
  81. package/src/semantic-grouping.ts +245 -0
  82. package/src/stage-area.ts +324 -0
  83. package/src/stash.ts +278 -0
  84. package/src/storage.ts +131 -0
  85. package/src/sync.ts +205 -0
  86. package/src/tags.ts +244 -0
  87. package/src/types.ts +119 -0
  88. package/src/webhooks.ts +119 -0
  89. package/src/workspace-isolation.ts +298 -0
  90. package/tests/auto-commit.test.ts +308 -0
  91. package/tests/checkout.test.ts +136 -0
  92. package/tests/commit.test.ts +118 -0
  93. package/tests/diff.test.ts +191 -0
  94. package/tests/github.test.ts +94 -0
  95. package/tests/integration.test.ts +267 -0
  96. package/tests/log.test.ts +125 -0
  97. package/tests/phase2-integration.test.ts +370 -0
  98. package/tests/storage.test.ts +167 -0
  99. package/tests/tags.test.ts +477 -0
  100. package/tests/types.test.ts +75 -0
  101. package/tests/v1.1/agent-audit.test.ts +472 -0
  102. package/tests/v1.1/agent-coordination.test.ts +308 -0
  103. package/tests/v1.1/async-queue.test.ts +253 -0
  104. package/tests/v1.1/comprehensive.test.ts +521 -0
  105. package/tests/v1.1/diff-formatter.test.ts +238 -0
  106. package/tests/v1.1/integration.test.ts +389 -0
  107. package/tests/v1.1/onboarding.test.ts +365 -0
  108. package/tests/v1.1/rollback.test.ts +370 -0
  109. package/tests/v1.1/semantic-grouping.test.ts +230 -0
  110. package/tests/v1.2/chunked-upload.test.ts +301 -0
  111. package/tests/v1.2/cliff-detection.test.ts +272 -0
  112. package/tests/v1.2/commit-hash-system.test.ts +288 -0
  113. package/tests/v1.2/compression.test.ts +220 -0
  114. package/tests/v1.2/conflict-visualization.test.ts +263 -0
  115. package/tests/v1.2/distributed.test.ts +261 -0
  116. package/tests/v1.2/performance-monitoring.test.ts +328 -0
  117. package/tests/v1.3/auto-branching.test.ts +270 -0
  118. package/tests/v1.3/message-search.test.ts +264 -0
  119. package/tests/v1.3/stage-area.test.ts +330 -0
  120. package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
  121. package/tests/v1.4/cli.test.ts +171 -0
  122. package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
  123. package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
  124. package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
  125. package/tests/v1.4/workspace-isolation.test.ts +268 -0
  126. package/tests/v1.5/agent-coordination.real.test.ts +401 -0
  127. package/tests/v1.5/cli-v2.test.ts +354 -0
  128. package/tests/v1.5/git-interop.real.test.ts +358 -0
  129. package/tests/v1.5/integration-testing.real.test.ts +440 -0
  130. package/tsconfig.json +26 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Conflict Resolution UI for Trace
3
+ * Visual & interactive conflict resolution
4
+ */
5
+
6
+ export interface ConflictMarker {
7
+ startLine: number;
8
+ endLine: number;
9
+ oursStart: number;
10
+ oursEnd: number;
11
+ theirsStart: number;
12
+ theirsEnd: number;
13
+ }
14
+
15
+ export interface ConflictResolution {
16
+ file: string;
17
+ resolution: 'ours' | 'theirs' | 'manual' | 'combined';
18
+ marker: ConflictMarker;
19
+ resolvedContent?: string;
20
+ }
21
+
22
+ export class ConflictResolutionUI {
23
+ /**
24
+ * Render conflict in interactive HTML
25
+ */
26
+ static renderHTML(file: string, ours: string, theirs: string, base: string): string {
27
+ const oursLines = ours.split('\n');
28
+ const theirsLines = theirs.split('\n');
29
+ const baseLines = base.split('\n');
30
+
31
+ const html = `
32
+ <!DOCTYPE html>
33
+ <html>
34
+ <head>
35
+ <title>Trace Conflict Resolution: ${file}</title>
36
+ <style>
37
+ * { margin: 0; padding: 0; font-family: monospace; }
38
+ body { background: #f5f5f5; padding: 20px; }
39
+ .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; }
40
+ .header { border-bottom: 2px solid #333; margin-bottom: 20px; padding-bottom: 10px; }
41
+ h1 { font-size: 24px; margin-bottom: 5px; }
42
+ .file-path { color: #666; font-size: 14px; }
43
+
44
+ .conflict-container { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
45
+ .section { border: 1px solid #ddd; border-radius: 4px; overflow: hidden; }
46
+ .section-header { background: #f0f0f0; padding: 10px; font-weight: bold; border-bottom: 1px solid #ddd; }
47
+ .section-header.ours { color: #2196F3; }
48
+ .section-header.theirs { color: #FF9800; }
49
+ .section-header.base { color: #999; }
50
+
51
+ .content { padding: 15px; max-height: 400px; overflow-y: auto; }
52
+ .line { display: flex; }
53
+ .line-num { width: 40px; background: #f9f9f9; padding: 5px; text-align: right; color: #999; border-right: 1px solid #eee; }
54
+ .line-content { flex: 1; padding: 5px 10px; white-space: pre-wrap; }
55
+
56
+ .actions { margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd; display: flex; gap: 10px; }
57
+ button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
58
+ .btn-ours { background: #2196F3; color: white; }
59
+ .btn-theirs { background: #FF9800; color: white; }
60
+ .btn-manual { background: #4CAF50; color: white; }
61
+ .btn-skip { background: #999; color: white; }
62
+
63
+ button:hover { opacity: 0.9; }
64
+
65
+ .stats { background: #f0f0f0; padding: 15px; margin-bottom: 20px; border-radius: 4px; }
66
+ .stat-row { display: flex; justify-content: space-between; margin-bottom: 5px; }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <div class="container">
71
+ <div class="header">
72
+ <h1>🔀 Conflict Resolution</h1>
73
+ <div class="file-path">File: ${file}</div>
74
+ </div>
75
+
76
+ <div class="stats">
77
+ <div class="stat-row">
78
+ <span>Your Version (Ours):</span>
79
+ <strong>${oursLines.length} lines</strong>
80
+ </div>
81
+ <div class="stat-row">
82
+ <span>Incoming Version (Theirs):</span>
83
+ <strong>${theirsLines.length} lines</strong>
84
+ </div>
85
+ <div class="stat-row">
86
+ <span>Base Version:</span>
87
+ <strong>${baseLines.length} lines</strong>
88
+ </div>
89
+ </div>
90
+
91
+ <div class="conflict-container">
92
+ <div class="section">
93
+ <div class="section-header ours">✓ Your Changes (Ours)</div>
94
+ <div class="content">
95
+ ${oursLines.map((line, i) => `<div class="line"><div class="line-num">${i + 1}</div><div class="line-content">${this.escapeHtml(line)}</div></div>`).join('')}
96
+ </div>
97
+ </div>
98
+
99
+ <div class="section">
100
+ <div class="section-header theirs">⚡ Incoming Changes (Theirs)</div>
101
+ <div class="content">
102
+ ${theirsLines.map((line, i) => `<div class="line"><div class="line-num">${i + 1}</div><div class="line-content">${this.escapeHtml(line)}</div></div>`).join('')}
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <div class="section">
108
+ <div class="section-header base">Base Version</div>
109
+ <div class="content">
110
+ ${baseLines.map((line, i) => `<div class="line"><div class="line-num">${i + 1}</div><div class="line-content">${this.escapeHtml(line)}</div></div>`).join('')}
111
+ </div>
112
+ </div>
113
+
114
+ <div class="actions">
115
+ <button class="btn-ours" onclick="resolve('ours')">✓ Use Ours</button>
116
+ <button class="btn-theirs" onclick="resolve('theirs')">⚡ Use Theirs</button>
117
+ <button class="btn-manual" onclick="resolve('manual')">✏️ Manual Merge</button>
118
+ <button class="btn-skip" onclick="resolve('skip')">⏭️ Skip</button>
119
+ </div>
120
+ </div>
121
+
122
+ <script>
123
+ function resolve(choice) {
124
+ console.log('Resolved:', choice);
125
+ // In real implementation, would send back to CLI/agent
126
+ }
127
+ </script>
128
+ </body>
129
+ </html>
130
+ `;
131
+
132
+ return html;
133
+ }
134
+
135
+ /**
136
+ * Render conflict in terminal (TUI)
137
+ */
138
+ static renderTerminal(
139
+ file: string,
140
+ ours: string[],
141
+ theirs: string[],
142
+ base: string[]
143
+ ): string {
144
+ let output = `\n🔀 CONFLICT RESOLUTION: ${file}\n`;
145
+ output += `${'='.repeat(70)}\n\n`;
146
+
147
+ output += `YOURS (Current Branch):\n`;
148
+ output += `${'-'.repeat(70)}\n`;
149
+ ours.slice(0, 10).forEach((line, i) => {
150
+ output += `${(i + 1).toString().padEnd(3)} | ${line}\n`;
151
+ });
152
+ if (ours.length > 10) output += `... +${ours.length - 10} more lines\n`;
153
+
154
+ output += `\nTHEIRS (Incoming Changes):\n`;
155
+ output += `${'-'.repeat(70)}\n`;
156
+ theirs.slice(0, 10).forEach((line, i) => {
157
+ output += `${(i + 1).toString().padEnd(3)} | ${line}\n`;
158
+ });
159
+ if (theirs.length > 10) output += `... +${theirs.length - 10} more lines\n`;
160
+
161
+ output += `\nBASE (Common Ancestor):\n`;
162
+ output += `${'-'.repeat(70)}\n`;
163
+ base.slice(0, 5).forEach((line, i) => {
164
+ output += `${(i + 1).toString().padEnd(3)} | ${line}\n`;
165
+ });
166
+ if (base.length > 5) output += `... +${base.length - 5} more lines\n`;
167
+
168
+ output += `\n${'-'.repeat(70)}\n`;
169
+ output += `RESOLUTION OPTIONS:\n`;
170
+ output += ` [o] Use OURS (current branch changes)\n`;
171
+ output += ` [t] Use THEIRS (incoming changes)\n`;
172
+ output += ` [m] MANUAL (combine both)\n`;
173
+ output += ` [s] SKIP (resolve later)\n`;
174
+ output += `${'-'.repeat(70)}\n`;
175
+
176
+ return output;
177
+ }
178
+
179
+ /**
180
+ * Suggest resolution based on heuristics
181
+ */
182
+ static suggestResolution(
183
+ ours: string,
184
+ theirs: string,
185
+ base: string
186
+ ): { suggestion: string; confidence: number; reason: string } {
187
+ const ourLength = ours.length;
188
+ const theirLength = theirs.length;
189
+ const baseLength = base.length;
190
+
191
+ // Simple heuristic: prefer shorter diffs
192
+ if (Math.abs(ourLength - baseLength) < Math.abs(theirLength - baseLength)) {
193
+ return {
194
+ suggestion: 'ours',
195
+ confidence: 0.7,
196
+ reason: 'Ours diff is smaller (less likely to break)',
197
+ };
198
+ }
199
+
200
+ if (Math.abs(theirLength - baseLength) < Math.abs(ourLength - baseLength)) {
201
+ return {
202
+ suggestion: 'theirs',
203
+ confidence: 0.7,
204
+ reason: 'Theirs diff is smaller (less likely to break)',
205
+ };
206
+ }
207
+
208
+ return {
209
+ suggestion: 'manual',
210
+ confidence: 0.5,
211
+ reason: 'Both diffs are similar - manual review recommended',
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Parse conflict markers
217
+ */
218
+ static parseConflictMarkers(content: string): {
219
+ hasConflicts: boolean;
220
+ markers: ConflictMarker[];
221
+ } {
222
+ const lines = content.split('\n');
223
+ const markers: ConflictMarker[] = [];
224
+ let i = 0;
225
+
226
+ while (i < lines.length) {
227
+ if (lines[i].startsWith('<<<<<<< ')) {
228
+ const startLine = i;
229
+ let oursEnd = i;
230
+ let theirsStart = i;
231
+ let theirsEnd = i;
232
+
233
+ // Find separator
234
+ while (i < lines.length && !lines[i].startsWith('=======')) {
235
+ i++;
236
+ }
237
+ oursEnd = i;
238
+ theirsStart = i + 1;
239
+
240
+ // Find end
241
+ while (i < lines.length && !lines[i].startsWith('>>>>>>>')) {
242
+ i++;
243
+ }
244
+ theirsEnd = i - 1;
245
+
246
+ markers.push({
247
+ startLine,
248
+ endLine: i,
249
+ oursStart: startLine + 1,
250
+ oursEnd,
251
+ theirsStart,
252
+ theirsEnd,
253
+ });
254
+ }
255
+ i++;
256
+ }
257
+
258
+ return {
259
+ hasConflicts: markers.length > 0,
260
+ markers,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Escape HTML special characters
266
+ */
267
+ private static escapeHtml(text: string): string {
268
+ return text
269
+ .replace(/&/g, '&amp;')
270
+ .replace(/</g, '&lt;')
271
+ .replace(/>/g, '&gt;')
272
+ .replace(/"/g, '&quot;')
273
+ .replace(/'/g, '&#039;');
274
+ }
275
+ }
276
+
277
+ export default ConflictResolutionUI;
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Conflict Visualization for Trace
3
+ * Render merge conflicts in human/agent readable format
4
+ */
5
+
6
+ export interface ConflictHunk {
7
+ startLine: number;
8
+ endLine: number;
9
+ ours: string[];
10
+ theirs: string[];
11
+ }
12
+
13
+ export interface ConflictMarker {
14
+ type: 'start' | 'divider' | 'end';
15
+ agentA: string;
16
+ agentB: string;
17
+ line: number;
18
+ }
19
+
20
+ export interface VisualizedConflict {
21
+ file: string;
22
+ format: 'ascii' | 'json' | 'html';
23
+ content: string;
24
+ hunks: ConflictHunk[];
25
+ }
26
+
27
+ export class ConflictVisualization {
28
+ /**
29
+ * Render conflict as ASCII (human-readable)
30
+ */
31
+ static toASCII(file: string, hunks: ConflictHunk[], agentA: string, agentB: string): string {
32
+ const lines: string[] = [];
33
+
34
+ lines.push(`╔═══════════════════════════════════════════╗`);
35
+ lines.push(`║ CONFLICT: ${file}`);
36
+ lines.push(`║ ${agentA} vs ${agentB}`);
37
+ lines.push(`╚═══════════════════════════════════════════╝\n`);
38
+
39
+ for (let i = 0; i < hunks.length; i++) {
40
+ const hunk = hunks[i];
41
+ lines.push(`┌─ Conflict ${i + 1} (lines ${hunk.startLine}-${hunk.endLine}) ─┐\n`);
42
+
43
+ lines.push(`╭─ ${agentA} ─────────────────────────`);
44
+ for (const line of hunk.ours) {
45
+ lines.push(`│ ${line}`);
46
+ }
47
+ lines.push(`╰───────────────────────────────────────`);
48
+
49
+ lines.push(``);
50
+
51
+ lines.push(`╭─ ${agentB} ─────────────────────────`);
52
+ for (const line of hunk.theirs) {
53
+ lines.push(`│ ${line}`);
54
+ }
55
+ lines.push(`╰───────────────────────────────────────`);
56
+
57
+ lines.push(`\n`);
58
+ }
59
+
60
+ return lines.join('\n');
61
+ }
62
+
63
+ /**
64
+ * Render conflict as JSON (machine-readable for agents)
65
+ */
66
+ static toJSON(file: string, hunks: ConflictHunk[], agentA: string, agentB: string): string {
67
+ const conflict = {
68
+ file,
69
+ agentA,
70
+ agentB,
71
+ timestamp: new Date().toISOString(),
72
+ hunks: hunks.map((h, i) => ({
73
+ index: i,
74
+ startLine: h.startLine,
75
+ endLine: h.endLine,
76
+ ours: h.ours,
77
+ theirs: h.theirs,
78
+ oursCount: h.ours.length,
79
+ theirsCount: h.theirs.length,
80
+ })),
81
+ totalHunks: hunks.length,
82
+ };
83
+
84
+ return JSON.stringify(conflict, null, 2);
85
+ }
86
+
87
+ /**
88
+ * Render conflict as HTML (visual UI)
89
+ */
90
+ static toHTML(file: string, hunks: ConflictHunk[], agentA: string, agentB: string): string {
91
+ let html = `
92
+ <html>
93
+ <head>
94
+ <style>
95
+ body { font-family: monospace; background: #f5f5f5; }
96
+ .conflict { background: #fff; border: 1px solid #ccc; margin: 10px; padding: 10px; }
97
+ .file-header { font-weight: bold; color: #d00; margin-bottom: 10px; }
98
+ .hunk { margin: 10px 0; border: 1px solid #ddd; }
99
+ .agent-a { background: #ffe0e0; border-left: 3px solid #f00; padding: 5px; }
100
+ .agent-b { background: #e0f0ff; border-left: 3px solid #00f; padding: 5px; }
101
+ .line { margin: 2px 0; }
102
+ </style>
103
+ </head>
104
+ <body>
105
+ <div class="conflict">
106
+ <div class="file-header">CONFLICT: ${file}</div>
107
+ <div>Agent: ${agentA} vs ${agentB}</div>
108
+ <div style="margin-top: 10px;">`;
109
+
110
+ for (let i = 0; i < hunks.length; i++) {
111
+ const hunk = hunks[i];
112
+ html += `
113
+ <div class="hunk">
114
+ <div style="font-weight: bold; margin-bottom: 5px;">Conflict ${i + 1} (lines ${hunk.startLine}-${hunk.endLine})</div>
115
+ <div class="agent-a">
116
+ <strong>${agentA}:</strong>
117
+ ${hunk.ours.map(line => `<div class="line">${line}</div>`).join('')}
118
+ </div>
119
+ <div class="agent-b" style="margin-top: 5px;">
120
+ <strong>${agentB}:</strong>
121
+ ${hunk.theirs.map(line => `<div class="line">${line}</div>`).join('')}
122
+ </div>
123
+ </div>`;
124
+ }
125
+
126
+ html += `
127
+ </div>
128
+ </div>
129
+ </body>
130
+ </html>`;
131
+
132
+ return html;
133
+ }
134
+
135
+ /**
136
+ * Render with standard git conflict markers
137
+ */
138
+ static toGitFormat(hunks: ConflictHunk[], agentA: string, agentB: string): string {
139
+ const lines: string[] = [];
140
+
141
+ for (const hunk of hunks) {
142
+ lines.push(`<<<<<<< ${agentA}`);
143
+ lines.push(...hunk.ours);
144
+ lines.push(`=======`);
145
+ lines.push(...hunk.theirs);
146
+ lines.push(`>>>>>>> ${agentB}`);
147
+ lines.push('');
148
+ }
149
+
150
+ return lines.join('\n');
151
+ }
152
+
153
+ /**
154
+ * Parse git conflict markers
155
+ */
156
+ static parseGitMarkers(content: string, agentA: string, agentB: string): ConflictHunk[] {
157
+ const hunks: ConflictHunk[] = [];
158
+ const lines = content.split('\n');
159
+
160
+ let i = 0;
161
+ while (i < lines.length) {
162
+ if (lines[i].startsWith('<<<<<<< ')) {
163
+ const startLine = i;
164
+ const ours: string[] = [];
165
+ let theirsStart = -1;
166
+
167
+ i++;
168
+ while (i < lines.length && !lines[i].startsWith('=======')) {
169
+ ours.push(lines[i]);
170
+ i++;
171
+ }
172
+
173
+ if (i < lines.length && lines[i].startsWith('=======')) {
174
+ theirsStart = i;
175
+ i++;
176
+ }
177
+
178
+ const theirs: string[] = [];
179
+ while (i < lines.length && !lines[i].startsWith('>>>>>>> ')) {
180
+ theirs.push(lines[i]);
181
+ i++;
182
+ }
183
+
184
+ if (i < lines.length && lines[i].startsWith('>>>>>>> ')) {
185
+ hunks.push({
186
+ startLine,
187
+ endLine: i,
188
+ ours,
189
+ theirs,
190
+ });
191
+ i++;
192
+ }
193
+ } else {
194
+ i++;
195
+ }
196
+ }
197
+
198
+ return hunks;
199
+ }
200
+
201
+ /**
202
+ * Suggest resolution (side with fewer changes or shorter diff)
203
+ */
204
+ static suggestResolution(hunks: ConflictHunk[], agentA: string, agentB: string): string {
205
+ let totalOurs = 0;
206
+ let totalTheirs = 0;
207
+
208
+ for (const hunk of hunks) {
209
+ totalOurs += hunk.ours.join('').length;
210
+ totalTheirs += hunk.theirs.join('').length;
211
+ }
212
+
213
+ return totalOurs <= totalTheirs ? agentA : agentB;
214
+ }
215
+
216
+ /**
217
+ * Calculate conflict statistics
218
+ */
219
+ static getStats(hunks: ConflictHunk[]): {
220
+ totalHunks: number;
221
+ totalLinesInConflict: number;
222
+ avgHunkSize: number;
223
+ } {
224
+ let totalLines = 0;
225
+
226
+ for (const hunk of hunks) {
227
+ totalLines += hunk.ours.length + hunk.theirs.length;
228
+ }
229
+
230
+ return {
231
+ totalHunks: hunks.length,
232
+ totalLinesInConflict: totalLines,
233
+ avgHunkSize: hunks.length > 0 ? totalLines / hunks.length : 0,
234
+ };
235
+ }
236
+ }
237
+
238
+ export default ConflictVisualization;
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Diff Formatter for Trace
3
+ * Outputs diffs in multiple formats: JSON (for agents), ASCII (for humans), Timeline
4
+ */
5
+
6
+ export interface DiffEntry {
7
+ path: string;
8
+ type: 'add' | 'modify' | 'delete';
9
+ lines?: { added: number; removed: number; changed: number };
10
+ size?: { before: number; after: number };
11
+ }
12
+
13
+ export interface DiffOutput {
14
+ format: 'json' | 'ascii' | 'timeline';
15
+ files: DiffEntry[];
16
+ summary: {
17
+ total: number;
18
+ added: number;
19
+ modified: number;
20
+ deleted: number;
21
+ totalLinesChanged: number;
22
+ };
23
+ }
24
+
25
+ export class DiffFormatter {
26
+ /**
27
+ * Parse git diff output into structured format
28
+ */
29
+ static parseGitDiff(gitOutput: string): DiffEntry[] {
30
+ const lines = gitOutput.split('\n');
31
+ const entries: DiffEntry[] = [];
32
+ let current: DiffEntry | null = null;
33
+
34
+ for (const line of lines) {
35
+ if (line.startsWith('diff --git')) {
36
+ // Extract path from: diff --git a/path b/path
37
+ const match = line.match(/b\/(.+)$/);
38
+ if (match) {
39
+ if (current) entries.push(current);
40
+ current = {
41
+ path: match[1],
42
+ type: 'modify',
43
+ lines: { added: 0, removed: 0, changed: 0 },
44
+ };
45
+ }
46
+ } else if (line.startsWith('new file')) {
47
+ if (current) current.type = 'add';
48
+ } else if (line.startsWith('deleted file')) {
49
+ if (current) current.type = 'delete';
50
+ } else if (line.startsWith('@@')) {
51
+ // Parse hunk header: @@ -old,count +new,count @@
52
+ const match = line.match(/@@ -(\d+),?(\d+)? \+(\d+),?(\d+)? @@/);
53
+ if (match && current) {
54
+ const oldCount = parseInt(match[2] || '1', 10);
55
+ const newCount = parseInt(match[4] || '1', 10);
56
+ current.lines!.added += Math.max(0, newCount - oldCount);
57
+ current.lines!.removed += Math.max(0, oldCount - newCount);
58
+ current.lines!.changed++;
59
+ }
60
+ }
61
+ }
62
+
63
+ if (current) entries.push(current);
64
+ return entries;
65
+ }
66
+
67
+ /**
68
+ * Format diff as JSON for agents
69
+ */
70
+ static toJSON(entries: DiffEntry[]): DiffOutput {
71
+ const summary = {
72
+ total: entries.length,
73
+ added: entries.filter(e => e.type === 'add').length,
74
+ modified: entries.filter(e => e.type === 'modify').length,
75
+ deleted: entries.filter(e => e.type === 'delete').length,
76
+ totalLinesChanged: entries.reduce((sum, e) => sum + (e.lines?.changed || 0), 0),
77
+ };
78
+
79
+ return {
80
+ format: 'json',
81
+ files: entries,
82
+ summary,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Format diff as ASCII for humans
88
+ */
89
+ static toASCII(entries: DiffEntry[]): string {
90
+ const lines: string[] = [];
91
+
92
+ lines.push('═'.repeat(60));
93
+ lines.push('CHANGES');
94
+ lines.push('═'.repeat(60));
95
+
96
+ for (const entry of entries) {
97
+ const icon = entry.type === 'add' ? '✨' : entry.type === 'delete' ? '🗑️' : '✏️';
98
+ lines.push(`${icon} ${entry.type.toUpperCase()}: ${entry.path}`);
99
+
100
+ if (entry.lines) {
101
+ const stats = [];
102
+ if (entry.lines.added > 0) stats.push(`+${entry.lines.added}`);
103
+ if (entry.lines.removed > 0) stats.push(`-${entry.lines.removed}`);
104
+ if (stats.length > 0) {
105
+ lines.push(` ${stats.join(' ')}`);
106
+ }
107
+ }
108
+ }
109
+
110
+ lines.push('═'.repeat(60));
111
+ lines.push(`Total: ${entries.length} files changed`);
112
+ lines.push('═'.repeat(60));
113
+
114
+ return lines.join('\n');
115
+ }
116
+
117
+ /**
118
+ * Format diff as timeline (ASCII art)
119
+ */
120
+ static toTimeline(entries: DiffEntry[]): string {
121
+ const lines: string[] = [];
122
+
123
+ lines.push('TIMELINE VIEW');
124
+ lines.push('─'.repeat(50));
125
+
126
+ let timeIndex = 0;
127
+ for (const entry of entries) {
128
+ const icon = entry.type === 'add' ? '●' : entry.type === 'delete' ? '◯' : '◐';
129
+ const time = String(timeIndex++).padStart(2, ' ');
130
+
131
+ lines.push(`${icon} [${time}] ${entry.path}`);
132
+ if (entry.lines?.changed) {
133
+ lines.push(` └─ ${entry.lines.changed} change(s)`);
134
+ }
135
+ }
136
+
137
+ lines.push('─'.repeat(50));
138
+ return lines.join('\n');
139
+ }
140
+
141
+ /**
142
+ * Auto-select format based on context
143
+ */
144
+ static format(entries: DiffEntry[], format?: 'json' | 'ascii' | 'timeline'): string | DiffOutput {
145
+ const selectedFormat = format || 'ascii'; // Default to human-readable
146
+
147
+ switch (selectedFormat) {
148
+ case 'json':
149
+ return this.toJSON(entries);
150
+ case 'ascii':
151
+ return this.toASCII(entries);
152
+ case 'timeline':
153
+ return this.toTimeline(entries);
154
+ default:
155
+ return this.toASCII(entries);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Sanitize diff output (remove sensitive paths)
161
+ */
162
+ static sanitize(entries: DiffEntry[]): DiffEntry[] {
163
+ const sensitivePatterns = [
164
+ /\.env/,
165
+ /\.key$/,
166
+ /secret/i,
167
+ /password/i,
168
+ /token/i,
169
+ /private/i,
170
+ /config\/.*key/,
171
+ ];
172
+
173
+ return entries.filter(entry => {
174
+ // Don't filter out sensitive files, just flag them
175
+ const isSensitive = sensitivePatterns.some(pattern => pattern.test(entry.path));
176
+ if (isSensitive) {
177
+ console.warn(`⚠️ Sensitive file detected: ${entry.path}`);
178
+ }
179
+ return true;
180
+ });
181
+ }
182
+ }
183
+
184
+ export default DiffFormatter;