@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.
- package/.agents/bundles/insight-mining-dashboard-research-2025-11-30.md +400 -0
- package/.agents/bundles/storage-enhancement-research-2025-11-30.md +292 -0
- package/.agents/bundles/timeline-feature-research-complete-2025-11-30.md +301 -0
- package/.agents/plans/insight-dashboard-plan-2025-11-30.md +1130 -0
- package/.agents/plans/json-storage-enhancement-plan.md +717 -0
- package/.agents/plans/storage-hardening-and-cache-plan.md +592 -0
- package/.agents/plans/test-coverage-gaps-plan.md +1117 -0
- package/.agents/plans/timeline-feature-plan.md +193 -0
- package/.agents/plans/vibe_timeline_research_findings.md +553 -0
- package/.claude/settings.local.json +1 -0
- package/.vibe-check/.gitignore +6 -0
- package/CHANGELOG.md +46 -0
- package/CLAUDE.md +24 -0
- package/CONTRIBUTING.md +227 -0
- package/README.md +200 -143
- package/claude-progress.json +191 -9
- package/claude-progress.txt +257 -0
- package/dashboard/app.js +75 -2
- package/dashboard/dashboard-data.json +653 -0
- package/dashboard/index.html +13 -0
- package/dashboard/styles.css +61 -0
- package/dist/analysis/cross-session-analysis.d.ts +68 -0
- package/dist/analysis/cross-session-analysis.d.ts.map +1 -0
- package/dist/analysis/cross-session-analysis.js +174 -0
- package/dist/analysis/cross-session-analysis.js.map +1 -0
- package/dist/analysis/index.d.ts +2 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +12 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/cli.js +10 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +105 -2
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/cache.d.ts +6 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +168 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/dashboard.d.ts +8 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +109 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +8 -1
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/timeline.d.ts +14 -0
- package/dist/commands/timeline.d.ts.map +1 -0
- package/dist/commands/timeline.js +462 -0
- package/dist/commands/timeline.js.map +1 -0
- package/dist/git.d.ts +24 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +94 -0
- package/dist/git.js.map +1 -1
- package/dist/insights/generators.d.ts +44 -0
- package/dist/insights/generators.d.ts.map +1 -0
- package/dist/insights/generators.js +289 -0
- package/dist/insights/generators.js.map +1 -0
- package/dist/insights/index.d.ts +16 -0
- package/dist/insights/index.d.ts.map +1 -0
- package/dist/insights/index.js +171 -0
- package/dist/insights/index.js.map +1 -0
- package/dist/insights/types.d.ts +93 -0
- package/dist/insights/types.d.ts.map +1 -0
- package/dist/insights/types.js +6 -0
- package/dist/insights/types.js.map +1 -0
- package/dist/output/timeline-html.d.ts +6 -0
- package/dist/output/timeline-html.d.ts.map +1 -0
- package/dist/output/timeline-html.js +389 -0
- package/dist/output/timeline-html.js.map +1 -0
- package/dist/output/timeline-markdown.d.ts +6 -0
- package/dist/output/timeline-markdown.d.ts.map +1 -0
- package/dist/output/timeline-markdown.js +167 -0
- package/dist/output/timeline-markdown.js.map +1 -0
- package/dist/output/timeline.d.ts +9 -0
- package/dist/output/timeline.d.ts.map +1 -0
- package/dist/output/timeline.js +318 -0
- package/dist/output/timeline.js.map +1 -0
- package/dist/patterns/detour.d.ts +32 -0
- package/dist/patterns/detour.d.ts.map +1 -0
- package/dist/patterns/detour.js +137 -0
- package/dist/patterns/detour.js.map +1 -0
- package/dist/patterns/flow-state.d.ts +16 -0
- package/dist/patterns/flow-state.d.ts.map +1 -0
- package/dist/patterns/flow-state.js +40 -0
- package/dist/patterns/flow-state.js.map +1 -0
- package/dist/patterns/index.d.ts +8 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +22 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/patterns/intervention-effectiveness.d.ts +42 -0
- package/dist/patterns/intervention-effectiveness.d.ts.map +1 -0
- package/dist/patterns/intervention-effectiveness.js +196 -0
- package/dist/patterns/intervention-effectiveness.js.map +1 -0
- package/dist/patterns/late-night.d.ts +30 -0
- package/dist/patterns/late-night.d.ts.map +1 -0
- package/dist/patterns/late-night.js +141 -0
- package/dist/patterns/late-night.js.map +1 -0
- package/dist/patterns/post-delete-sprint.d.ts +28 -0
- package/dist/patterns/post-delete-sprint.d.ts.map +1 -0
- package/dist/patterns/post-delete-sprint.js +85 -0
- package/dist/patterns/post-delete-sprint.js.map +1 -0
- package/dist/patterns/spiral-regression.d.ts +49 -0
- package/dist/patterns/spiral-regression.d.ts.map +1 -0
- package/dist/patterns/spiral-regression.js +219 -0
- package/dist/patterns/spiral-regression.js.map +1 -0
- package/dist/patterns/thrashing.d.ts +25 -0
- package/dist/patterns/thrashing.d.ts.map +1 -0
- package/dist/patterns/thrashing.js +111 -0
- package/dist/patterns/thrashing.js.map +1 -0
- package/dist/storage/atomic.d.ts +40 -0
- package/dist/storage/atomic.d.ts.map +1 -0
- package/dist/storage/atomic.js +155 -0
- package/dist/storage/atomic.js.map +1 -0
- package/dist/storage/commit-log.d.ts +35 -0
- package/dist/storage/commit-log.d.ts.map +1 -0
- package/dist/storage/commit-log.js +128 -0
- package/dist/storage/commit-log.js.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +33 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/schema.d.ts +32 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +37 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/timeline-store.d.ts +117 -0
- package/dist/storage/timeline-store.d.ts.map +1 -0
- package/dist/storage/timeline-store.js +438 -0
- package/dist/storage/timeline-store.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/ARCHITECTURE.md +458 -0
- package/docs/DATA-ARCHITECTURE.md +565 -0
- package/docs/GAMIFICATION.md +564 -0
- package/docs/JSON-STORAGE-PATTERNS.md +512 -0
- package/docs/METRICS-EXPLAINED.md +394 -0
- package/docs/UNIFIED-ECOSYSTEM.md +560 -0
- package/docs/VIBE-ECOSYSTEM.md +406 -0
- package/feature-list.json +48 -0
- package/package.json +2 -1
- package/vitest.config.ts +1 -5
- package/.vibe-check/calibration.json +0 -38
- package/.vibe-check/latest.json +0 -114
- package/.vibe-check/sessions.json +0 -44
- 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.
|