tlc-claude-code 1.2.26 → 1.2.28

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 (177) hide show
  1. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  2. package/dashboard/dist/components/ActivityFeed.js +42 -0
  3. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  4. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  5. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  6. package/dashboard/dist/components/BranchSelector.js +49 -0
  7. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  8. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  9. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  10. package/dashboard/dist/components/CommandPalette.js +118 -0
  11. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  12. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  13. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  14. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  15. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  17. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  18. package/dashboard/dist/components/DeviceFrame.js +52 -0
  19. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  20. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  21. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  22. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  23. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  25. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  26. package/dashboard/dist/components/FocusIndicator.js +47 -0
  27. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  28. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  29. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  30. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  31. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  33. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  34. package/dashboard/dist/components/LogSearch.js +43 -0
  35. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  36. package/dashboard/dist/components/LogSearch.test.js +100 -0
  37. package/dashboard/dist/components/LogStream.d.ts +21 -0
  38. package/dashboard/dist/components/LogStream.js +123 -0
  39. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  40. package/dashboard/dist/components/LogStream.test.js +159 -0
  41. package/dashboard/dist/components/PlanView.d.ts +7 -0
  42. package/dashboard/dist/components/PlanView.js +74 -2
  43. package/dashboard/dist/components/PlanView.test.js +70 -1
  44. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  45. package/dashboard/dist/components/PreviewPanel.js +73 -0
  46. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  47. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  48. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  49. package/dashboard/dist/components/ProjectCard.js +19 -0
  50. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  51. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  52. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  53. package/dashboard/dist/components/ProjectDetail.js +65 -0
  54. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  55. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  56. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  57. package/dashboard/dist/components/ProjectList.js +62 -0
  58. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  59. package/dashboard/dist/components/ProjectList.test.js +93 -0
  60. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  61. package/dashboard/dist/components/SettingsPanel.js +154 -0
  62. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  63. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  64. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  65. package/dashboard/dist/components/StatusBar.js +47 -0
  66. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  67. package/dashboard/dist/components/StatusBar.test.js +123 -0
  68. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  69. package/dashboard/dist/components/TaskBoard.js +102 -0
  70. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  71. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  72. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  73. package/dashboard/dist/components/TaskCard.js +29 -0
  74. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  75. package/dashboard/dist/components/TaskCard.test.js +109 -0
  76. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  77. package/dashboard/dist/components/TaskDetail.js +41 -0
  78. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  79. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  80. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  81. package/dashboard/dist/components/TaskFilter.js +138 -0
  82. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  83. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  84. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  85. package/dashboard/dist/components/TeamPanel.js +24 -0
  86. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  87. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  88. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  89. package/dashboard/dist/components/TeamPresence.js +31 -0
  90. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  91. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  92. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  93. package/dashboard/dist/components/layout/Header.js +11 -0
  94. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  95. package/dashboard/dist/components/layout/Header.test.js +35 -0
  96. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  97. package/dashboard/dist/components/layout/Shell.js +5 -0
  98. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  99. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  100. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  101. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  102. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  103. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  104. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  105. package/dashboard/dist/components/ui/Badge.js +13 -0
  106. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  107. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  108. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  109. package/dashboard/dist/components/ui/Button.js +14 -0
  110. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  111. package/dashboard/dist/components/ui/Button.test.js +81 -0
  112. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  113. package/dashboard/dist/components/ui/Card.js +20 -0
  114. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  115. package/dashboard/dist/components/ui/Card.test.js +82 -0
  116. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  117. package/dashboard/dist/components/ui/Input.js +8 -0
  118. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  119. package/dashboard/dist/components/ui/Input.test.js +68 -0
  120. package/dashboard/dist/styles/tokens.d.ts +150 -0
  121. package/dashboard/dist/styles/tokens.js +184 -0
  122. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  123. package/dashboard/dist/styles/tokens.test.js +95 -0
  124. package/dashboard/dist/test/setup.d.ts +1 -0
  125. package/dashboard/dist/test/setup.js +1 -0
  126. package/dashboard/package.json +3 -0
  127. package/package.json +1 -1
  128. package/server/dashboard/index.html +157 -2
  129. package/server/index.js +38 -21
  130. package/server/lib/adapters/base-adapter.js +114 -0
  131. package/server/lib/adapters/base-adapter.test.js +90 -0
  132. package/server/lib/adapters/claude-adapter.js +141 -0
  133. package/server/lib/adapters/claude-adapter.test.js +180 -0
  134. package/server/lib/adapters/deepseek-adapter.js +153 -0
  135. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  136. package/server/lib/adapters/openai-adapter.js +190 -0
  137. package/server/lib/adapters/openai-adapter.test.js +231 -0
  138. package/server/lib/budget-tracker.js +169 -0
  139. package/server/lib/budget-tracker.test.js +165 -0
  140. package/server/lib/claude-injector.js +85 -0
  141. package/server/lib/claude-injector.test.js +161 -0
  142. package/server/lib/consensus-engine.js +135 -0
  143. package/server/lib/consensus-engine.test.js +152 -0
  144. package/server/lib/context-builder.js +112 -0
  145. package/server/lib/context-builder.test.js +120 -0
  146. package/server/lib/file-collector.js +322 -0
  147. package/server/lib/file-collector.test.js +307 -0
  148. package/server/lib/memory-classifier.js +175 -0
  149. package/server/lib/memory-classifier.test.js +169 -0
  150. package/server/lib/memory-committer.js +138 -0
  151. package/server/lib/memory-committer.test.js +136 -0
  152. package/server/lib/memory-hooks.js +127 -0
  153. package/server/lib/memory-hooks.test.js +136 -0
  154. package/server/lib/memory-init.js +104 -0
  155. package/server/lib/memory-init.test.js +119 -0
  156. package/server/lib/memory-observer.js +149 -0
  157. package/server/lib/memory-observer.test.js +158 -0
  158. package/server/lib/memory-reader.js +243 -0
  159. package/server/lib/memory-reader.test.js +216 -0
  160. package/server/lib/memory-storage.js +120 -0
  161. package/server/lib/memory-storage.test.js +136 -0
  162. package/server/lib/memory-writer.js +176 -0
  163. package/server/lib/memory-writer.test.js +231 -0
  164. package/server/lib/overdrive-command.js +30 -6
  165. package/server/lib/overdrive-command.test.js +8 -1
  166. package/server/lib/pattern-detector.js +216 -0
  167. package/server/lib/pattern-detector.test.js +241 -0
  168. package/server/lib/relevance-scorer.js +175 -0
  169. package/server/lib/relevance-scorer.test.js +107 -0
  170. package/server/lib/review-command.js +238 -0
  171. package/server/lib/review-command.test.js +245 -0
  172. package/server/lib/review-orchestrator.js +273 -0
  173. package/server/lib/review-orchestrator.test.js +300 -0
  174. package/server/lib/review-reporter.js +288 -0
  175. package/server/lib/review-reporter.test.js +240 -0
  176. package/server/lib/session-summary.js +90 -0
  177. package/server/lib/session-summary.test.js +156 -0
@@ -0,0 +1,300 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { ReviewOrchestrator } from './review-orchestrator.js';
6
+
7
+ // Create mock adapter
8
+ const createMockAdapter = (name, issues = [], options = {}) => ({
9
+ name,
10
+ canAfford: vi.fn(() => options.canAfford !== false),
11
+ getUsage: vi.fn(() => options.usage || { daily: 0, monthly: 0, requests: 0 }),
12
+ review: vi.fn(() => Promise.resolve({
13
+ issues,
14
+ suggestions: options.suggestions || [],
15
+ score: options.score || 80,
16
+ model: name,
17
+ tokensUsed: 100,
18
+ cost: options.cost || 0.01,
19
+ })),
20
+ });
21
+
22
+ describe('ReviewOrchestrator', () => {
23
+ let testDir;
24
+
25
+ beforeEach(() => {
26
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-review-test-'));
27
+ });
28
+
29
+ afterEach(() => {
30
+ fs.rmSync(testDir, { recursive: true, force: true });
31
+ });
32
+
33
+ describe('constructor', () => {
34
+ it('initializes with adapters', () => {
35
+ const adapters = [createMockAdapter('claude')];
36
+ const orchestrator = new ReviewOrchestrator(adapters);
37
+ expect(orchestrator.adapters).toHaveLength(1);
38
+ });
39
+
40
+ it('accepts options', () => {
41
+ const adapters = [createMockAdapter('claude')];
42
+ const orchestrator = new ReviewOrchestrator(adapters, { consensusType: 'unanimous' });
43
+ expect(orchestrator.options.consensusType).toBe('unanimous');
44
+ });
45
+ });
46
+
47
+ describe('reviewFile', () => {
48
+ it('reviews a single file', async () => {
49
+ const filePath = path.join(testDir, 'test.js');
50
+ fs.writeFileSync(filePath, 'const x = 1;');
51
+
52
+ const adapters = [createMockAdapter('claude', [{ id: 'A', severity: 'high', message: 'Issue' }])];
53
+ const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
54
+
55
+ const result = await orchestrator.reviewFile(filePath);
56
+
57
+ expect(result.file).toBe(filePath);
58
+ expect(result.issues).toHaveLength(1);
59
+ });
60
+
61
+ it('returns error for non-existent file', async () => {
62
+ const adapters = [createMockAdapter('claude')];
63
+ const orchestrator = new ReviewOrchestrator(adapters);
64
+
65
+ const result = await orchestrator.reviewFile('/nonexistent/file.js');
66
+
67
+ expect(result.error).toBeTruthy();
68
+ expect(result.issues).toEqual([]);
69
+ });
70
+
71
+ it('skips files over size limit', async () => {
72
+ const filePath = path.join(testDir, 'large.js');
73
+ fs.writeFileSync(filePath, 'x'.repeat(200 * 1024)); // 200KB
74
+
75
+ const adapters = [createMockAdapter('claude')];
76
+ const orchestrator = new ReviewOrchestrator(adapters, { maxFileSizeKB: 100 });
77
+
78
+ const result = await orchestrator.reviewFile(filePath);
79
+
80
+ expect(result.warning).toContain('too large');
81
+ expect(result.issues).toEqual([]);
82
+ });
83
+
84
+ it('tracks costs', async () => {
85
+ const filePath = path.join(testDir, 'test.js');
86
+ fs.writeFileSync(filePath, 'const x = 1;');
87
+
88
+ const adapters = [
89
+ createMockAdapter('claude', [], { cost: 0.01 }),
90
+ createMockAdapter('openai', [], { cost: 0.02 }),
91
+ ];
92
+ const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
93
+
94
+ const result = await orchestrator.reviewFile(filePath);
95
+
96
+ expect(result.costs.total).toBeCloseTo(0.03, 2);
97
+ });
98
+
99
+ it('includes models used', async () => {
100
+ const filePath = path.join(testDir, 'test.js');
101
+ fs.writeFileSync(filePath, 'const x = 1;');
102
+
103
+ const adapters = [
104
+ createMockAdapter('claude'),
105
+ createMockAdapter('openai'),
106
+ ];
107
+ const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
108
+
109
+ const result = await orchestrator.reviewFile(filePath);
110
+
111
+ expect(result.models).toContain('claude');
112
+ expect(result.models).toContain('openai');
113
+ });
114
+ });
115
+
116
+ describe('reviewFiles', () => {
117
+ it('reviews multiple files', async () => {
118
+ fs.writeFileSync(path.join(testDir, 'a.js'), 'code a');
119
+ fs.writeFileSync(path.join(testDir, 'b.js'), 'code b');
120
+
121
+ const adapters = [createMockAdapter('claude', [{ id: 'A', message: 'Issue' }])];
122
+ const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
123
+
124
+ const result = await orchestrator.reviewFiles([
125
+ path.join(testDir, 'a.js'),
126
+ path.join(testDir, 'b.js'),
127
+ ]);
128
+
129
+ expect(result.files).toHaveLength(2);
130
+ expect(result.fileResults).toHaveLength(2);
131
+ });
132
+
133
+ it('aggregates costs across files', async () => {
134
+ fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
135
+ fs.writeFileSync(path.join(testDir, 'b.js'), 'code');
136
+
137
+ const adapters = [createMockAdapter('claude', [], { cost: 0.01 })];
138
+ const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
139
+
140
+ const result = await orchestrator.reviewFiles([
141
+ path.join(testDir, 'a.js'),
142
+ path.join(testDir, 'b.js'),
143
+ ]);
144
+
145
+ expect(result.totalCost).toBeCloseTo(0.02, 2);
146
+ });
147
+
148
+ it('counts total issues', async () => {
149
+ fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
150
+ fs.writeFileSync(path.join(testDir, 'b.js'), 'code');
151
+
152
+ const adapters = [createMockAdapter('claude', [
153
+ { id: 'A', message: 'Issue A' },
154
+ { id: 'B', message: 'Issue B' },
155
+ ])];
156
+ const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
157
+
158
+ const result = await orchestrator.reviewFiles([
159
+ path.join(testDir, 'a.js'),
160
+ path.join(testDir, 'b.js'),
161
+ ]);
162
+
163
+ expect(result.totalIssues).toBe(4); // 2 issues per file
164
+ });
165
+ });
166
+
167
+ describe('reviewDirectory', () => {
168
+ it('reviews all files in directory', async () => {
169
+ fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
170
+ fs.writeFileSync(path.join(testDir, 'b.js'), 'code');
171
+
172
+ const adapters = [createMockAdapter('claude')];
173
+ const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
174
+
175
+ const result = await orchestrator.reviewDirectory(testDir);
176
+
177
+ expect(result.files).toHaveLength(2);
178
+ });
179
+
180
+ it('returns error for invalid directory', async () => {
181
+ const adapters = [createMockAdapter('claude')];
182
+ const orchestrator = new ReviewOrchestrator(adapters);
183
+
184
+ const result = await orchestrator.reviewDirectory('/nonexistent');
185
+
186
+ expect(result.error).toBeTruthy();
187
+ });
188
+
189
+ it('returns warning for empty directory', async () => {
190
+ const adapters = [createMockAdapter('claude')];
191
+ const orchestrator = new ReviewOrchestrator(adapters);
192
+
193
+ const result = await orchestrator.reviewDirectory(testDir);
194
+
195
+ expect(result.warning).toContain('No files');
196
+ });
197
+
198
+ it('respects extension filter', async () => {
199
+ fs.writeFileSync(path.join(testDir, 'a.js'), 'code');
200
+ fs.writeFileSync(path.join(testDir, 'b.ts'), 'code');
201
+ fs.writeFileSync(path.join(testDir, 'c.css'), 'code');
202
+
203
+ const adapters = [createMockAdapter('claude')];
204
+ const orchestrator = new ReviewOrchestrator(adapters, { requireMinimum: 1 });
205
+
206
+ const result = await orchestrator.reviewDirectory(testDir, { extensions: ['.js'] });
207
+
208
+ expect(result.files).toHaveLength(1);
209
+ });
210
+ });
211
+
212
+ describe('summarizeResults', () => {
213
+ it('calculates average confidence', () => {
214
+ const adapters = [createMockAdapter('claude')];
215
+ const orchestrator = new ReviewOrchestrator(adapters);
216
+
217
+ // Provide pre-formed results with known confidence values
218
+ const fileResults = [
219
+ {
220
+ file: 'a.js',
221
+ issues: [
222
+ { id: 'A', message: 'Issue', confidence: 0.8 },
223
+ { id: 'B', message: 'Issue 2', confidence: 0.6 },
224
+ ],
225
+ },
226
+ ];
227
+
228
+ const summary = orchestrator.summarizeResults(fileResults, ['claude'], { byModel: {}, total: 0 });
229
+
230
+ expect(summary.averageConfidence).toBeCloseTo(0.7, 1);
231
+ });
232
+
233
+ it('handles files with no issues', () => {
234
+ const adapters = [createMockAdapter('claude')];
235
+ const orchestrator = new ReviewOrchestrator(adapters);
236
+
237
+ const fileResults = [{ file: 'a.js', issues: [] }];
238
+ const summary = orchestrator.summarizeResults(fileResults, ['claude'], { byModel: {}, total: 0 });
239
+
240
+ expect(summary.averageConfidence).toBe(0);
241
+ });
242
+ });
243
+
244
+ describe('getAvailableModels', () => {
245
+ it('returns all models when budgetAware is false', () => {
246
+ const adapters = [
247
+ createMockAdapter('claude', [], { canAfford: false }),
248
+ createMockAdapter('openai', [], { canAfford: true }),
249
+ ];
250
+ const orchestrator = new ReviewOrchestrator(adapters, { budgetAware: false });
251
+
252
+ const models = orchestrator.getAvailableModels();
253
+ expect(models).toEqual(['claude', 'openai']);
254
+ });
255
+
256
+ it('returns only affordable models when budgetAware is true', () => {
257
+ const adapters = [
258
+ createMockAdapter('claude', [], { canAfford: false }),
259
+ createMockAdapter('openai', [], { canAfford: true }),
260
+ ];
261
+ const orchestrator = new ReviewOrchestrator(adapters, { budgetAware: true });
262
+
263
+ const models = orchestrator.getAvailableModels();
264
+ expect(models).toEqual(['openai']);
265
+ });
266
+ });
267
+
268
+ describe('getUsageSummary', () => {
269
+ it('returns usage for all adapters', () => {
270
+ const adapters = [
271
+ createMockAdapter('claude', [], { usage: { daily: 1, monthly: 10, requests: 5 } }),
272
+ createMockAdapter('openai', [], { usage: { daily: 2, monthly: 20, requests: 10 } }),
273
+ ];
274
+ const orchestrator = new ReviewOrchestrator(adapters);
275
+
276
+ const usage = orchestrator.getUsageSummary();
277
+
278
+ expect(usage.claude.daily).toBe(1);
279
+ expect(usage.openai.daily).toBe(2);
280
+ });
281
+ });
282
+
283
+ describe('aggregateSuggestions', () => {
284
+ it('combines unique suggestions', () => {
285
+ const adapters = [createMockAdapter('claude')];
286
+ const orchestrator = new ReviewOrchestrator(adapters);
287
+
288
+ const reviews = [
289
+ { suggestions: ['A', 'B'] },
290
+ { suggestions: ['B', 'C'] },
291
+ ];
292
+
293
+ const suggestions = orchestrator.aggregateSuggestions(reviews);
294
+ expect(suggestions).toHaveLength(3);
295
+ expect(suggestions).toContain('A');
296
+ expect(suggestions).toContain('B');
297
+ expect(suggestions).toContain('C');
298
+ });
299
+ });
300
+ });
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Review Reporter - Generate review reports in multiple formats
3
+ */
4
+
5
+ /**
6
+ * Generate markdown report from review results
7
+ * @param {Object} results - Review results from orchestrator
8
+ * @returns {string} Markdown report
9
+ */
10
+ function generateMarkdown(results) {
11
+ const lines = [];
12
+
13
+ // Header
14
+ lines.push('# Code Review Report');
15
+ lines.push('');
16
+ lines.push(`**Generated:** ${new Date().toISOString()}`);
17
+ lines.push(`**Files Reviewed:** ${results.files?.length || 0}`);
18
+ lines.push(`**Models Used:** ${results.models?.join(', ') || 'none'}`);
19
+ lines.push('');
20
+
21
+ // Summary
22
+ lines.push('## Summary');
23
+ lines.push('');
24
+ lines.push(`| Metric | Value |`);
25
+ lines.push(`|--------|-------|`);
26
+ lines.push(`| Total Issues | ${results.totalIssues || 0} |`);
27
+ lines.push(`| High Severity | ${countBySeverity(results, 'high')} |`);
28
+ lines.push(`| Medium Severity | ${countBySeverity(results, 'medium')} |`);
29
+ lines.push(`| Low Severity | ${countBySeverity(results, 'low')} |`);
30
+ lines.push(`| Average Confidence | ${formatPercent(results.averageConfidence)} |`);
31
+ lines.push(`| Total Cost | $${formatCost(results.totalCost)} |`);
32
+ lines.push('');
33
+
34
+ // Model Agreement
35
+ if (results.modelAgreement) {
36
+ lines.push('## Model Agreement');
37
+ lines.push('');
38
+ lines.push(`| Issue | Models Agreed | Confidence |`);
39
+ lines.push(`|-------|---------------|------------|`);
40
+ for (const issue of results.consensusIssues || []) {
41
+ const voters = issue.voters?.join(', ') || '';
42
+ lines.push(`| ${escapeMarkdown(issue.message || issue.id)} | ${voters} | ${formatPercent(issue.confidence)} |`);
43
+ }
44
+ lines.push('');
45
+ }
46
+
47
+ // Issues by File
48
+ if (results.fileResults && results.fileResults.length > 0) {
49
+ lines.push('## Issues by File');
50
+ lines.push('');
51
+
52
+ for (const fileResult of results.fileResults) {
53
+ if (fileResult.issues && fileResult.issues.length > 0) {
54
+ lines.push(`### ${fileResult.file}`);
55
+ lines.push('');
56
+ lines.push(`| Line | Severity | Message | Confidence |`);
57
+ lines.push(`|------|----------|---------|------------|`);
58
+ for (const issue of fileResult.issues) {
59
+ lines.push(`| ${issue.line || '-'} | ${issue.severity || 'unknown'} | ${escapeMarkdown(issue.message)} | ${formatPercent(issue.confidence)} |`);
60
+ }
61
+ lines.push('');
62
+ }
63
+ }
64
+ }
65
+
66
+ // Cost Breakdown
67
+ if (results.costs) {
68
+ lines.push('## Cost Breakdown');
69
+ lines.push('');
70
+ lines.push(`| Model | Cost |`);
71
+ lines.push(`|-------|------|`);
72
+ for (const [model, cost] of Object.entries(results.costs.byModel || {})) {
73
+ lines.push(`| ${model} | $${formatCost(cost)} |`);
74
+ }
75
+ lines.push(`| **Total** | **$${formatCost(results.costs.total)}** |`);
76
+ lines.push('');
77
+ }
78
+
79
+ return lines.join('\n');
80
+ }
81
+
82
+ /**
83
+ * Generate JSON report from review results
84
+ * @param {Object} results - Review results from orchestrator
85
+ * @returns {string} JSON report
86
+ */
87
+ function generateJSON(results) {
88
+ const report = {
89
+ meta: {
90
+ generated: new Date().toISOString(),
91
+ filesReviewed: results.files?.length || 0,
92
+ modelsUsed: results.models || [],
93
+ },
94
+ summary: {
95
+ totalIssues: results.totalIssues || 0,
96
+ bySeverity: {
97
+ high: countBySeverity(results, 'high'),
98
+ medium: countBySeverity(results, 'medium'),
99
+ low: countBySeverity(results, 'low'),
100
+ },
101
+ averageConfidence: results.averageConfidence || 0,
102
+ totalCost: results.totalCost || 0,
103
+ },
104
+ consensusIssues: results.consensusIssues || [],
105
+ fileResults: results.fileResults || [],
106
+ costs: results.costs || { byModel: {}, total: 0 },
107
+ };
108
+
109
+ return JSON.stringify(report, null, 2);
110
+ }
111
+
112
+ /**
113
+ * Generate HTML report from review results
114
+ * @param {Object} results - Review results from orchestrator
115
+ * @returns {string} HTML report
116
+ */
117
+ function generateHTML(results) {
118
+ const severityColors = {
119
+ high: '#dc3545',
120
+ medium: '#ffc107',
121
+ low: '#28a745',
122
+ };
123
+
124
+ return `<!DOCTYPE html>
125
+ <html lang="en">
126
+ <head>
127
+ <meta charset="UTF-8">
128
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
129
+ <title>Code Review Report</title>
130
+ <style>
131
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2rem; background: #f5f5f5; }
132
+ .container { max-width: 1200px; margin: 0 auto; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
133
+ h1 { color: #333; border-bottom: 2px solid #007bff; padding-bottom: 0.5rem; }
134
+ h2 { color: #555; margin-top: 2rem; }
135
+ h3 { color: #666; }
136
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
137
+ th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #ddd; }
138
+ th { background: #f8f9fa; font-weight: 600; }
139
+ tr:hover { background: #f8f9fa; }
140
+ .severity-high { color: ${severityColors.high}; font-weight: bold; }
141
+ .severity-medium { color: ${severityColors.medium}; font-weight: bold; }
142
+ .severity-low { color: ${severityColors.low}; font-weight: bold; }
143
+ .confidence { color: #6c757d; }
144
+ .meta { color: #6c757d; font-size: 0.9rem; margin-bottom: 1rem; }
145
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin: 1rem 0; }
146
+ .summary-card { background: #f8f9fa; padding: 1rem; border-radius: 4px; text-align: center; }
147
+ .summary-card .value { font-size: 2rem; font-weight: bold; color: #007bff; }
148
+ .summary-card .label { color: #6c757d; font-size: 0.9rem; }
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <div class="container">
153
+ <h1>Code Review Report</h1>
154
+ <div class="meta">
155
+ <p><strong>Generated:</strong> ${new Date().toISOString()}</p>
156
+ <p><strong>Files Reviewed:</strong> ${results.files?.length || 0}</p>
157
+ <p><strong>Models:</strong> ${results.models?.join(', ') || 'none'}</p>
158
+ </div>
159
+
160
+ <h2>Summary</h2>
161
+ <div class="summary-grid">
162
+ <div class="summary-card">
163
+ <div class="value">${results.totalIssues || 0}</div>
164
+ <div class="label">Total Issues</div>
165
+ </div>
166
+ <div class="summary-card">
167
+ <div class="value" style="color: ${severityColors.high}">${countBySeverity(results, 'high')}</div>
168
+ <div class="label">High Severity</div>
169
+ </div>
170
+ <div class="summary-card">
171
+ <div class="value" style="color: ${severityColors.medium}">${countBySeverity(results, 'medium')}</div>
172
+ <div class="label">Medium Severity</div>
173
+ </div>
174
+ <div class="summary-card">
175
+ <div class="value" style="color: ${severityColors.low}">${countBySeverity(results, 'low')}</div>
176
+ <div class="label">Low Severity</div>
177
+ </div>
178
+ <div class="summary-card">
179
+ <div class="value">${formatPercent(results.averageConfidence)}</div>
180
+ <div class="label">Avg Confidence</div>
181
+ </div>
182
+ <div class="summary-card">
183
+ <div class="value">$${formatCost(results.totalCost)}</div>
184
+ <div class="label">Total Cost</div>
185
+ </div>
186
+ </div>
187
+
188
+ ${results.fileResults && results.fileResults.length > 0 ? `
189
+ <h2>Issues by File</h2>
190
+ ${results.fileResults.map(fileResult => fileResult.issues?.length > 0 ? `
191
+ <h3>${escapeHTML(fileResult.file)}</h3>
192
+ <table>
193
+ <thead>
194
+ <tr><th>Line</th><th>Severity</th><th>Message</th><th>Confidence</th></tr>
195
+ </thead>
196
+ <tbody>
197
+ ${fileResult.issues.map(issue => `
198
+ <tr>
199
+ <td>${issue.line || '-'}</td>
200
+ <td class="severity-${issue.severity || 'low'}">${issue.severity || 'unknown'}</td>
201
+ <td>${escapeHTML(issue.message)}</td>
202
+ <td class="confidence">${formatPercent(issue.confidence)}</td>
203
+ </tr>
204
+ `).join('')}
205
+ </tbody>
206
+ </table>
207
+ ` : '').join('')}
208
+ ` : ''}
209
+
210
+ ${results.costs ? `
211
+ <h2>Cost Breakdown</h2>
212
+ <table>
213
+ <thead>
214
+ <tr><th>Model</th><th>Cost</th></tr>
215
+ </thead>
216
+ <tbody>
217
+ ${Object.entries(results.costs.byModel || {}).map(([model, cost]) => `
218
+ <tr><td>${model}</td><td>$${formatCost(cost)}</td></tr>
219
+ `).join('')}
220
+ <tr style="font-weight: bold;"><td>Total</td><td>$${formatCost(results.costs.total)}</td></tr>
221
+ </tbody>
222
+ </table>
223
+ ` : ''}
224
+ </div>
225
+ </body>
226
+ </html>`;
227
+ }
228
+
229
+ /**
230
+ * Generate report in specified format
231
+ * @param {Object} results - Review results
232
+ * @param {string} format - Output format (md, json, html)
233
+ * @returns {string} Formatted report
234
+ */
235
+ function generateReport(results, format = 'md') {
236
+ switch (format.toLowerCase()) {
237
+ case 'json':
238
+ return generateJSON(results);
239
+ case 'html':
240
+ return generateHTML(results);
241
+ case 'md':
242
+ case 'markdown':
243
+ default:
244
+ return generateMarkdown(results);
245
+ }
246
+ }
247
+
248
+ // Helper functions
249
+ function countBySeverity(results, severity) {
250
+ let count = 0;
251
+ for (const fileResult of results.fileResults || []) {
252
+ for (const issue of fileResult.issues || []) {
253
+ if (issue.severity === severity) count++;
254
+ }
255
+ }
256
+ return count;
257
+ }
258
+
259
+ function formatPercent(value) {
260
+ if (value === undefined || value === null) return '0%';
261
+ return `${Math.round(value * 100)}%`;
262
+ }
263
+
264
+ function formatCost(value) {
265
+ if (value === undefined || value === null) return '0.00';
266
+ return value.toFixed(4);
267
+ }
268
+
269
+ function escapeMarkdown(text) {
270
+ if (!text) return '';
271
+ return text.replace(/\|/g, '\\|').replace(/\n/g, ' ');
272
+ }
273
+
274
+ function escapeHTML(text) {
275
+ if (!text) return '';
276
+ return text
277
+ .replace(/&/g, '&amp;')
278
+ .replace(/</g, '&lt;')
279
+ .replace(/>/g, '&gt;')
280
+ .replace(/"/g, '&quot;');
281
+ }
282
+
283
+ module.exports = {
284
+ generateReport,
285
+ generateMarkdown,
286
+ generateJSON,
287
+ generateHTML,
288
+ };