@boshu2/vibe-check 1.0.2 → 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.
Files changed (128) hide show
  1. package/.agents/bundles/ml-learning-loop-complete-plan-2025-11-28.md +908 -0
  2. package/.agents/bundles/unified-vibe-system-plan-phase1-2025-11-28.md +962 -0
  3. package/.agents/bundles/unified-vibe-system-research-2025-11-28.md +1003 -0
  4. package/.agents/bundles/vibe-check-ecosystem-plan-2025-11-29.md +635 -0
  5. package/.agents/bundles/vibe-check-gamification-complete-2025-11-29.md +132 -0
  6. package/.agents/bundles/vibe-score-scientific-framework-2025-11-28.md +602 -0
  7. package/.vibe-check/calibration.json +38 -0
  8. package/.vibe-check/latest.json +114 -0
  9. package/CHANGELOG.md +38 -0
  10. package/CLAUDE.md +178 -0
  11. package/README.md +185 -7
  12. package/action.yml +270 -0
  13. package/dashboard/app.js +494 -0
  14. package/dashboard/index.html +235 -0
  15. package/dashboard/styles.css +647 -0
  16. package/dist/calibration/ece.d.ts +26 -0
  17. package/dist/calibration/ece.d.ts.map +1 -0
  18. package/dist/calibration/ece.js +93 -0
  19. package/dist/calibration/ece.js.map +1 -0
  20. package/dist/calibration/index.d.ts +3 -0
  21. package/dist/calibration/index.d.ts.map +1 -0
  22. package/dist/calibration/index.js +15 -0
  23. package/dist/calibration/index.js.map +1 -0
  24. package/dist/calibration/storage.d.ts +34 -0
  25. package/dist/calibration/storage.d.ts.map +1 -0
  26. package/dist/calibration/storage.js +188 -0
  27. package/dist/calibration/storage.js.map +1 -0
  28. package/dist/cli.js +30 -76
  29. package/dist/cli.js.map +1 -1
  30. package/dist/commands/analyze.d.ts +16 -0
  31. package/dist/commands/analyze.d.ts.map +1 -0
  32. package/dist/commands/analyze.js +256 -0
  33. package/dist/commands/analyze.js.map +1 -0
  34. package/dist/commands/index.d.ts +4 -0
  35. package/dist/commands/index.d.ts.map +1 -0
  36. package/dist/commands/index.js +11 -0
  37. package/dist/commands/index.js.map +1 -0
  38. package/dist/commands/level.d.ts +3 -0
  39. package/dist/commands/level.d.ts.map +1 -0
  40. package/dist/commands/level.js +277 -0
  41. package/dist/commands/level.js.map +1 -0
  42. package/dist/commands/profile.d.ts +4 -0
  43. package/dist/commands/profile.d.ts.map +1 -0
  44. package/dist/commands/profile.js +143 -0
  45. package/dist/commands/profile.js.map +1 -0
  46. package/dist/gamification/achievements.d.ts +15 -0
  47. package/dist/gamification/achievements.d.ts.map +1 -0
  48. package/dist/gamification/achievements.js +273 -0
  49. package/dist/gamification/achievements.js.map +1 -0
  50. package/dist/gamification/index.d.ts +8 -0
  51. package/dist/gamification/index.d.ts.map +1 -0
  52. package/dist/gamification/index.js +30 -0
  53. package/dist/gamification/index.js.map +1 -0
  54. package/dist/gamification/profile.d.ts +46 -0
  55. package/dist/gamification/profile.d.ts.map +1 -0
  56. package/dist/gamification/profile.js +272 -0
  57. package/dist/gamification/profile.js.map +1 -0
  58. package/dist/gamification/streaks.d.ts +26 -0
  59. package/dist/gamification/streaks.d.ts.map +1 -0
  60. package/dist/gamification/streaks.js +132 -0
  61. package/dist/gamification/streaks.js.map +1 -0
  62. package/dist/gamification/types.d.ts +111 -0
  63. package/dist/gamification/types.d.ts.map +1 -0
  64. package/dist/gamification/types.js +26 -0
  65. package/dist/gamification/types.js.map +1 -0
  66. package/dist/gamification/xp.d.ts +37 -0
  67. package/dist/gamification/xp.d.ts.map +1 -0
  68. package/dist/gamification/xp.js +115 -0
  69. package/dist/gamification/xp.js.map +1 -0
  70. package/dist/git.d.ts +11 -0
  71. package/dist/git.d.ts.map +1 -1
  72. package/dist/git.js +52 -0
  73. package/dist/git.js.map +1 -1
  74. package/dist/metrics/code-stability.d.ts +13 -0
  75. package/dist/metrics/code-stability.d.ts.map +1 -0
  76. package/dist/metrics/code-stability.js +74 -0
  77. package/dist/metrics/code-stability.js.map +1 -0
  78. package/dist/metrics/file-churn.d.ts +8 -0
  79. package/dist/metrics/file-churn.d.ts.map +1 -0
  80. package/dist/metrics/file-churn.js +75 -0
  81. package/dist/metrics/file-churn.js.map +1 -0
  82. package/dist/metrics/time-spiral.d.ts +8 -0
  83. package/dist/metrics/time-spiral.d.ts.map +1 -0
  84. package/dist/metrics/time-spiral.js +69 -0
  85. package/dist/metrics/time-spiral.js.map +1 -0
  86. package/dist/metrics/velocity-anomaly.d.ts +13 -0
  87. package/dist/metrics/velocity-anomaly.d.ts.map +1 -0
  88. package/dist/metrics/velocity-anomaly.js +67 -0
  89. package/dist/metrics/velocity-anomaly.js.map +1 -0
  90. package/dist/output/index.d.ts +6 -3
  91. package/dist/output/index.d.ts.map +1 -1
  92. package/dist/output/index.js +4 -3
  93. package/dist/output/index.js.map +1 -1
  94. package/dist/output/json.d.ts +2 -2
  95. package/dist/output/json.d.ts.map +1 -1
  96. package/dist/output/json.js +54 -0
  97. package/dist/output/json.js.map +1 -1
  98. package/dist/output/markdown.d.ts +2 -2
  99. package/dist/output/markdown.d.ts.map +1 -1
  100. package/dist/output/markdown.js +34 -1
  101. package/dist/output/markdown.js.map +1 -1
  102. package/dist/output/terminal.d.ts +6 -2
  103. package/dist/output/terminal.d.ts.map +1 -1
  104. package/dist/output/terminal.js +131 -3
  105. package/dist/output/terminal.js.map +1 -1
  106. package/dist/recommend/index.d.ts +3 -0
  107. package/dist/recommend/index.d.ts.map +1 -0
  108. package/dist/recommend/index.js +14 -0
  109. package/dist/recommend/index.js.map +1 -0
  110. package/dist/recommend/ordered-logistic.d.ts +49 -0
  111. package/dist/recommend/ordered-logistic.d.ts.map +1 -0
  112. package/dist/recommend/ordered-logistic.js +153 -0
  113. package/dist/recommend/ordered-logistic.js.map +1 -0
  114. package/dist/recommend/questions.d.ts +19 -0
  115. package/dist/recommend/questions.d.ts.map +1 -0
  116. package/dist/recommend/questions.js +73 -0
  117. package/dist/recommend/questions.js.map +1 -0
  118. package/dist/score/index.d.ts +21 -0
  119. package/dist/score/index.d.ts.map +1 -0
  120. package/dist/score/index.js +48 -0
  121. package/dist/score/index.js.map +1 -0
  122. package/dist/score/weights.d.ts +16 -0
  123. package/dist/score/weights.d.ts.map +1 -0
  124. package/dist/score/weights.js +28 -0
  125. package/dist/score/weights.js.map +1 -0
  126. package/dist/types.d.ts +83 -0
  127. package/dist/types.d.ts.map +1 -1
  128. 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`