@boshu2/vibe-check 1.5.0 → 1.6.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 (147) hide show
  1. package/.agents/bundles/insight-mining-dashboard-research-2025-11-30.md +400 -0
  2. package/.agents/bundles/storage-enhancement-research-2025-11-30.md +292 -0
  3. package/.agents/bundles/timeline-feature-research-complete-2025-11-30.md +301 -0
  4. package/.agents/plans/insight-dashboard-plan-2025-11-30.md +1130 -0
  5. package/.agents/plans/json-storage-enhancement-plan.md +717 -0
  6. package/.agents/plans/storage-hardening-and-cache-plan.md +592 -0
  7. package/.agents/plans/test-coverage-gaps-plan.md +1117 -0
  8. package/.agents/plans/timeline-feature-plan.md +193 -0
  9. package/.agents/plans/vibe_timeline_research_findings.md +553 -0
  10. package/.claude/settings.local.json +1 -0
  11. package/.vibe-check/.gitignore +6 -0
  12. package/CHANGELOG.md +46 -0
  13. package/CLAUDE.md +24 -0
  14. package/CONTRIBUTING.md +227 -0
  15. package/README.md +200 -143
  16. package/claude-progress.json +191 -9
  17. package/claude-progress.txt +257 -0
  18. package/dashboard/app.js +75 -2
  19. package/dashboard/dashboard-data.json +653 -0
  20. package/dashboard/index.html +13 -0
  21. package/dashboard/styles.css +61 -0
  22. package/dist/analysis/cross-session-analysis.d.ts +68 -0
  23. package/dist/analysis/cross-session-analysis.d.ts.map +1 -0
  24. package/dist/analysis/cross-session-analysis.js +174 -0
  25. package/dist/analysis/cross-session-analysis.js.map +1 -0
  26. package/dist/analysis/index.d.ts +2 -0
  27. package/dist/analysis/index.d.ts.map +1 -0
  28. package/dist/analysis/index.js +12 -0
  29. package/dist/analysis/index.js.map +1 -0
  30. package/dist/cli.js +10 -1
  31. package/dist/cli.js.map +1 -1
  32. package/dist/commands/analyze.d.ts +2 -0
  33. package/dist/commands/analyze.d.ts.map +1 -1
  34. package/dist/commands/analyze.js +105 -2
  35. package/dist/commands/analyze.js.map +1 -1
  36. package/dist/commands/cache.d.ts +6 -0
  37. package/dist/commands/cache.d.ts.map +1 -0
  38. package/dist/commands/cache.js +168 -0
  39. package/dist/commands/cache.js.map +1 -0
  40. package/dist/commands/dashboard.d.ts +8 -0
  41. package/dist/commands/dashboard.d.ts.map +1 -0
  42. package/dist/commands/dashboard.js +109 -0
  43. package/dist/commands/dashboard.js.map +1 -0
  44. package/dist/commands/index.d.ts +3 -0
  45. package/dist/commands/index.d.ts.map +1 -1
  46. package/dist/commands/index.js +8 -1
  47. package/dist/commands/index.js.map +1 -1
  48. package/dist/commands/timeline.d.ts +14 -0
  49. package/dist/commands/timeline.d.ts.map +1 -0
  50. package/dist/commands/timeline.js +462 -0
  51. package/dist/commands/timeline.js.map +1 -0
  52. package/dist/git.d.ts +24 -0
  53. package/dist/git.d.ts.map +1 -1
  54. package/dist/git.js +94 -0
  55. package/dist/git.js.map +1 -1
  56. package/dist/insights/generators.d.ts +44 -0
  57. package/dist/insights/generators.d.ts.map +1 -0
  58. package/dist/insights/generators.js +289 -0
  59. package/dist/insights/generators.js.map +1 -0
  60. package/dist/insights/index.d.ts +16 -0
  61. package/dist/insights/index.d.ts.map +1 -0
  62. package/dist/insights/index.js +171 -0
  63. package/dist/insights/index.js.map +1 -0
  64. package/dist/insights/types.d.ts +93 -0
  65. package/dist/insights/types.d.ts.map +1 -0
  66. package/dist/insights/types.js +6 -0
  67. package/dist/insights/types.js.map +1 -0
  68. package/dist/output/timeline-html.d.ts +6 -0
  69. package/dist/output/timeline-html.d.ts.map +1 -0
  70. package/dist/output/timeline-html.js +389 -0
  71. package/dist/output/timeline-html.js.map +1 -0
  72. package/dist/output/timeline-markdown.d.ts +6 -0
  73. package/dist/output/timeline-markdown.d.ts.map +1 -0
  74. package/dist/output/timeline-markdown.js +167 -0
  75. package/dist/output/timeline-markdown.js.map +1 -0
  76. package/dist/output/timeline.d.ts +9 -0
  77. package/dist/output/timeline.d.ts.map +1 -0
  78. package/dist/output/timeline.js +318 -0
  79. package/dist/output/timeline.js.map +1 -0
  80. package/dist/patterns/detour.d.ts +32 -0
  81. package/dist/patterns/detour.d.ts.map +1 -0
  82. package/dist/patterns/detour.js +137 -0
  83. package/dist/patterns/detour.js.map +1 -0
  84. package/dist/patterns/flow-state.d.ts +16 -0
  85. package/dist/patterns/flow-state.d.ts.map +1 -0
  86. package/dist/patterns/flow-state.js +40 -0
  87. package/dist/patterns/flow-state.js.map +1 -0
  88. package/dist/patterns/index.d.ts +8 -0
  89. package/dist/patterns/index.d.ts.map +1 -0
  90. package/dist/patterns/index.js +22 -0
  91. package/dist/patterns/index.js.map +1 -0
  92. package/dist/patterns/intervention-effectiveness.d.ts +42 -0
  93. package/dist/patterns/intervention-effectiveness.d.ts.map +1 -0
  94. package/dist/patterns/intervention-effectiveness.js +196 -0
  95. package/dist/patterns/intervention-effectiveness.js.map +1 -0
  96. package/dist/patterns/late-night.d.ts +30 -0
  97. package/dist/patterns/late-night.d.ts.map +1 -0
  98. package/dist/patterns/late-night.js +141 -0
  99. package/dist/patterns/late-night.js.map +1 -0
  100. package/dist/patterns/post-delete-sprint.d.ts +28 -0
  101. package/dist/patterns/post-delete-sprint.d.ts.map +1 -0
  102. package/dist/patterns/post-delete-sprint.js +85 -0
  103. package/dist/patterns/post-delete-sprint.js.map +1 -0
  104. package/dist/patterns/spiral-regression.d.ts +49 -0
  105. package/dist/patterns/spiral-regression.d.ts.map +1 -0
  106. package/dist/patterns/spiral-regression.js +219 -0
  107. package/dist/patterns/spiral-regression.js.map +1 -0
  108. package/dist/patterns/thrashing.d.ts +25 -0
  109. package/dist/patterns/thrashing.d.ts.map +1 -0
  110. package/dist/patterns/thrashing.js +111 -0
  111. package/dist/patterns/thrashing.js.map +1 -0
  112. package/dist/storage/atomic.d.ts +40 -0
  113. package/dist/storage/atomic.d.ts.map +1 -0
  114. package/dist/storage/atomic.js +155 -0
  115. package/dist/storage/atomic.js.map +1 -0
  116. package/dist/storage/commit-log.d.ts +35 -0
  117. package/dist/storage/commit-log.d.ts.map +1 -0
  118. package/dist/storage/commit-log.js +128 -0
  119. package/dist/storage/commit-log.js.map +1 -0
  120. package/dist/storage/index.d.ts +5 -0
  121. package/dist/storage/index.d.ts.map +1 -0
  122. package/dist/storage/index.js +33 -0
  123. package/dist/storage/index.js.map +1 -0
  124. package/dist/storage/schema.d.ts +32 -0
  125. package/dist/storage/schema.d.ts.map +1 -0
  126. package/dist/storage/schema.js +37 -0
  127. package/dist/storage/schema.js.map +1 -0
  128. package/dist/storage/timeline-store.d.ts +117 -0
  129. package/dist/storage/timeline-store.d.ts.map +1 -0
  130. package/dist/storage/timeline-store.js +438 -0
  131. package/dist/storage/timeline-store.js.map +1 -0
  132. package/dist/types.d.ts +96 -0
  133. package/dist/types.d.ts.map +1 -1
  134. package/docs/ARCHITECTURE.md +458 -0
  135. package/docs/DATA-ARCHITECTURE.md +565 -0
  136. package/docs/GAMIFICATION.md +564 -0
  137. package/docs/JSON-STORAGE-PATTERNS.md +512 -0
  138. package/docs/METRICS-EXPLAINED.md +394 -0
  139. package/docs/UNIFIED-ECOSYSTEM.md +560 -0
  140. package/docs/VIBE-ECOSYSTEM.md +406 -0
  141. package/feature-list.json +48 -0
  142. package/package.json +2 -1
  143. package/vitest.config.ts +1 -5
  144. package/.vibe-check/calibration.json +0 -38
  145. package/.vibe-check/latest.json +0 -114
  146. package/.vibe-check/sessions.json +0 -44
  147. package/PLAN-ultimate-game.md +0 -1362
@@ -0,0 +1,1117 @@
1
+ # Test Coverage Gaps Implementation Plan
2
+
3
+ **Type:** Plan
4
+ **Created:** 2025-11-30
5
+ **Loop:** Middle (bridges research to implementation)
6
+ **Tags:** testing, coverage, vitest
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ Fill remaining test coverage gaps identified in code review. Current coverage: 36.28%. Target: >60% statement coverage on critical modules.
13
+
14
+ ## Priority Modules (0% Coverage Currently)
15
+
16
+ | Module | Lines | Complexity | Priority |
17
+ |--------|-------|------------|----------|
18
+ | `metrics/file-churn.ts` | 80 | Medium | HIGH |
19
+ | `metrics/time-spiral.ts` | 69 | Low | HIGH |
20
+ | `metrics/velocity-anomaly.ts` | 75 | Medium | HIGH |
21
+ | `metrics/code-stability.ts` | 83 | Medium | HIGH |
22
+ | `output/json.ts` | 100 | Low | MEDIUM |
23
+ | `output/markdown.ts` | 146 | Low | MEDIUM |
24
+
25
+ ---
26
+
27
+ ## Files to Create
28
+
29
+ ### 1. `tests/metrics/file-churn.test.ts`
30
+
31
+ **Purpose:** Test file churn calculation - files touched 3+ times in 1 hour
32
+
33
+ ```typescript
34
+ import { describe, it, expect } from 'vitest';
35
+ import { calculateFileChurn } from '../../src/metrics/file-churn';
36
+ import { Commit } from '../../src/types';
37
+
38
+ describe('metrics/file-churn', () => {
39
+ const mockCommit = (hash: string, date: Date): Commit => ({
40
+ hash,
41
+ date,
42
+ message: 'test',
43
+ type: 'feat',
44
+ scope: null,
45
+ author: 'test',
46
+ });
47
+
48
+ describe('calculateFileChurn', () => {
49
+ it('returns 100% for empty commits', () => {
50
+ const result = calculateFileChurn([], new Map());
51
+ expect(result.value).toBe(100);
52
+ expect(result.churnedFiles).toBe(0);
53
+ expect(result.totalFiles).toBe(0);
54
+ });
55
+
56
+ it('returns 100% when no files are churned', () => {
57
+ const commits = [
58
+ mockCommit('abc', new Date('2025-11-28T10:00:00Z')),
59
+ mockCommit('def', new Date('2025-11-28T11:00:00Z')),
60
+ ];
61
+ const filesPerCommit = new Map([
62
+ ['abc', ['file1.ts']],
63
+ ['def', ['file2.ts']],
64
+ ]);
65
+
66
+ const result = calculateFileChurn(commits, filesPerCommit);
67
+
68
+ expect(result.value).toBe(100);
69
+ expect(result.rating).toBe('elite');
70
+ expect(result.churnedFiles).toBe(0);
71
+ expect(result.totalFiles).toBe(2);
72
+ });
73
+
74
+ it('detects churn when file touched 3+ times in 1 hour', () => {
75
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
76
+ const commits = [
77
+ mockCommit('a', new Date(baseTime)),
78
+ mockCommit('b', new Date(baseTime + 10 * 60 * 1000)), // +10 min
79
+ mockCommit('c', new Date(baseTime + 20 * 60 * 1000)), // +20 min
80
+ ];
81
+ const filesPerCommit = new Map([
82
+ ['a', ['file1.ts']],
83
+ ['b', ['file1.ts']],
84
+ ['c', ['file1.ts']],
85
+ ]);
86
+
87
+ const result = calculateFileChurn(commits, filesPerCommit);
88
+
89
+ expect(result.churnedFiles).toBe(1);
90
+ expect(result.totalFiles).toBe(1);
91
+ expect(result.value).toBe(0); // 100% churn = 0 score
92
+ expect(result.rating).toBe('low');
93
+ });
94
+
95
+ it('does not detect churn if touches span more than 1 hour', () => {
96
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
97
+ const commits = [
98
+ mockCommit('a', new Date(baseTime)),
99
+ mockCommit('b', new Date(baseTime + 40 * 60 * 1000)), // +40 min
100
+ mockCommit('c', new Date(baseTime + 90 * 60 * 1000)), // +90 min (>1hr span)
101
+ ];
102
+ const filesPerCommit = new Map([
103
+ ['a', ['file1.ts']],
104
+ ['b', ['file1.ts']],
105
+ ['c', ['file1.ts']],
106
+ ]);
107
+
108
+ const result = calculateFileChurn(commits, filesPerCommit);
109
+
110
+ expect(result.churnedFiles).toBe(0);
111
+ expect(result.rating).toBe('elite');
112
+ });
113
+
114
+ it('calculates correct rating thresholds', () => {
115
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
116
+
117
+ // Create 10 files, 2 churned (20% churn ratio)
118
+ const commits: Commit[] = [];
119
+ const filesPerCommit = new Map<string, string[]>();
120
+
121
+ // 2 churned files
122
+ for (let i = 0; i < 3; i++) {
123
+ const hash = `churn1-${i}`;
124
+ commits.push(mockCommit(hash, new Date(baseTime + i * 5 * 60 * 1000)));
125
+ filesPerCommit.set(hash, ['churnedFile1.ts']);
126
+ }
127
+ for (let i = 0; i < 3; i++) {
128
+ const hash = `churn2-${i}`;
129
+ commits.push(mockCommit(hash, new Date(baseTime + i * 5 * 60 * 1000)));
130
+ filesPerCommit.set(hash, ['churnedFile2.ts']);
131
+ }
132
+
133
+ // 8 non-churned files
134
+ for (let i = 0; i < 8; i++) {
135
+ const hash = `single-${i}`;
136
+ commits.push(mockCommit(hash, new Date(baseTime + i * 60 * 60 * 1000)));
137
+ filesPerCommit.set(hash, [`file${i}.ts`]);
138
+ }
139
+
140
+ const result = calculateFileChurn(commits, filesPerCommit);
141
+
142
+ expect(result.churnedFiles).toBe(2);
143
+ expect(result.totalFiles).toBe(10);
144
+ expect(result.rating).toBe('high'); // 20% is in 10-25% range
145
+ });
146
+
147
+ it('handles missing files in map gracefully', () => {
148
+ const commits = [
149
+ mockCommit('abc', new Date('2025-11-28T10:00:00Z')),
150
+ ];
151
+ const filesPerCommit = new Map<string, string[]>(); // Empty map
152
+
153
+ const result = calculateFileChurn(commits, filesPerCommit);
154
+
155
+ expect(result.value).toBe(100);
156
+ expect(result.totalFiles).toBe(0);
157
+ });
158
+
159
+ it('includes correct description for each rating', () => {
160
+ const elite = calculateFileChurn([], new Map());
161
+ expect(elite.description).toContain('Elite');
162
+
163
+ // For other ratings, would need to construct appropriate data
164
+ });
165
+ });
166
+ });
167
+ ```
168
+
169
+ **Validation:** `npm test -- tests/metrics/file-churn.test.ts`
170
+
171
+ ---
172
+
173
+ ### 2. `tests/metrics/time-spiral.test.ts`
174
+
175
+ **Purpose:** Test time spiral detection - commits < 5 min apart
176
+
177
+ ```typescript
178
+ import { describe, it, expect } from 'vitest';
179
+ import { calculateTimeSpiral } from '../../src/metrics/time-spiral';
180
+ import { Commit } from '../../src/types';
181
+
182
+ describe('metrics/time-spiral', () => {
183
+ const mockCommit = (date: Date): Commit => ({
184
+ hash: Math.random().toString(36).substring(7),
185
+ date,
186
+ message: 'test',
187
+ type: 'feat',
188
+ scope: null,
189
+ author: 'test',
190
+ });
191
+
192
+ describe('calculateTimeSpiral', () => {
193
+ it('returns elite for empty commits', () => {
194
+ const result = calculateTimeSpiral([]);
195
+
196
+ expect(result.value).toBe(100);
197
+ expect(result.rating).toBe('elite');
198
+ expect(result.spiralCommits).toBe(0);
199
+ expect(result.totalCommits).toBe(0);
200
+ });
201
+
202
+ it('returns elite for single commit', () => {
203
+ const result = calculateTimeSpiral([
204
+ mockCommit(new Date('2025-11-28T10:00:00Z')),
205
+ ]);
206
+
207
+ expect(result.value).toBe(100);
208
+ expect(result.rating).toBe('elite');
209
+ expect(result.description).toContain('Insufficient');
210
+ });
211
+
212
+ it('detects spiral when commits < 5 min apart', () => {
213
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
214
+ const commits = [
215
+ mockCommit(new Date(baseTime)),
216
+ mockCommit(new Date(baseTime + 2 * 60 * 1000)), // +2 min (spiral)
217
+ mockCommit(new Date(baseTime + 3 * 60 * 1000)), // +1 min (spiral)
218
+ ];
219
+
220
+ const result = calculateTimeSpiral(commits);
221
+
222
+ expect(result.spiralCommits).toBe(2);
223
+ expect(result.totalCommits).toBe(3);
224
+ });
225
+
226
+ it('no spiral when commits >= 5 min apart', () => {
227
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
228
+ const commits = [
229
+ mockCommit(new Date(baseTime)),
230
+ mockCommit(new Date(baseTime + 10 * 60 * 1000)), // +10 min
231
+ mockCommit(new Date(baseTime + 20 * 60 * 1000)), // +10 min
232
+ ];
233
+
234
+ const result = calculateTimeSpiral(commits);
235
+
236
+ expect(result.spiralCommits).toBe(0);
237
+ expect(result.rating).toBe('elite');
238
+ });
239
+
240
+ it('calculates correct rating based on spiral ratio', () => {
241
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
242
+
243
+ // 50% spiral ratio (5 spirals out of 10 commits)
244
+ const commits = [
245
+ mockCommit(new Date(baseTime)),
246
+ mockCommit(new Date(baseTime + 1 * 60 * 1000)), // spiral
247
+ mockCommit(new Date(baseTime + 10 * 60 * 1000)),
248
+ mockCommit(new Date(baseTime + 11 * 60 * 1000)), // spiral
249
+ mockCommit(new Date(baseTime + 20 * 60 * 1000)),
250
+ mockCommit(new Date(baseTime + 21 * 60 * 1000)), // spiral
251
+ mockCommit(new Date(baseTime + 30 * 60 * 1000)),
252
+ mockCommit(new Date(baseTime + 31 * 60 * 1000)), // spiral
253
+ mockCommit(new Date(baseTime + 40 * 60 * 1000)),
254
+ mockCommit(new Date(baseTime + 41 * 60 * 1000)), // spiral
255
+ ];
256
+
257
+ const result = calculateTimeSpiral(commits);
258
+
259
+ expect(result.spiralCommits).toBe(5);
260
+ expect(result.rating).toBe('medium'); // 50% is in 30-50% range
261
+ });
262
+
263
+ it('returns low rating for >50% spiral ratio', () => {
264
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
265
+
266
+ // All rapid-fire commits
267
+ const commits = Array.from({ length: 10 }, (_, i) =>
268
+ mockCommit(new Date(baseTime + i * 60 * 1000)) // 1 min apart
269
+ );
270
+
271
+ const result = calculateTimeSpiral(commits);
272
+
273
+ expect(result.rating).toBe('low');
274
+ expect(result.description).toContain('frustrated iteration');
275
+ });
276
+
277
+ it('sorts commits by date before analysis', () => {
278
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
279
+
280
+ // Commits in random order
281
+ const commits = [
282
+ mockCommit(new Date(baseTime + 10 * 60 * 1000)),
283
+ mockCommit(new Date(baseTime)),
284
+ mockCommit(new Date(baseTime + 5 * 60 * 1000)),
285
+ ];
286
+
287
+ const result = calculateTimeSpiral(commits);
288
+
289
+ // Should detect no spirals (5+ min between each when sorted)
290
+ expect(result.spiralCommits).toBe(0);
291
+ });
292
+
293
+ it('includes description for each rating', () => {
294
+ const eliteResult = calculateTimeSpiral([
295
+ mockCommit(new Date('2025-11-28T10:00:00Z')),
296
+ ]);
297
+ expect(eliteResult.description).toContain('Insufficient');
298
+ });
299
+ });
300
+ });
301
+ ```
302
+
303
+ **Validation:** `npm test -- tests/metrics/time-spiral.test.ts`
304
+
305
+ ---
306
+
307
+ ### 3. `tests/metrics/velocity-anomaly.test.ts`
308
+
309
+ **Purpose:** Test velocity anomaly detection using z-score
310
+
311
+ ```typescript
312
+ import { describe, it, expect } from 'vitest';
313
+ import { calculateVelocityAnomaly } from '../../src/metrics/velocity-anomaly';
314
+ import { Commit } from '../../src/types';
315
+
316
+ describe('metrics/velocity-anomaly', () => {
317
+ const mockCommit = (date: Date): Commit => ({
318
+ hash: Math.random().toString(36).substring(7),
319
+ date,
320
+ message: 'test',
321
+ type: 'feat',
322
+ scope: null,
323
+ author: 'test',
324
+ });
325
+
326
+ describe('calculateVelocityAnomaly', () => {
327
+ it('returns result for empty commits', () => {
328
+ const result = calculateVelocityAnomaly([]);
329
+
330
+ expect(result.currentVelocity).toBe(0);
331
+ expect(result.zScore).toBeDefined();
332
+ });
333
+
334
+ it('uses default baseline when none provided', () => {
335
+ const commits = [
336
+ mockCommit(new Date('2025-11-28T10:00:00Z')),
337
+ ];
338
+
339
+ const result = calculateVelocityAnomaly(commits);
340
+
341
+ expect(result.baselineMean).toBe(3.0);
342
+ expect(result.baselineStdDev).toBe(1.5);
343
+ });
344
+
345
+ it('uses provided baseline', () => {
346
+ const commits = [
347
+ mockCommit(new Date('2025-11-28T10:00:00Z')),
348
+ ];
349
+ const baseline = { mean: 5.0, stdDev: 2.0 };
350
+
351
+ const result = calculateVelocityAnomaly(commits, baseline);
352
+
353
+ expect(result.baselineMean).toBe(5.0);
354
+ expect(result.baselineStdDev).toBe(2.0);
355
+ });
356
+
357
+ it('calculates z-score correctly', () => {
358
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
359
+ // 6 commits in 1 hour = 6 commits/hour velocity
360
+ const commits = Array.from({ length: 6 }, (_, i) =>
361
+ mockCommit(new Date(baseTime + i * 10 * 60 * 1000))
362
+ );
363
+
364
+ // Baseline: mean=3, stdDev=1.5
365
+ // Current velocity ~6, z-score = (6-3)/1.5 = 2
366
+ const result = calculateVelocityAnomaly(commits);
367
+
368
+ expect(result.currentVelocity).toBeGreaterThan(0);
369
+ expect(result.zScore).toBeGreaterThan(0);
370
+ });
371
+
372
+ it('returns elite for velocity near baseline', () => {
373
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
374
+ // ~3 commits per hour (matches default baseline mean)
375
+ const commits = [
376
+ mockCommit(new Date(baseTime)),
377
+ mockCommit(new Date(baseTime + 20 * 60 * 1000)),
378
+ mockCommit(new Date(baseTime + 40 * 60 * 1000)),
379
+ ];
380
+
381
+ const result = calculateVelocityAnomaly(commits);
382
+
383
+ expect(result.rating).toBe('elite');
384
+ expect(result.description).toContain('near baseline');
385
+ });
386
+
387
+ it('handles zero stdDev gracefully', () => {
388
+ const commits = [
389
+ mockCommit(new Date('2025-11-28T10:00:00Z')),
390
+ ];
391
+ const baseline = { mean: 3.0, stdDev: 0 };
392
+
393
+ const result = calculateVelocityAnomaly(commits, baseline);
394
+
395
+ expect(result.zScore).toBe(0);
396
+ });
397
+
398
+ it('returns low rating for high z-score', () => {
399
+ const baseTime = new Date('2025-11-28T10:00:00Z').getTime();
400
+ // 20 commits in 1 hour = very high velocity
401
+ const commits = Array.from({ length: 20 }, (_, i) =>
402
+ mockCommit(new Date(baseTime + i * 3 * 60 * 1000))
403
+ );
404
+
405
+ const result = calculateVelocityAnomaly(commits);
406
+
407
+ // Should be far from baseline
408
+ expect(result.zScore).toBeGreaterThan(2);
409
+ expect(result.rating).toBe('low');
410
+ expect(result.description).toContain('unusual pattern');
411
+ });
412
+
413
+ it('includes velocity in description', () => {
414
+ const commits = [
415
+ mockCommit(new Date('2025-11-28T10:00:00Z')),
416
+ mockCommit(new Date('2025-11-28T10:30:00Z')),
417
+ ];
418
+
419
+ const result = calculateVelocityAnomaly(commits);
420
+
421
+ expect(result.description).toContain('/hr');
422
+ });
423
+ });
424
+ });
425
+ ```
426
+
427
+ **Validation:** `npm test -- tests/metrics/velocity-anomaly.test.ts`
428
+
429
+ ---
430
+
431
+ ### 4. `tests/metrics/code-stability.test.ts`
432
+
433
+ **Purpose:** Test code stability calculation from line stats
434
+
435
+ ```typescript
436
+ import { describe, it, expect } from 'vitest';
437
+ import { calculateCodeStability } from '../../src/metrics/code-stability';
438
+ import { Commit } from '../../src/types';
439
+
440
+ describe('metrics/code-stability', () => {
441
+ const mockCommit = (message: string): Commit => ({
442
+ hash: Math.random().toString(36).substring(7),
443
+ date: new Date(),
444
+ message,
445
+ type: 'feat',
446
+ scope: null,
447
+ author: 'test',
448
+ });
449
+
450
+ describe('calculateCodeStability', () => {
451
+ it('estimates stability from commit messages when no stats', () => {
452
+ const commits = [
453
+ mockCommit('feat: add feature'),
454
+ mockCommit('fix: something'),
455
+ mockCommit('feat: another feature'),
456
+ ];
457
+
458
+ const result = calculateCodeStability(commits);
459
+
460
+ expect(result.description).toContain('Estimated');
461
+ expect(result.linesAdded).toBe(0);
462
+ expect(result.linesSurviving).toBe(0);
463
+ });
464
+
465
+ it('returns elite when no fix/revert commits', () => {
466
+ const commits = [
467
+ mockCommit('feat: add feature'),
468
+ mockCommit('feat: another feature'),
469
+ mockCommit('docs: update readme'),
470
+ ];
471
+
472
+ const result = calculateCodeStability(commits);
473
+
474
+ expect(result.value).toBe(100);
475
+ expect(result.rating).toBe('elite');
476
+ });
477
+
478
+ it('penalizes fix commits in estimation', () => {
479
+ const commits = [
480
+ mockCommit('feat: add feature'),
481
+ mockCommit('fix: broken thing'),
482
+ mockCommit('fix: another fix'),
483
+ mockCommit('fix: yet another'),
484
+ ];
485
+
486
+ const result = calculateCodeStability(commits);
487
+
488
+ // 75% fix commits = 25% score
489
+ expect(result.value).toBe(25);
490
+ expect(result.rating).toBe('low');
491
+ });
492
+
493
+ it('detects revert commits', () => {
494
+ const commits = [
495
+ mockCommit('feat: add feature'),
496
+ mockCommit('revert: undo feature'),
497
+ ];
498
+
499
+ const result = calculateCodeStability(commits);
500
+
501
+ expect(result.value).toBe(50); // 50% fix/revert
502
+ });
503
+
504
+ it('detects undo in messages', () => {
505
+ const commits = [
506
+ mockCommit('feat: add feature'),
507
+ mockCommit('chore: undo previous change'),
508
+ ];
509
+
510
+ const result = calculateCodeStability(commits);
511
+
512
+ expect(result.value).toBe(50);
513
+ });
514
+
515
+ it('calculates stability from line stats', () => {
516
+ const commits = [mockCommit('feat: test')];
517
+ const stats = [
518
+ { additions: 100, deletions: 20 },
519
+ ];
520
+
521
+ const result = calculateCodeStability(commits, stats);
522
+
523
+ // deletions/additions = 0.2, score = 1 - (0.2 * 0.5) = 0.9
524
+ expect(result.value).toBe(90);
525
+ expect(result.rating).toBe('elite');
526
+ expect(result.linesAdded).toBe(100);
527
+ });
528
+
529
+ it('handles high churn (many deletions)', () => {
530
+ const commits = [mockCommit('refactor: cleanup')];
531
+ const stats = [
532
+ { additions: 100, deletions: 100 },
533
+ ];
534
+
535
+ const result = calculateCodeStability(commits, stats);
536
+
537
+ // deletions/additions = 1.0 (capped), score = 1 - (1.0 * 0.5) = 0.5
538
+ expect(result.value).toBe(50);
539
+ expect(result.rating).toBe('medium');
540
+ });
541
+
542
+ it('aggregates multiple commit stats', () => {
543
+ const commits = [
544
+ mockCommit('feat: one'),
545
+ mockCommit('feat: two'),
546
+ ];
547
+ const stats = [
548
+ { additions: 50, deletions: 10 },
549
+ { additions: 50, deletions: 10 },
550
+ ];
551
+
552
+ const result = calculateCodeStability(commits, stats);
553
+
554
+ expect(result.linesAdded).toBe(100);
555
+ // 20/100 = 0.2 churn, score = 0.9
556
+ expect(result.value).toBe(90);
557
+ });
558
+
559
+ it('handles zero additions gracefully', () => {
560
+ const commits = [mockCommit('chore: delete files')];
561
+ const stats = [
562
+ { additions: 0, deletions: 50 },
563
+ ];
564
+
565
+ const result = calculateCodeStability(commits, stats);
566
+
567
+ expect(result.value).toBe(100); // 0 churn when no additions
568
+ });
569
+
570
+ it('returns correct rating for each threshold', () => {
571
+ // Elite >= 85%
572
+ const eliteStats = [{ additions: 100, deletions: 10 }];
573
+ const elite = calculateCodeStability([mockCommit('t')], eliteStats);
574
+ expect(elite.rating).toBe('elite');
575
+
576
+ // High 70-85%
577
+ const highStats = [{ additions: 100, deletions: 50 }];
578
+ const high = calculateCodeStability([mockCommit('t')], highStats);
579
+ expect(high.rating).toBe('high');
580
+
581
+ // Medium 50-70%
582
+ const medStats = [{ additions: 100, deletions: 80 }];
583
+ const med = calculateCodeStability([mockCommit('t')], medStats);
584
+ expect(med.rating).toBe('medium');
585
+
586
+ // Low < 50% (would need > 100% deletions which is capped)
587
+ });
588
+
589
+ it('includes line counts in description', () => {
590
+ const commits = [mockCommit('feat: test')];
591
+ const stats = [{ additions: 100, deletions: 20 }];
592
+
593
+ const result = calculateCodeStability(commits, stats);
594
+
595
+ expect(result.description).toContain('+100');
596
+ expect(result.description).toContain('-20');
597
+ });
598
+
599
+ it('calculates surviving lines', () => {
600
+ const commits = [mockCommit('feat: test')];
601
+ const stats = [{ additions: 100, deletions: 20 }];
602
+
603
+ const result = calculateCodeStability(commits, stats);
604
+
605
+ // linesSurviving = additions * score = 100 * 0.9 = 90
606
+ expect(result.linesSurviving).toBe(90);
607
+ });
608
+ });
609
+ });
610
+ ```
611
+
612
+ **Validation:** `npm test -- tests/metrics/code-stability.test.ts`
613
+
614
+ ---
615
+
616
+ ### 5. `tests/output/json.test.ts`
617
+
618
+ **Purpose:** Test JSON output formatting
619
+
620
+ ```typescript
621
+ import { describe, it, expect } from 'vitest';
622
+ import { formatJson } from '../../src/output/json';
623
+ import { VibeCheckResult, VibeCheckResultV2 } from '../../src/types';
624
+
625
+ describe('output/json', () => {
626
+ const createMockResult = (): VibeCheckResult => ({
627
+ period: {
628
+ from: new Date('2025-11-21T10:00:00Z'),
629
+ to: new Date('2025-11-28T10:00:00Z'),
630
+ activeHours: 24.5,
631
+ },
632
+ commits: {
633
+ total: 50,
634
+ feat: 20,
635
+ fix: 15,
636
+ docs: 5,
637
+ other: 10,
638
+ },
639
+ metrics: {
640
+ iterationVelocity: { value: 4.5, unit: 'commits/hour', rating: 'high', description: 'Good' },
641
+ reworkRatio: { value: 30, unit: '%', rating: 'medium', description: 'Normal' },
642
+ trustPassRate: { value: 92, unit: '%', rating: 'high', description: 'Good' },
643
+ debugSpiralDuration: { value: 15, unit: 'min', rating: 'high', description: 'Normal' },
644
+ flowEfficiency: { value: 85, unit: '%', rating: 'high', description: 'Good' },
645
+ },
646
+ fixChains: [
647
+ {
648
+ component: 'auth',
649
+ commits: 3,
650
+ duration: 15,
651
+ isSpiral: true,
652
+ pattern: 'SECRETS_AUTH',
653
+ firstCommit: new Date(),
654
+ lastCommit: new Date(),
655
+ },
656
+ ],
657
+ patterns: {
658
+ categories: { SECRETS_AUTH: 3 },
659
+ total: 3,
660
+ tracerAvailable: 100,
661
+ },
662
+ overall: 'HIGH',
663
+ });
664
+
665
+ describe('formatJson', () => {
666
+ it('returns valid JSON string', () => {
667
+ const result = createMockResult();
668
+ const output = formatJson(result);
669
+
670
+ expect(() => JSON.parse(output)).not.toThrow();
671
+ });
672
+
673
+ it('converts dates to ISO strings', () => {
674
+ const result = createMockResult();
675
+ const output = JSON.parse(formatJson(result));
676
+
677
+ expect(output.period.from).toBe('2025-11-21T10:00:00.000Z');
678
+ expect(output.period.to).toBe('2025-11-28T10:00:00.000Z');
679
+ });
680
+
681
+ it('includes all period info', () => {
682
+ const result = createMockResult();
683
+ const output = JSON.parse(formatJson(result));
684
+
685
+ expect(output.period.activeHours).toBe(24.5);
686
+ });
687
+
688
+ it('includes commit counts', () => {
689
+ const result = createMockResult();
690
+ const output = JSON.parse(formatJson(result));
691
+
692
+ expect(output.commits.total).toBe(50);
693
+ expect(output.commits.feat).toBe(20);
694
+ expect(output.commits.fix).toBe(15);
695
+ });
696
+
697
+ it('includes all metrics', () => {
698
+ const result = createMockResult();
699
+ const output = JSON.parse(formatJson(result));
700
+
701
+ expect(output.metrics.iterationVelocity.value).toBe(4.5);
702
+ expect(output.metrics.reworkRatio.rating).toBe('medium');
703
+ expect(output.metrics.trustPassRate.unit).toBe('%');
704
+ });
705
+
706
+ it('includes fix chains', () => {
707
+ const result = createMockResult();
708
+ const output = JSON.parse(formatJson(result));
709
+
710
+ expect(output.fixChains).toHaveLength(1);
711
+ expect(output.fixChains[0].component).toBe('auth');
712
+ expect(output.fixChains[0].pattern).toBe('SECRETS_AUTH');
713
+ });
714
+
715
+ it('includes patterns', () => {
716
+ const result = createMockResult();
717
+ const output = JSON.parse(formatJson(result));
718
+
719
+ expect(output.patterns.total).toBe(3);
720
+ expect(output.patterns.categories.SECRETS_AUTH).toBe(3);
721
+ });
722
+
723
+ it('includes overall rating', () => {
724
+ const result = createMockResult();
725
+ const output = JSON.parse(formatJson(result));
726
+
727
+ expect(output.overall).toBe('HIGH');
728
+ });
729
+
730
+ it('includes vibeScore for V2 results', () => {
731
+ const result: VibeCheckResultV2 = {
732
+ ...createMockResult(),
733
+ semanticMetrics: createMockResult().metrics,
734
+ vibeScore: {
735
+ value: 0.85,
736
+ components: { fileChurn: 0.9, timeSpiral: 0.8, velocityAnomaly: 0.85, codeStability: 0.85 },
737
+ weights: { fileChurn: 0.3, timeSpiral: 0.25, velocityAnomaly: 0.2, codeStability: 0.25 },
738
+ },
739
+ };
740
+
741
+ const output = JSON.parse(formatJson(result));
742
+
743
+ expect(output.vibeScore.value).toBe(0.85);
744
+ });
745
+
746
+ it('includes semanticFreeMetrics for V2 results', () => {
747
+ const result: VibeCheckResultV2 = {
748
+ ...createMockResult(),
749
+ semanticMetrics: createMockResult().metrics,
750
+ semanticFreeMetrics: {
751
+ fileChurn: { value: 90, unit: '%', rating: 'elite', description: 'Low', churnedFiles: 1, totalFiles: 10 },
752
+ timeSpiral: { value: 85, unit: '%', rating: 'high', description: 'Normal', spiralCommits: 2, totalCommits: 20 },
753
+ velocityAnomaly: { value: 80, unit: '%', rating: 'high', description: 'Normal', currentVelocity: 3.5, baselineMean: 3, baselineStdDev: 1, zScore: 0.5 },
754
+ codeStability: { value: 88, unit: '%', rating: 'elite', description: 'Good', linesAdded: 500, linesSurviving: 440 },
755
+ },
756
+ };
757
+
758
+ const output = JSON.parse(formatJson(result));
759
+
760
+ expect(output.semanticFreeMetrics.fileChurn.churnedFiles).toBe(1);
761
+ expect(output.semanticFreeMetrics.timeSpiral.spiralCommits).toBe(2);
762
+ expect(output.semanticFreeMetrics.velocityAnomaly.zScore).toBe(0.5);
763
+ expect(output.semanticFreeMetrics.codeStability.linesAdded).toBe(500);
764
+ });
765
+
766
+ it('handles empty fix chains', () => {
767
+ const result = createMockResult();
768
+ result.fixChains = [];
769
+
770
+ const output = JSON.parse(formatJson(result));
771
+
772
+ expect(output.fixChains).toEqual([]);
773
+ });
774
+
775
+ it('produces pretty-printed output', () => {
776
+ const result = createMockResult();
777
+ const output = formatJson(result);
778
+
779
+ expect(output).toContain('\n');
780
+ expect(output).toContain(' '); // indentation
781
+ });
782
+ });
783
+ });
784
+ ```
785
+
786
+ **Validation:** `npm test -- tests/output/json.test.ts`
787
+
788
+ ---
789
+
790
+ ### 6. `tests/output/markdown.test.ts`
791
+
792
+ **Purpose:** Test markdown output formatting
793
+
794
+ ```typescript
795
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
796
+ import { formatMarkdown } from '../../src/output/markdown';
797
+ import { VibeCheckResult, VibeCheckResultV2 } from '../../src/types';
798
+
799
+ describe('output/markdown', () => {
800
+ // Mock Date for consistent timestamp
801
+ beforeEach(() => {
802
+ vi.useFakeTimers();
803
+ vi.setSystemTime(new Date('2025-11-28T12:00:00Z'));
804
+ });
805
+
806
+ afterEach(() => {
807
+ vi.useRealTimers();
808
+ });
809
+
810
+ const createMockResult = (): VibeCheckResult => ({
811
+ period: {
812
+ from: new Date('2025-11-21T10:00:00Z'),
813
+ to: new Date('2025-11-28T10:00:00Z'),
814
+ activeHours: 24.5,
815
+ },
816
+ commits: {
817
+ total: 50,
818
+ feat: 20,
819
+ fix: 15,
820
+ docs: 5,
821
+ other: 10,
822
+ },
823
+ metrics: {
824
+ iterationVelocity: { value: 4.5, unit: 'commits/hour', rating: 'high', description: 'Good' },
825
+ reworkRatio: { value: 30, unit: '%', rating: 'medium', description: 'Normal' },
826
+ trustPassRate: { value: 92, unit: '%', rating: 'high', description: 'Good' },
827
+ debugSpiralDuration: { value: 15, unit: 'min', rating: 'high', description: 'Normal' },
828
+ flowEfficiency: { value: 85, unit: '%', rating: 'high', description: 'Good' },
829
+ },
830
+ fixChains: [],
831
+ patterns: { categories: {}, total: 0, tracerAvailable: 0 },
832
+ overall: 'HIGH',
833
+ });
834
+
835
+ describe('formatMarkdown', () => {
836
+ it('includes markdown header', () => {
837
+ const result = createMockResult();
838
+ const output = formatMarkdown(result);
839
+
840
+ expect(output).toContain('# Vibe-Check Report');
841
+ });
842
+
843
+ it('includes period information', () => {
844
+ const result = createMockResult();
845
+ const output = formatMarkdown(result);
846
+
847
+ expect(output).toContain('**Period:**');
848
+ expect(output).toContain('Nov 21, 2025');
849
+ expect(output).toContain('Nov 28, 2025');
850
+ expect(output).toContain('24.5h active');
851
+ });
852
+
853
+ it('includes commit counts', () => {
854
+ const result = createMockResult();
855
+ const output = formatMarkdown(result);
856
+
857
+ expect(output).toContain('**Commits:** 50 total');
858
+ expect(output).toContain('20 feat');
859
+ expect(output).toContain('15 fix');
860
+ });
861
+
862
+ it('includes overall rating', () => {
863
+ const result = createMockResult();
864
+ const output = formatMarkdown(result);
865
+
866
+ expect(output).toContain('**Overall Rating:** HIGH');
867
+ });
868
+
869
+ it('includes metrics table', () => {
870
+ const result = createMockResult();
871
+ const output = formatMarkdown(result);
872
+
873
+ expect(output).toContain('## Semantic Metrics');
874
+ expect(output).toContain('| Metric | Value | Rating | Description |');
875
+ expect(output).toContain('Iteration Velocity');
876
+ expect(output).toContain('4.5commits/hour');
877
+ expect(output).toContain('HIGH');
878
+ });
879
+
880
+ it('includes VibeScore for V2 results', () => {
881
+ const result: VibeCheckResultV2 = {
882
+ ...createMockResult(),
883
+ semanticMetrics: createMockResult().metrics,
884
+ vibeScore: {
885
+ value: 0.85,
886
+ components: { fileChurn: 0.9, timeSpiral: 0.8, velocityAnomaly: 0.85, codeStability: 0.85 },
887
+ weights: { fileChurn: 0.3, timeSpiral: 0.25, velocityAnomaly: 0.2, codeStability: 0.25 },
888
+ },
889
+ };
890
+
891
+ const output = formatMarkdown(result);
892
+
893
+ expect(output).toContain('**VibeScore:** 85%');
894
+ });
895
+
896
+ it('includes semantic-free metrics table for V2', () => {
897
+ const result: VibeCheckResultV2 = {
898
+ ...createMockResult(),
899
+ semanticMetrics: createMockResult().metrics,
900
+ semanticFreeMetrics: {
901
+ fileChurn: { value: 90, unit: '%', rating: 'elite', description: 'Low churn', churnedFiles: 1, totalFiles: 10 },
902
+ timeSpiral: { value: 85, unit: '%', rating: 'high', description: 'Normal', spiralCommits: 2, totalCommits: 20 },
903
+ velocityAnomaly: { value: 80, unit: '%', rating: 'high', description: 'Normal', currentVelocity: 3.5, baselineMean: 3, baselineStdDev: 1, zScore: 0.5 },
904
+ codeStability: { value: 88, unit: '%', rating: 'elite', description: 'Good', linesAdded: 500, linesSurviving: 440 },
905
+ },
906
+ };
907
+
908
+ const output = formatMarkdown(result);
909
+
910
+ expect(output).toContain('## Semantic-Free Metrics (v2.0)');
911
+ expect(output).toContain('File Churn');
912
+ expect(output).toContain('Time Spiral');
913
+ });
914
+
915
+ it('includes debug spirals when present', () => {
916
+ const result = createMockResult();
917
+ result.fixChains = [
918
+ {
919
+ component: 'auth',
920
+ commits: 3,
921
+ duration: 15,
922
+ isSpiral: true,
923
+ pattern: 'SECRETS_AUTH',
924
+ firstCommit: new Date(),
925
+ lastCommit: new Date(),
926
+ },
927
+ ];
928
+
929
+ const output = formatMarkdown(result);
930
+
931
+ expect(output).toContain('## Debug Spirals');
932
+ expect(output).toContain('| auth | 3 | 15m | SECRETS_AUTH |');
933
+ });
934
+
935
+ it('includes pattern analysis when patterns present', () => {
936
+ const result = createMockResult();
937
+ result.patterns = {
938
+ categories: { SECRETS_AUTH: 3, API_MISMATCH: 2 },
939
+ total: 5,
940
+ tracerAvailable: 60,
941
+ };
942
+
943
+ const output = formatMarkdown(result);
944
+
945
+ expect(output).toContain('## Pattern Analysis');
946
+ expect(output).toContain('SECRETS_AUTH');
947
+ expect(output).toContain('**60%** of fix patterns');
948
+ });
949
+
950
+ it('includes recommendations section', () => {
951
+ const result = createMockResult();
952
+ const output = formatMarkdown(result);
953
+
954
+ expect(output).toContain('## Recommendations');
955
+ });
956
+
957
+ it('adds recommendation for low rework ratio', () => {
958
+ const result = createMockResult();
959
+ result.metrics.reworkRatio.rating = 'low';
960
+
961
+ const output = formatMarkdown(result);
962
+
963
+ expect(output).toContain('High rework ratio');
964
+ expect(output).toContain('tracer tests');
965
+ });
966
+
967
+ it('adds recommendation for low trust pass rate', () => {
968
+ const result = createMockResult();
969
+ result.metrics.trustPassRate.rating = 'low';
970
+
971
+ const output = formatMarkdown(result);
972
+
973
+ expect(output).toContain('Trust pass rate below target');
974
+ });
975
+
976
+ it('adds recommendation for long debug spirals', () => {
977
+ const result = createMockResult();
978
+ result.metrics.debugSpiralDuration.rating = 'low';
979
+
980
+ const output = formatMarkdown(result);
981
+
982
+ expect(output).toContain('Long debug spirals');
983
+ expect(output).toContain('smaller, verifiable steps');
984
+ });
985
+
986
+ it('suggests tracer tests for detected patterns', () => {
987
+ const result = createMockResult();
988
+ result.fixChains = [
989
+ {
990
+ component: 'auth',
991
+ commits: 3,
992
+ duration: 15,
993
+ isSpiral: true,
994
+ pattern: 'SECRETS_AUTH',
995
+ firstCommit: new Date(),
996
+ lastCommit: new Date(),
997
+ },
998
+ ];
999
+
1000
+ const output = formatMarkdown(result);
1001
+
1002
+ expect(output).toContain('tracer tests for: SECRETS_AUTH');
1003
+ });
1004
+
1005
+ it('shows healthy message when all metrics good', () => {
1006
+ const result = createMockResult();
1007
+ // All ratings are already high/elite
1008
+
1009
+ const output = formatMarkdown(result);
1010
+
1011
+ expect(output).toContain('All metrics healthy');
1012
+ });
1013
+
1014
+ it('includes generation timestamp', () => {
1015
+ const result = createMockResult();
1016
+ const output = formatMarkdown(result);
1017
+
1018
+ expect(output).toContain('Generated by vibe-check');
1019
+ expect(output).toContain('2025-11-28');
1020
+ });
1021
+
1022
+ it('handles pattern without tracer (OTHER)', () => {
1023
+ const result = createMockResult();
1024
+ result.patterns = {
1025
+ categories: { OTHER: 2 },
1026
+ total: 2,
1027
+ tracerAvailable: 0,
1028
+ };
1029
+
1030
+ const output = formatMarkdown(result);
1031
+
1032
+ expect(output).toContain('| OTHER | 2 | No |');
1033
+ });
1034
+ });
1035
+ });
1036
+ ```
1037
+
1038
+ **Validation:** `npm test -- tests/output/markdown.test.ts`
1039
+
1040
+ ---
1041
+
1042
+ ## Implementation Order
1043
+
1044
+ | Step | Action | Validation | Rollback |
1045
+ |------|--------|------------|----------|
1046
+ | 1 | Create `tests/metrics/` directory | `ls tests/metrics/` | `rm -rf tests/metrics/` |
1047
+ | 2 | Create `file-churn.test.ts` | `npm test -- tests/metrics/file-churn.test.ts` | Delete file |
1048
+ | 3 | Create `time-spiral.test.ts` | `npm test -- tests/metrics/time-spiral.test.ts` | Delete file |
1049
+ | 4 | Create `velocity-anomaly.test.ts` | `npm test -- tests/metrics/velocity-anomaly.test.ts` | Delete file |
1050
+ | 5 | Create `code-stability.test.ts` | `npm test -- tests/metrics/code-stability.test.ts` | Delete file |
1051
+ | 6 | Create `json.test.ts` | `npm test -- tests/output/json.test.ts` | Delete file |
1052
+ | 7 | Create `markdown.test.ts` | `npm test -- tests/output/markdown.test.ts` | Delete file |
1053
+ | 8 | Full test suite | `npm test` | N/A |
1054
+ | 9 | Coverage report | `npm run test:coverage` | N/A |
1055
+
1056
+ ---
1057
+
1058
+ ## Validation Strategy
1059
+
1060
+ ### Per-file Validation
1061
+ ```bash
1062
+ npm test -- [path/to/test/file]
1063
+ # Expected: All tests pass
1064
+ ```
1065
+
1066
+ ### Full Suite Validation
1067
+ ```bash
1068
+ npm test
1069
+ # Expected: 250+ tests passing
1070
+ ```
1071
+
1072
+ ### Coverage Validation
1073
+ ```bash
1074
+ npm run test:coverage
1075
+ # Expected: >60% statement coverage overall
1076
+ # Expected: >80% on new test modules
1077
+ ```
1078
+
1079
+ ---
1080
+
1081
+ ## Rollback Procedure
1082
+
1083
+ **Time to rollback:** 1 minute
1084
+
1085
+ ### Full Rollback
1086
+ ```bash
1087
+ # Remove all new test files
1088
+ rm -f tests/metrics/file-churn.test.ts
1089
+ rm -f tests/metrics/time-spiral.test.ts
1090
+ rm -f tests/metrics/velocity-anomaly.test.ts
1091
+ rm -f tests/metrics/code-stability.test.ts
1092
+ rm -f tests/output/json.test.ts
1093
+ rm -f tests/output/markdown.test.ts
1094
+ rmdir tests/metrics 2>/dev/null
1095
+
1096
+ # Verify rollback
1097
+ npm test
1098
+ # Should show ~194 tests (original count)
1099
+ ```
1100
+
1101
+ ---
1102
+
1103
+ ## Approval Checklist
1104
+
1105
+ **Human must verify before /implement:**
1106
+
1107
+ - [ ] Every file specified precisely with full content
1108
+ - [ ] All tests follow project patterns (vitest, describe/it structure)
1109
+ - [ ] Test coverage targets reasonable (unit tests, not integration)
1110
+ - [ ] Implementation order is correct (directory before files)
1111
+ - [ ] Rollback procedure complete
1112
+
1113
+ ---
1114
+
1115
+ ## Next Step
1116
+
1117
+ Once approved: Execute implementation following order above.