@boshu2/vibe-check 1.0.1 → 1.1.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/ml-learning-loop-complete-plan-2025-11-28.md +908 -0
- package/.agents/bundles/unified-vibe-system-plan-phase1-2025-11-28.md +962 -0
- package/.agents/bundles/unified-vibe-system-research-2025-11-28.md +1003 -0
- package/.agents/bundles/vibe-check-ecosystem-plan-2025-11-29.md +635 -0
- package/.agents/bundles/vibe-check-gamification-complete-2025-11-29.md +132 -0
- package/.agents/bundles/vibe-score-scientific-framework-2025-11-28.md +602 -0
- package/.vibe-check/calibration.json +38 -0
- package/.vibe-check/latest.json +114 -0
- package/CHANGELOG.md +46 -0
- package/CLAUDE.md +178 -0
- package/README.md +265 -63
- package/action.yml +270 -0
- package/dashboard/app.js +494 -0
- package/dashboard/index.html +235 -0
- package/dashboard/styles.css +647 -0
- package/dist/calibration/ece.d.ts +26 -0
- package/dist/calibration/ece.d.ts.map +1 -0
- package/dist/calibration/ece.js +93 -0
- package/dist/calibration/ece.js.map +1 -0
- package/dist/calibration/index.d.ts +3 -0
- package/dist/calibration/index.d.ts.map +1 -0
- package/dist/calibration/index.js +15 -0
- package/dist/calibration/index.js.map +1 -0
- package/dist/calibration/storage.d.ts +34 -0
- package/dist/calibration/storage.d.ts.map +1 -0
- package/dist/calibration/storage.js +188 -0
- package/dist/calibration/storage.js.map +1 -0
- package/dist/cli.js +30 -76
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts +16 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +256 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +11 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/level.d.ts +3 -0
- package/dist/commands/level.d.ts.map +1 -0
- package/dist/commands/level.js +277 -0
- package/dist/commands/level.js.map +1 -0
- package/dist/commands/profile.d.ts +4 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +143 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/gamification/achievements.d.ts +15 -0
- package/dist/gamification/achievements.d.ts.map +1 -0
- package/dist/gamification/achievements.js +273 -0
- package/dist/gamification/achievements.js.map +1 -0
- package/dist/gamification/index.d.ts +8 -0
- package/dist/gamification/index.d.ts.map +1 -0
- package/dist/gamification/index.js +30 -0
- package/dist/gamification/index.js.map +1 -0
- package/dist/gamification/profile.d.ts +46 -0
- package/dist/gamification/profile.d.ts.map +1 -0
- package/dist/gamification/profile.js +272 -0
- package/dist/gamification/profile.js.map +1 -0
- package/dist/gamification/streaks.d.ts +26 -0
- package/dist/gamification/streaks.d.ts.map +1 -0
- package/dist/gamification/streaks.js +132 -0
- package/dist/gamification/streaks.js.map +1 -0
- package/dist/gamification/types.d.ts +111 -0
- package/dist/gamification/types.d.ts.map +1 -0
- package/dist/gamification/types.js +26 -0
- package/dist/gamification/types.js.map +1 -0
- package/dist/gamification/xp.d.ts +37 -0
- package/dist/gamification/xp.d.ts.map +1 -0
- package/dist/gamification/xp.js +115 -0
- package/dist/gamification/xp.js.map +1 -0
- package/dist/git.d.ts +11 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +52 -0
- package/dist/git.js.map +1 -1
- package/dist/metrics/code-stability.d.ts +13 -0
- package/dist/metrics/code-stability.d.ts.map +1 -0
- package/dist/metrics/code-stability.js +74 -0
- package/dist/metrics/code-stability.js.map +1 -0
- package/dist/metrics/file-churn.d.ts +8 -0
- package/dist/metrics/file-churn.d.ts.map +1 -0
- package/dist/metrics/file-churn.js +75 -0
- package/dist/metrics/file-churn.js.map +1 -0
- package/dist/metrics/time-spiral.d.ts +8 -0
- package/dist/metrics/time-spiral.d.ts.map +1 -0
- package/dist/metrics/time-spiral.js +69 -0
- package/dist/metrics/time-spiral.js.map +1 -0
- package/dist/metrics/velocity-anomaly.d.ts +13 -0
- package/dist/metrics/velocity-anomaly.d.ts.map +1 -0
- package/dist/metrics/velocity-anomaly.js +67 -0
- package/dist/metrics/velocity-anomaly.js.map +1 -0
- package/dist/output/index.d.ts +6 -3
- package/dist/output/index.d.ts.map +1 -1
- package/dist/output/index.js +4 -3
- package/dist/output/index.js.map +1 -1
- package/dist/output/json.d.ts +2 -2
- package/dist/output/json.d.ts.map +1 -1
- package/dist/output/json.js +54 -0
- package/dist/output/json.js.map +1 -1
- package/dist/output/markdown.d.ts +2 -2
- package/dist/output/markdown.d.ts.map +1 -1
- package/dist/output/markdown.js +34 -1
- package/dist/output/markdown.js.map +1 -1
- package/dist/output/terminal.d.ts +6 -2
- package/dist/output/terminal.d.ts.map +1 -1
- package/dist/output/terminal.js +131 -3
- package/dist/output/terminal.js.map +1 -1
- package/dist/recommend/index.d.ts +3 -0
- package/dist/recommend/index.d.ts.map +1 -0
- package/dist/recommend/index.js +14 -0
- package/dist/recommend/index.js.map +1 -0
- package/dist/recommend/ordered-logistic.d.ts +49 -0
- package/dist/recommend/ordered-logistic.d.ts.map +1 -0
- package/dist/recommend/ordered-logistic.js +153 -0
- package/dist/recommend/ordered-logistic.js.map +1 -0
- package/dist/recommend/questions.d.ts +19 -0
- package/dist/recommend/questions.d.ts.map +1 -0
- package/dist/recommend/questions.js +73 -0
- package/dist/recommend/questions.js.map +1 -0
- package/dist/score/index.d.ts +21 -0
- package/dist/score/index.d.ts.map +1 -0
- package/dist/score/index.js +48 -0
- package/dist/score/index.js.map +1 -0
- package/dist/score/weights.d.ts +16 -0
- package/dist/score/weights.d.ts.map +1 -0
- package/dist/score/weights.js +28 -0
- package/dist/score/weights.js.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -9
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
# Unified Vibe System: Phase 1 Implementation Plan
|
|
2
|
+
|
|
3
|
+
**Type:** Plan
|
|
4
|
+
**Created:** 2025-11-28
|
|
5
|
+
**Depends On:** `unified-vibe-system-research-2025-11-28.md`
|
|
6
|
+
**Loop:** Middle (bridges research to implementation)
|
|
7
|
+
**Tags:** vibe-check, vibe-score, semantic-free-metrics, phase-1
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Add **4 semantic-commit-free metrics** to vibe-check, producing a composite **Vibe Score (0-1)**. This extends the existing tool without breaking changes.
|
|
14
|
+
|
|
15
|
+
**What changes:**
|
|
16
|
+
- New types for commit files and vibe score
|
|
17
|
+
- Enhanced git log parsing to include files changed
|
|
18
|
+
- 4 new metric calculators (file churn, time spiral, velocity anomaly, code stability)
|
|
19
|
+
- Vibe Score composite calculator
|
|
20
|
+
- Enhanced output to display Vibe Score
|
|
21
|
+
|
|
22
|
+
**What doesn't change:**
|
|
23
|
+
- Existing 5 metrics continue to work
|
|
24
|
+
- CLI interface unchanged (Vibe Score auto-displayed)
|
|
25
|
+
- All existing tests should pass
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Approach Selected
|
|
30
|
+
|
|
31
|
+
From research: **Weighted Composite Score** with 4 components
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
VibeScore = 0.30×FileChurn + 0.25×TimeSpiral + 0.20×VelocityAnomaly + 0.25×CodeStability
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
All components normalized to 0-1, higher = better.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## PDC Strategy
|
|
42
|
+
|
|
43
|
+
### Prevent
|
|
44
|
+
- [x] Read all existing code before modifications
|
|
45
|
+
- [ ] Run `npm test` before starting to establish baseline
|
|
46
|
+
- [ ] Commit after each file creation/modification
|
|
47
|
+
|
|
48
|
+
### Detect
|
|
49
|
+
- [ ] `npm run build` after each TypeScript change
|
|
50
|
+
- [ ] `npm test` after completing each metric
|
|
51
|
+
- [ ] Manual test with `npm run dev -- --since "1 week ago"`
|
|
52
|
+
|
|
53
|
+
### Correct
|
|
54
|
+
- [ ] Git revert individual commits if issues found
|
|
55
|
+
- [ ] Each new file can be deleted independently
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Files to Create
|
|
60
|
+
|
|
61
|
+
### 1. `src/types.ts` (MODIFY - lines 5-12)
|
|
62
|
+
|
|
63
|
+
**Purpose:** Add CommitWithFiles interface and VibeScore types
|
|
64
|
+
|
|
65
|
+
**Current (lines 5-12):**
|
|
66
|
+
```typescript
|
|
67
|
+
export interface Commit {
|
|
68
|
+
hash: string;
|
|
69
|
+
date: Date;
|
|
70
|
+
message: string;
|
|
71
|
+
type: 'feat' | 'fix' | 'docs' | 'chore' | 'refactor' | 'test' | 'style' | 'other';
|
|
72
|
+
scope: string | null;
|
|
73
|
+
author: string;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**After (replace lines 5-28):**
|
|
78
|
+
```typescript
|
|
79
|
+
export interface Commit {
|
|
80
|
+
hash: string;
|
|
81
|
+
date: Date;
|
|
82
|
+
message: string;
|
|
83
|
+
type: 'feat' | 'fix' | 'docs' | 'chore' | 'refactor' | 'test' | 'style' | 'other';
|
|
84
|
+
scope: string | null;
|
|
85
|
+
author: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface CommitWithFiles extends Commit {
|
|
89
|
+
files: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface VibeScoreComponents {
|
|
93
|
+
fileChurn: number;
|
|
94
|
+
timeSpiral: number;
|
|
95
|
+
velocityAnomaly: number;
|
|
96
|
+
codeStability: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface VibeScoreResult {
|
|
100
|
+
score: number;
|
|
101
|
+
components: VibeScoreComponents;
|
|
102
|
+
weights: number[];
|
|
103
|
+
interpretation: 'elite' | 'high' | 'medium' | 'low';
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Validation:** `npm run build` should compile without errors
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### 2. `src/types.ts` (MODIFY - add to VibeCheckResult)
|
|
112
|
+
|
|
113
|
+
**Purpose:** Add vibeScore to the result interface
|
|
114
|
+
|
|
115
|
+
**Current (lines 37-60):**
|
|
116
|
+
```typescript
|
|
117
|
+
export interface VibeCheckResult {
|
|
118
|
+
period: {
|
|
119
|
+
from: Date;
|
|
120
|
+
to: Date;
|
|
121
|
+
activeHours: number;
|
|
122
|
+
};
|
|
123
|
+
commits: {
|
|
124
|
+
total: number;
|
|
125
|
+
feat: number;
|
|
126
|
+
fix: number;
|
|
127
|
+
docs: number;
|
|
128
|
+
other: number;
|
|
129
|
+
};
|
|
130
|
+
metrics: {
|
|
131
|
+
iterationVelocity: MetricResult;
|
|
132
|
+
reworkRatio: MetricResult;
|
|
133
|
+
trustPassRate: MetricResult;
|
|
134
|
+
debugSpiralDuration: MetricResult;
|
|
135
|
+
flowEfficiency: MetricResult;
|
|
136
|
+
};
|
|
137
|
+
fixChains: FixChain[];
|
|
138
|
+
patterns: PatternSummary;
|
|
139
|
+
overall: OverallRating;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**After (replace lines 37-61):**
|
|
144
|
+
```typescript
|
|
145
|
+
export interface VibeCheckResult {
|
|
146
|
+
period: {
|
|
147
|
+
from: Date;
|
|
148
|
+
to: Date;
|
|
149
|
+
activeHours: number;
|
|
150
|
+
};
|
|
151
|
+
commits: {
|
|
152
|
+
total: number;
|
|
153
|
+
feat: number;
|
|
154
|
+
fix: number;
|
|
155
|
+
docs: number;
|
|
156
|
+
other: number;
|
|
157
|
+
};
|
|
158
|
+
metrics: {
|
|
159
|
+
iterationVelocity: MetricResult;
|
|
160
|
+
reworkRatio: MetricResult;
|
|
161
|
+
trustPassRate: MetricResult;
|
|
162
|
+
debugSpiralDuration: MetricResult;
|
|
163
|
+
flowEfficiency: MetricResult;
|
|
164
|
+
};
|
|
165
|
+
fixChains: FixChain[];
|
|
166
|
+
patterns: PatternSummary;
|
|
167
|
+
overall: OverallRating;
|
|
168
|
+
vibeScore: VibeScoreResult;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Validation:** `npm run build`
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### 3. `src/git.ts` (MODIFY - enhanced parsing)
|
|
177
|
+
|
|
178
|
+
**Purpose:** Fetch files changed per commit for file churn analysis
|
|
179
|
+
|
|
180
|
+
**Current function `getCommits` (lines 6-33):**
|
|
181
|
+
```typescript
|
|
182
|
+
export async function getCommits(
|
|
183
|
+
repoPath: string,
|
|
184
|
+
since?: string,
|
|
185
|
+
until?: string
|
|
186
|
+
): Promise<Commit[]> {
|
|
187
|
+
const git: SimpleGit = simpleGit(repoPath);
|
|
188
|
+
|
|
189
|
+
// Build options for git log
|
|
190
|
+
const options: Record<string, string | number | boolean> = {};
|
|
191
|
+
|
|
192
|
+
if (since) {
|
|
193
|
+
options['--since'] = since;
|
|
194
|
+
}
|
|
195
|
+
if (until) {
|
|
196
|
+
options['--until'] = until;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const log: LogResult<DefaultLogFields> = await git.log(options);
|
|
201
|
+
|
|
202
|
+
return log.all.map((entry) => parseCommit(entry));
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error instanceof Error) {
|
|
205
|
+
throw new Error(`Failed to read git log: ${error.message}`);
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**After (replace lines 1-73 entirely):**
|
|
213
|
+
```typescript
|
|
214
|
+
import simpleGit, { SimpleGit, LogResult, DefaultLogFields } from 'simple-git';
|
|
215
|
+
import { Commit, CommitWithFiles } from './types';
|
|
216
|
+
|
|
217
|
+
const COMMIT_TYPES = ['feat', 'fix', 'docs', 'chore', 'refactor', 'test', 'style'] as const;
|
|
218
|
+
|
|
219
|
+
export async function getCommits(
|
|
220
|
+
repoPath: string,
|
|
221
|
+
since?: string,
|
|
222
|
+
until?: string
|
|
223
|
+
): Promise<Commit[]> {
|
|
224
|
+
const commits = await getCommitsWithFiles(repoPath, since, until);
|
|
225
|
+
return commits;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function getCommitsWithFiles(
|
|
229
|
+
repoPath: string,
|
|
230
|
+
since?: string,
|
|
231
|
+
until?: string
|
|
232
|
+
): Promise<CommitWithFiles[]> {
|
|
233
|
+
const git: SimpleGit = simpleGit(repoPath);
|
|
234
|
+
|
|
235
|
+
// Build options for git log with file stats
|
|
236
|
+
const options: string[] = ['--name-only'];
|
|
237
|
+
|
|
238
|
+
if (since) {
|
|
239
|
+
options.push(`--since=${since}`);
|
|
240
|
+
}
|
|
241
|
+
if (until) {
|
|
242
|
+
options.push(`--until=${until}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const log: LogResult<DefaultLogFields> = await git.log(options);
|
|
247
|
+
|
|
248
|
+
// Parse each commit and extract files
|
|
249
|
+
const commits: CommitWithFiles[] = [];
|
|
250
|
+
|
|
251
|
+
for (const entry of log.all) {
|
|
252
|
+
const baseCommit = parseCommit(entry);
|
|
253
|
+
const files = await getFilesForCommit(git, entry.hash);
|
|
254
|
+
commits.push({ ...baseCommit, files });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return commits;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (error instanceof Error) {
|
|
260
|
+
throw new Error(`Failed to read git log: ${error.message}`);
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function getFilesForCommit(git: SimpleGit, hash: string): Promise<string[]> {
|
|
267
|
+
try {
|
|
268
|
+
const result = await git.show([hash, '--name-only', '--format=']);
|
|
269
|
+
return result
|
|
270
|
+
.split('\n')
|
|
271
|
+
.map(line => line.trim())
|
|
272
|
+
.filter(line => line.length > 0);
|
|
273
|
+
} catch {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseCommit(entry: DefaultLogFields): Commit {
|
|
279
|
+
const { hash, date, message, author_name } = entry;
|
|
280
|
+
|
|
281
|
+
// Parse conventional commit format: type(scope): description
|
|
282
|
+
const conventionalMatch = message.match(/^(\w+)(?:\(([^)]+)\))?:\s*(.+)/);
|
|
283
|
+
|
|
284
|
+
let type: Commit['type'] = 'other';
|
|
285
|
+
let scope: string | null = null;
|
|
286
|
+
|
|
287
|
+
if (conventionalMatch) {
|
|
288
|
+
const [, rawType, rawScope] = conventionalMatch;
|
|
289
|
+
const normalizedType = rawType.toLowerCase();
|
|
290
|
+
|
|
291
|
+
if (COMMIT_TYPES.includes(normalizedType as typeof COMMIT_TYPES[number])) {
|
|
292
|
+
type = normalizedType as Commit['type'];
|
|
293
|
+
}
|
|
294
|
+
scope = rawScope || null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
hash: hash.substring(0, 7),
|
|
299
|
+
date: new Date(date),
|
|
300
|
+
message: message.split('\n')[0], // First line only
|
|
301
|
+
type,
|
|
302
|
+
scope,
|
|
303
|
+
author: author_name,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function isGitRepo(repoPath: string): Promise<boolean> {
|
|
308
|
+
const git: SimpleGit = simpleGit(repoPath);
|
|
309
|
+
try {
|
|
310
|
+
await git.status();
|
|
311
|
+
return true;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Validation:** `npm run build && npm run dev -- --since "1 week ago"`
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### 4. `src/metrics/vibeScore.ts` (CREATE)
|
|
323
|
+
|
|
324
|
+
**Purpose:** Calculate the 4 semantic-free metrics and composite Vibe Score
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { CommitWithFiles, VibeScoreResult, VibeScoreComponents } from '../types';
|
|
328
|
+
import { calculateActiveHours } from './velocity';
|
|
329
|
+
|
|
330
|
+
// Default weights (will be calibrated over time)
|
|
331
|
+
const DEFAULT_WEIGHTS = [0.30, 0.25, 0.20, 0.25];
|
|
332
|
+
|
|
333
|
+
// Thresholds
|
|
334
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
335
|
+
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
336
|
+
|
|
337
|
+
interface Baseline {
|
|
338
|
+
mean: number;
|
|
339
|
+
stdDev: number;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Calculate composite Vibe Score from 4 semantic-free metrics
|
|
344
|
+
*/
|
|
345
|
+
export function calculateVibeScore(
|
|
346
|
+
commits: CommitWithFiles[],
|
|
347
|
+
baseline?: Baseline,
|
|
348
|
+
weights: number[] = DEFAULT_WEIGHTS
|
|
349
|
+
): VibeScoreResult {
|
|
350
|
+
if (commits.length === 0) {
|
|
351
|
+
return {
|
|
352
|
+
score: 1.0,
|
|
353
|
+
components: {
|
|
354
|
+
fileChurn: 1.0,
|
|
355
|
+
timeSpiral: 1.0,
|
|
356
|
+
velocityAnomaly: 1.0,
|
|
357
|
+
codeStability: 1.0,
|
|
358
|
+
},
|
|
359
|
+
weights,
|
|
360
|
+
interpretation: 'elite',
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const components: VibeScoreComponents = {
|
|
365
|
+
fileChurn: calculateFileChurnScore(commits),
|
|
366
|
+
timeSpiral: calculateTimeSpiralScore(commits),
|
|
367
|
+
velocityAnomaly: calculateVelocityAnomalyScore(commits, baseline),
|
|
368
|
+
codeStability: 1.0, // Placeholder - requires git blame (Phase 2)
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const score =
|
|
372
|
+
weights[0] * components.fileChurn +
|
|
373
|
+
weights[1] * components.timeSpiral +
|
|
374
|
+
weights[2] * components.velocityAnomaly +
|
|
375
|
+
weights[3] * components.codeStability;
|
|
376
|
+
|
|
377
|
+
const interpretation = getInterpretation(score);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
score: Math.round(score * 100) / 100,
|
|
381
|
+
components,
|
|
382
|
+
weights,
|
|
383
|
+
interpretation,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* File Churn Score: Did code stick on first touch?
|
|
389
|
+
* Detects files touched 3+ times within 1 hour
|
|
390
|
+
*/
|
|
391
|
+
export function calculateFileChurnScore(commits: CommitWithFiles[]): number {
|
|
392
|
+
if (commits.length < 3) return 1.0;
|
|
393
|
+
|
|
394
|
+
const fileTimestamps = new Map<string, Date[]>();
|
|
395
|
+
|
|
396
|
+
// Collect all touch timestamps per file
|
|
397
|
+
for (const commit of commits) {
|
|
398
|
+
for (const file of commit.files) {
|
|
399
|
+
const times = fileTimestamps.get(file) || [];
|
|
400
|
+
times.push(commit.date);
|
|
401
|
+
fileTimestamps.set(file, times);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (fileTimestamps.size === 0) return 1.0;
|
|
406
|
+
|
|
407
|
+
let churnedFiles = 0;
|
|
408
|
+
|
|
409
|
+
for (const [, times] of fileTimestamps) {
|
|
410
|
+
if (times.length < 3) continue;
|
|
411
|
+
|
|
412
|
+
const sorted = [...times].sort((a, b) => a.getTime() - b.getTime());
|
|
413
|
+
|
|
414
|
+
// Detect 3+ touches within 1 hour (spiral indicator)
|
|
415
|
+
for (let i = 0; i < sorted.length - 2; i++) {
|
|
416
|
+
const span = sorted[i + 2].getTime() - sorted[i].getTime();
|
|
417
|
+
if (span < ONE_HOUR_MS) {
|
|
418
|
+
churnedFiles++;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const churnRatio = churnedFiles / fileTimestamps.size;
|
|
425
|
+
return Math.round((1 - churnRatio) * 100) / 100;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Time Spiral Score: Are commits clustered in frustrated bursts?
|
|
430
|
+
* Detects commits less than 5 minutes apart
|
|
431
|
+
*/
|
|
432
|
+
export function calculateTimeSpiralScore(commits: CommitWithFiles[]): number {
|
|
433
|
+
if (commits.length < 2) return 1.0;
|
|
434
|
+
|
|
435
|
+
const sorted = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
436
|
+
|
|
437
|
+
let spiralCommits = 0;
|
|
438
|
+
|
|
439
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
440
|
+
const gap = sorted[i].date.getTime() - sorted[i - 1].date.getTime();
|
|
441
|
+
if (gap < FIVE_MINUTES_MS) {
|
|
442
|
+
spiralCommits++;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const spiralRatio = spiralCommits / (commits.length - 1);
|
|
447
|
+
return Math.round((1 - spiralRatio) * 100) / 100;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Velocity Anomaly Score: Is this pattern abnormal?
|
|
452
|
+
* Uses z-score from personal baseline (if available)
|
|
453
|
+
*/
|
|
454
|
+
export function calculateVelocityAnomalyScore(
|
|
455
|
+
commits: CommitWithFiles[],
|
|
456
|
+
baseline?: Baseline
|
|
457
|
+
): number {
|
|
458
|
+
if (!baseline || baseline.stdDev === 0) {
|
|
459
|
+
// No baseline available, return neutral score
|
|
460
|
+
return 0.75;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const hours = calculateActiveHours(commits);
|
|
464
|
+
const currentVelocity = hours > 0 ? commits.length / hours : 0;
|
|
465
|
+
|
|
466
|
+
// Z-score: how many std devs from personal mean
|
|
467
|
+
const zScore = Math.abs((currentVelocity - baseline.mean) / baseline.stdDev);
|
|
468
|
+
|
|
469
|
+
// Sigmoid transform: z=0 → 1.0, z=2 → ~0.38, z=3 → ~0.18
|
|
470
|
+
const score = 1 / (1 + Math.exp(zScore - 1.5));
|
|
471
|
+
return Math.round(score * 100) / 100;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get interpretation from composite score
|
|
476
|
+
*/
|
|
477
|
+
function getInterpretation(score: number): 'elite' | 'high' | 'medium' | 'low' {
|
|
478
|
+
if (score >= 0.85) return 'elite';
|
|
479
|
+
if (score >= 0.70) return 'high';
|
|
480
|
+
if (score >= 0.50) return 'medium';
|
|
481
|
+
return 'low';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Calculate baseline from historical commits
|
|
486
|
+
* Call this with 30 days of history to establish personal baseline
|
|
487
|
+
*/
|
|
488
|
+
export function calculateBaseline(commits: CommitWithFiles[]): Baseline {
|
|
489
|
+
if (commits.length < 10) {
|
|
490
|
+
return { mean: 3, stdDev: 1.5 }; // Default baseline
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Group commits by day and calculate daily velocities
|
|
494
|
+
const dailyVelocities: number[] = [];
|
|
495
|
+
const byDay = new Map<string, CommitWithFiles[]>();
|
|
496
|
+
|
|
497
|
+
for (const commit of commits) {
|
|
498
|
+
const day = commit.date.toISOString().split('T')[0];
|
|
499
|
+
const dayCommits = byDay.get(day) || [];
|
|
500
|
+
dayCommits.push(commit);
|
|
501
|
+
byDay.set(day, dayCommits);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
for (const dayCommits of byDay.values()) {
|
|
505
|
+
const hours = calculateActiveHours(dayCommits);
|
|
506
|
+
if (hours > 0) {
|
|
507
|
+
dailyVelocities.push(dayCommits.length / hours);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (dailyVelocities.length === 0) {
|
|
512
|
+
return { mean: 3, stdDev: 1.5 };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const mean = dailyVelocities.reduce((a, b) => a + b, 0) / dailyVelocities.length;
|
|
516
|
+
const variance =
|
|
517
|
+
dailyVelocities.reduce((sum, v) => sum + (v - mean) ** 2, 0) / dailyVelocities.length;
|
|
518
|
+
const stdDev = Math.sqrt(variance);
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
mean: Math.round(mean * 100) / 100,
|
|
522
|
+
stdDev: Math.round(stdDev * 100) / 100 || 1,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Validation:** `npm run build`
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
### 5. `src/metrics/index.ts` (MODIFY - integrate Vibe Score)
|
|
532
|
+
|
|
533
|
+
**Purpose:** Add vibeScore calculation to analyzeCommits
|
|
534
|
+
|
|
535
|
+
**Current (lines 1-10):**
|
|
536
|
+
```typescript
|
|
537
|
+
import { Commit, VibeCheckResult, OverallRating, Rating } from '../types';
|
|
538
|
+
import { calculateIterationVelocity, calculateActiveHours } from './velocity';
|
|
539
|
+
import { calculateReworkRatio } from './rework';
|
|
540
|
+
import { calculateTrustPassRate } from './trust';
|
|
541
|
+
import {
|
|
542
|
+
detectFixChains,
|
|
543
|
+
calculateDebugSpiralDuration,
|
|
544
|
+
calculatePatternSummary,
|
|
545
|
+
} from './spirals';
|
|
546
|
+
import { calculateFlowEfficiency } from './flow';
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**After (lines 1-12):**
|
|
550
|
+
```typescript
|
|
551
|
+
import { Commit, CommitWithFiles, VibeCheckResult, OverallRating, Rating } from '../types';
|
|
552
|
+
import { calculateIterationVelocity, calculateActiveHours } from './velocity';
|
|
553
|
+
import { calculateReworkRatio } from './rework';
|
|
554
|
+
import { calculateTrustPassRate } from './trust';
|
|
555
|
+
import {
|
|
556
|
+
detectFixChains,
|
|
557
|
+
calculateDebugSpiralDuration,
|
|
558
|
+
calculatePatternSummary,
|
|
559
|
+
} from './spirals';
|
|
560
|
+
import { calculateFlowEfficiency } from './flow';
|
|
561
|
+
import { calculateVibeScore } from './vibeScore';
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Current `analyzeCommits` function (lines 12-66):**
|
|
565
|
+
```typescript
|
|
566
|
+
export function analyzeCommits(commits: Commit[]): VibeCheckResult {
|
|
567
|
+
if (commits.length === 0) {
|
|
568
|
+
return emptyResult();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Sort commits by date
|
|
572
|
+
const sorted = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
573
|
+
const from = sorted[0].date;
|
|
574
|
+
const to = sorted[sorted.length - 1].date;
|
|
575
|
+
const activeHours = calculateActiveHours(sorted);
|
|
576
|
+
|
|
577
|
+
// Count commit types
|
|
578
|
+
const commitCounts = countCommitTypes(sorted);
|
|
579
|
+
|
|
580
|
+
// Detect fix chains
|
|
581
|
+
const fixChains = detectFixChains(sorted);
|
|
582
|
+
|
|
583
|
+
// Calculate all metrics
|
|
584
|
+
const iterationVelocity = calculateIterationVelocity(sorted);
|
|
585
|
+
const reworkRatio = calculateReworkRatio(sorted);
|
|
586
|
+
const trustPassRate = calculateTrustPassRate(sorted);
|
|
587
|
+
const debugSpiralDuration = calculateDebugSpiralDuration(fixChains);
|
|
588
|
+
const flowEfficiency = calculateFlowEfficiency(activeHours * 60, fixChains);
|
|
589
|
+
|
|
590
|
+
// Calculate pattern summary
|
|
591
|
+
const patterns = calculatePatternSummary(fixChains);
|
|
592
|
+
|
|
593
|
+
// Determine overall rating
|
|
594
|
+
const overall = calculateOverallRating([
|
|
595
|
+
iterationVelocity.rating,
|
|
596
|
+
reworkRatio.rating,
|
|
597
|
+
trustPassRate.rating,
|
|
598
|
+
debugSpiralDuration.rating,
|
|
599
|
+
flowEfficiency.rating,
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
period: {
|
|
604
|
+
from,
|
|
605
|
+
to,
|
|
606
|
+
activeHours: Math.round(activeHours * 10) / 10,
|
|
607
|
+
},
|
|
608
|
+
commits: commitCounts,
|
|
609
|
+
metrics: {
|
|
610
|
+
iterationVelocity,
|
|
611
|
+
reworkRatio,
|
|
612
|
+
trustPassRate,
|
|
613
|
+
debugSpiralDuration,
|
|
614
|
+
flowEfficiency,
|
|
615
|
+
},
|
|
616
|
+
fixChains,
|
|
617
|
+
patterns,
|
|
618
|
+
overall,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**After (lines 12-70):**
|
|
624
|
+
```typescript
|
|
625
|
+
export function analyzeCommits(commits: Commit[] | CommitWithFiles[]): VibeCheckResult {
|
|
626
|
+
if (commits.length === 0) {
|
|
627
|
+
return emptyResult();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Sort commits by date
|
|
631
|
+
const sorted = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
632
|
+
const from = sorted[0].date;
|
|
633
|
+
const to = sorted[sorted.length - 1].date;
|
|
634
|
+
const activeHours = calculateActiveHours(sorted);
|
|
635
|
+
|
|
636
|
+
// Count commit types
|
|
637
|
+
const commitCounts = countCommitTypes(sorted);
|
|
638
|
+
|
|
639
|
+
// Detect fix chains
|
|
640
|
+
const fixChains = detectFixChains(sorted);
|
|
641
|
+
|
|
642
|
+
// Calculate all metrics
|
|
643
|
+
const iterationVelocity = calculateIterationVelocity(sorted);
|
|
644
|
+
const reworkRatio = calculateReworkRatio(sorted);
|
|
645
|
+
const trustPassRate = calculateTrustPassRate(sorted);
|
|
646
|
+
const debugSpiralDuration = calculateDebugSpiralDuration(fixChains);
|
|
647
|
+
const flowEfficiency = calculateFlowEfficiency(activeHours * 60, fixChains);
|
|
648
|
+
|
|
649
|
+
// Calculate pattern summary
|
|
650
|
+
const patterns = calculatePatternSummary(fixChains);
|
|
651
|
+
|
|
652
|
+
// Calculate Vibe Score (semantic-free metrics)
|
|
653
|
+
const commitsWithFiles = commits as CommitWithFiles[];
|
|
654
|
+
const hasFiles = commitsWithFiles.length > 0 && 'files' in commitsWithFiles[0];
|
|
655
|
+
const vibeScore = hasFiles
|
|
656
|
+
? calculateVibeScore(commitsWithFiles)
|
|
657
|
+
: calculateVibeScore(commitsWithFiles.map(c => ({ ...c, files: [] })));
|
|
658
|
+
|
|
659
|
+
// Determine overall rating
|
|
660
|
+
const overall = calculateOverallRating([
|
|
661
|
+
iterationVelocity.rating,
|
|
662
|
+
reworkRatio.rating,
|
|
663
|
+
trustPassRate.rating,
|
|
664
|
+
debugSpiralDuration.rating,
|
|
665
|
+
flowEfficiency.rating,
|
|
666
|
+
]);
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
period: {
|
|
670
|
+
from,
|
|
671
|
+
to,
|
|
672
|
+
activeHours: Math.round(activeHours * 10) / 10,
|
|
673
|
+
},
|
|
674
|
+
commits: commitCounts,
|
|
675
|
+
metrics: {
|
|
676
|
+
iterationVelocity,
|
|
677
|
+
reworkRatio,
|
|
678
|
+
trustPassRate,
|
|
679
|
+
debugSpiralDuration,
|
|
680
|
+
flowEfficiency,
|
|
681
|
+
},
|
|
682
|
+
fixChains,
|
|
683
|
+
patterns,
|
|
684
|
+
overall,
|
|
685
|
+
vibeScore,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Also update `emptyResult` function (add vibeScore):**
|
|
691
|
+
|
|
692
|
+
**Current (lines 113-166):**
|
|
693
|
+
Find `function emptyResult()` and add vibeScore to the return.
|
|
694
|
+
|
|
695
|
+
**After (add before final closing brace):**
|
|
696
|
+
```typescript
|
|
697
|
+
vibeScore: {
|
|
698
|
+
score: 1.0,
|
|
699
|
+
components: {
|
|
700
|
+
fileChurn: 1.0,
|
|
701
|
+
timeSpiral: 1.0,
|
|
702
|
+
velocityAnomaly: 1.0,
|
|
703
|
+
codeStability: 1.0,
|
|
704
|
+
},
|
|
705
|
+
weights: [0.30, 0.25, 0.20, 0.25],
|
|
706
|
+
interpretation: 'elite',
|
|
707
|
+
},
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**Validation:** `npm run build && npm test`
|
|
711
|
+
|
|
712
|
+
---
|
|
713
|
+
|
|
714
|
+
### 6. `src/output/terminal.ts` (MODIFY - display Vibe Score)
|
|
715
|
+
|
|
716
|
+
**Purpose:** Add Vibe Score display to terminal output
|
|
717
|
+
|
|
718
|
+
**After line 50 (after overall rating display), insert:**
|
|
719
|
+
```typescript
|
|
720
|
+
// Vibe Score (semantic-free)
|
|
721
|
+
lines.push('');
|
|
722
|
+
lines.push(chalk.bold.cyan('-'.repeat(64)));
|
|
723
|
+
lines.push(` ${chalk.bold('VIBE SCORE:')} ${formatVibeScore(result.vibeScore.score)} ${formatVibeScoreRating(result.vibeScore.interpretation)}`);
|
|
724
|
+
lines.push(chalk.gray(` Components: File Churn ${(result.vibeScore.components.fileChurn * 100).toFixed(0)}% | Time Flow ${(result.vibeScore.components.timeSpiral * 100).toFixed(0)}% | Velocity ${(result.vibeScore.components.velocityAnomaly * 100).toFixed(0)}%`));
|
|
725
|
+
lines.push(chalk.bold.cyan('-'.repeat(64)));
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
**Add helper functions after `formatOverallRating`:**
|
|
729
|
+
```typescript
|
|
730
|
+
function formatVibeScore(score: number): string {
|
|
731
|
+
const percentage = (score * 100).toFixed(0);
|
|
732
|
+
if (score >= 0.85) return chalk.green.bold(`${percentage}%`);
|
|
733
|
+
if (score >= 0.70) return chalk.blue.bold(`${percentage}%`);
|
|
734
|
+
if (score >= 0.50) return chalk.yellow.bold(`${percentage}%`);
|
|
735
|
+
return chalk.red.bold(`${percentage}%`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function formatVibeScoreRating(rating: 'elite' | 'high' | 'medium' | 'low'): string {
|
|
739
|
+
switch (rating) {
|
|
740
|
+
case 'elite':
|
|
741
|
+
return chalk.green('(Elite Flow)');
|
|
742
|
+
case 'high':
|
|
743
|
+
return chalk.blue('(High Flow)');
|
|
744
|
+
case 'medium':
|
|
745
|
+
return chalk.yellow('(Moderate)');
|
|
746
|
+
case 'low':
|
|
747
|
+
return chalk.red('(Struggling)');
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Validation:** `npm run dev -- --since "1 week ago"` - should show Vibe Score
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
### 7. `src/output/json.ts` (MODIFY - include Vibe Score)
|
|
757
|
+
|
|
758
|
+
**Purpose:** Ensure vibeScore is in JSON output
|
|
759
|
+
|
|
760
|
+
The existing `formatJson` function likely just does `JSON.stringify(result)`. Since we added `vibeScore` to `VibeCheckResult`, it should automatically be included.
|
|
761
|
+
|
|
762
|
+
**Validation:** `npm run dev -- --since "1 week ago" -f json | jq .vibeScore`
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
### 8. `src/output/markdown.ts` (MODIFY - include Vibe Score)
|
|
767
|
+
|
|
768
|
+
**Purpose:** Add Vibe Score section to markdown output
|
|
769
|
+
|
|
770
|
+
**Add after overall rating section:**
|
|
771
|
+
```typescript
|
|
772
|
+
// Vibe Score
|
|
773
|
+
lines.push('');
|
|
774
|
+
lines.push('## Vibe Score (Semantic-Free)');
|
|
775
|
+
lines.push('');
|
|
776
|
+
lines.push(`**Score:** ${(result.vibeScore.score * 100).toFixed(0)}% (${result.vibeScore.interpretation})`);
|
|
777
|
+
lines.push('');
|
|
778
|
+
lines.push('| Component | Score |');
|
|
779
|
+
lines.push('|-----------|-------|');
|
|
780
|
+
lines.push(`| File Churn | ${(result.vibeScore.components.fileChurn * 100).toFixed(0)}% |`);
|
|
781
|
+
lines.push(`| Time Flow | ${(result.vibeScore.components.timeSpiral * 100).toFixed(0)}% |`);
|
|
782
|
+
lines.push(`| Velocity | ${(result.vibeScore.components.velocityAnomaly * 100).toFixed(0)}% |`);
|
|
783
|
+
lines.push(`| Code Stability | ${(result.vibeScore.components.codeStability * 100).toFixed(0)}% |`);
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**Validation:** `npm run dev -- --since "1 week ago" -f markdown`
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## Implementation Order
|
|
791
|
+
|
|
792
|
+
**CRITICAL: Sequence matters. Do not reorder.**
|
|
793
|
+
|
|
794
|
+
| Step | Action | Validation | Rollback |
|
|
795
|
+
|------|--------|------------|----------|
|
|
796
|
+
| 0 | Run baseline tests | `npm test` passes | N/A |
|
|
797
|
+
| 1 | Modify `src/types.ts` - add interfaces | `npm run build` | `git checkout src/types.ts` |
|
|
798
|
+
| 2 | Create `src/metrics/vibeScore.ts` | `npm run build` | Delete file |
|
|
799
|
+
| 3 | Modify `src/git.ts` - add files parsing | `npm run build` | `git checkout src/git.ts` |
|
|
800
|
+
| 4 | Modify `src/metrics/index.ts` - integrate | `npm run build` | `git checkout src/metrics/index.ts` |
|
|
801
|
+
| 5 | Modify `src/output/terminal.ts` | `npm run dev` | `git checkout src/output/terminal.ts` |
|
|
802
|
+
| 6 | Modify `src/output/markdown.ts` | `npm run dev -f markdown` | `git checkout src/output/markdown.ts` |
|
|
803
|
+
| 7 | Full integration test | `npm test && npm run dev` | Revert all |
|
|
804
|
+
| 8 | Commit | `git commit` | N/A |
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## Validation Strategy
|
|
809
|
+
|
|
810
|
+
### Syntax Validation
|
|
811
|
+
```bash
|
|
812
|
+
npm run build
|
|
813
|
+
# Expected: No TypeScript errors
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### Unit Test Validation
|
|
817
|
+
```bash
|
|
818
|
+
npm test
|
|
819
|
+
# Expected: All existing tests pass
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### Integration Validation
|
|
823
|
+
```bash
|
|
824
|
+
# Test on vibe-check repo itself
|
|
825
|
+
npm run dev -- --since "1 week ago"
|
|
826
|
+
# Expected: Output includes Vibe Score section
|
|
827
|
+
|
|
828
|
+
# Test JSON output
|
|
829
|
+
npm run dev -- --since "1 week ago" -f json | grep vibeScore
|
|
830
|
+
# Expected: vibeScore object present
|
|
831
|
+
|
|
832
|
+
# Test markdown output
|
|
833
|
+
npm run dev -- --since "1 week ago" -f markdown | grep "Vibe Score"
|
|
834
|
+
# Expected: Vibe Score section present
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### Manual Validation
|
|
838
|
+
```bash
|
|
839
|
+
# Test on a repo with known debug spiral
|
|
840
|
+
cd /path/to/repo-with-churn
|
|
841
|
+
npx vibe-check --since "1 week ago"
|
|
842
|
+
# Expected: File Churn score < 100% if files were touched repeatedly
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
## Rollback Procedure
|
|
848
|
+
|
|
849
|
+
**Time to rollback:** ~2 minutes
|
|
850
|
+
|
|
851
|
+
### Full Rollback
|
|
852
|
+
```bash
|
|
853
|
+
# Step 1: Reset all changes
|
|
854
|
+
git checkout src/types.ts src/git.ts src/metrics/index.ts src/output/terminal.ts src/output/markdown.ts
|
|
855
|
+
|
|
856
|
+
# Step 2: Remove new file
|
|
857
|
+
rm src/metrics/vibeScore.ts
|
|
858
|
+
|
|
859
|
+
# Step 3: Rebuild
|
|
860
|
+
npm run build
|
|
861
|
+
|
|
862
|
+
# Step 4: Verify
|
|
863
|
+
npm test
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
### Partial Rollback (keep specific changes)
|
|
867
|
+
```bash
|
|
868
|
+
# Only revert output changes
|
|
869
|
+
git checkout src/output/terminal.ts src/output/markdown.ts
|
|
870
|
+
npm run build
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Risk Assessment
|
|
876
|
+
|
|
877
|
+
### Medium Risk: Git Performance
|
|
878
|
+
- **What:** `git show` per commit may be slow for large histories
|
|
879
|
+
- **Mitigation:** Batch file fetching, limit to recent commits
|
|
880
|
+
- **Detection:** `time npm run dev` on large repo
|
|
881
|
+
- **Recovery:** Add `--no-files` flag to skip file analysis
|
|
882
|
+
|
|
883
|
+
### Low Risk: Existing Tests
|
|
884
|
+
- **What:** Changes to types might break tests
|
|
885
|
+
- **Mitigation:** Types are additive, not changing existing
|
|
886
|
+
- **Detection:** `npm test` after each change
|
|
887
|
+
- **Recovery:** Revert specific file
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## Approval Checklist
|
|
892
|
+
|
|
893
|
+
**Human must verify before /implement:**
|
|
894
|
+
|
|
895
|
+
- [ ] Every file specified precisely (file:line)
|
|
896
|
+
- [ ] All templates complete (no placeholders)
|
|
897
|
+
- [ ] Validation commands provided
|
|
898
|
+
- [ ] Rollback procedure complete
|
|
899
|
+
- [ ] Implementation order is correct
|
|
900
|
+
- [ ] Risks identified and mitigated
|
|
901
|
+
- [ ] No breaking changes to existing functionality
|
|
902
|
+
|
|
903
|
+
---
|
|
904
|
+
|
|
905
|
+
## Progress Files
|
|
906
|
+
|
|
907
|
+
### `feature-list.json`
|
|
908
|
+
|
|
909
|
+
```json
|
|
910
|
+
{
|
|
911
|
+
"project": "vibe-check",
|
|
912
|
+
"version": "1.1.0",
|
|
913
|
+
"features": [
|
|
914
|
+
{
|
|
915
|
+
"id": "vibe-score-core",
|
|
916
|
+
"name": "Vibe Score (Semantic-Free Metrics)",
|
|
917
|
+
"description": "Add 4 semantic-commit-free metrics producing composite 0-1 score",
|
|
918
|
+
"status": "pending",
|
|
919
|
+
"passes": false,
|
|
920
|
+
"files": [
|
|
921
|
+
"src/types.ts",
|
|
922
|
+
"src/git.ts",
|
|
923
|
+
"src/metrics/vibeScore.ts",
|
|
924
|
+
"src/metrics/index.ts",
|
|
925
|
+
"src/output/terminal.ts",
|
|
926
|
+
"src/output/markdown.ts"
|
|
927
|
+
],
|
|
928
|
+
"validation": "npm run build && npm test && npm run dev -- --since '1 week ago'"
|
|
929
|
+
}
|
|
930
|
+
]
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
### `claude-progress.json`
|
|
935
|
+
|
|
936
|
+
```json
|
|
937
|
+
{
|
|
938
|
+
"project": "vibe-check",
|
|
939
|
+
"current_state": {
|
|
940
|
+
"phase": "planning",
|
|
941
|
+
"working_on": "Phase 1: Core Metrics",
|
|
942
|
+
"next_steps": [
|
|
943
|
+
"Approve implementation plan",
|
|
944
|
+
"Run /implement",
|
|
945
|
+
"Validate output includes Vibe Score"
|
|
946
|
+
],
|
|
947
|
+
"blockers": []
|
|
948
|
+
},
|
|
949
|
+
"sessions": [
|
|
950
|
+
{
|
|
951
|
+
"date": "2025-11-28",
|
|
952
|
+
"summary": "Completed unified research and implementation plan for semantic-free Vibe Score"
|
|
953
|
+
}
|
|
954
|
+
]
|
|
955
|
+
}
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
## Next Step
|
|
961
|
+
|
|
962
|
+
Once approved: `/implement unified-vibe-system-plan-phase1-2025-11-28.md`
|