@boshu2/vibe-check 1.5.0 → 1.6.1

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 (148) hide show
  1. package/.agents/bundles/insight-mining-dashboard-research-2025-11-30.md +400 -0
  2. package/.agents/bundles/storage-enhancement-research-2025-11-30.md +292 -0
  3. package/.agents/bundles/timeline-feature-research-complete-2025-11-30.md +301 -0
  4. package/.agents/plans/insight-dashboard-plan-2025-11-30.md +1130 -0
  5. package/.agents/plans/json-storage-enhancement-plan.md +717 -0
  6. package/.agents/plans/storage-hardening-and-cache-plan.md +592 -0
  7. package/.agents/plans/test-coverage-gaps-plan.md +1117 -0
  8. package/.agents/plans/timeline-feature-plan.md +193 -0
  9. package/.agents/plans/vibe_timeline_research_findings.md +553 -0
  10. package/.claude/settings.local.json +1 -0
  11. package/.vibe-check/.gitignore +6 -0
  12. package/CHANGELOG.md +46 -0
  13. package/CLAUDE.md +24 -0
  14. package/CONTRIBUTING.md +227 -0
  15. package/README.md +165 -144
  16. package/claude-progress.json +191 -9
  17. package/claude-progress.txt +257 -0
  18. package/dashboard/app.js +75 -2
  19. package/dashboard/dashboard-data.json +653 -0
  20. package/dashboard/index.html +13 -0
  21. package/dashboard/styles.css +61 -0
  22. package/dist/analysis/cross-session-analysis.d.ts +68 -0
  23. package/dist/analysis/cross-session-analysis.d.ts.map +1 -0
  24. package/dist/analysis/cross-session-analysis.js +174 -0
  25. package/dist/analysis/cross-session-analysis.js.map +1 -0
  26. package/dist/analysis/index.d.ts +2 -0
  27. package/dist/analysis/index.d.ts.map +1 -0
  28. package/dist/analysis/index.js +12 -0
  29. package/dist/analysis/index.js.map +1 -0
  30. package/dist/cli.js +10 -1
  31. package/dist/cli.js.map +1 -1
  32. package/dist/commands/analyze.d.ts +2 -0
  33. package/dist/commands/analyze.d.ts.map +1 -1
  34. package/dist/commands/analyze.js +105 -2
  35. package/dist/commands/analyze.js.map +1 -1
  36. package/dist/commands/cache.d.ts +6 -0
  37. package/dist/commands/cache.d.ts.map +1 -0
  38. package/dist/commands/cache.js +168 -0
  39. package/dist/commands/cache.js.map +1 -0
  40. package/dist/commands/dashboard.d.ts +8 -0
  41. package/dist/commands/dashboard.d.ts.map +1 -0
  42. package/dist/commands/dashboard.js +109 -0
  43. package/dist/commands/dashboard.js.map +1 -0
  44. package/dist/commands/index.d.ts +3 -0
  45. package/dist/commands/index.d.ts.map +1 -1
  46. package/dist/commands/index.js +8 -1
  47. package/dist/commands/index.js.map +1 -1
  48. package/dist/commands/timeline.d.ts +14 -0
  49. package/dist/commands/timeline.d.ts.map +1 -0
  50. package/dist/commands/timeline.js +462 -0
  51. package/dist/commands/timeline.js.map +1 -0
  52. package/dist/git.d.ts +24 -0
  53. package/dist/git.d.ts.map +1 -1
  54. package/dist/git.js +94 -0
  55. package/dist/git.js.map +1 -1
  56. package/dist/insights/generators.d.ts +44 -0
  57. package/dist/insights/generators.d.ts.map +1 -0
  58. package/dist/insights/generators.js +289 -0
  59. package/dist/insights/generators.js.map +1 -0
  60. package/dist/insights/index.d.ts +16 -0
  61. package/dist/insights/index.d.ts.map +1 -0
  62. package/dist/insights/index.js +171 -0
  63. package/dist/insights/index.js.map +1 -0
  64. package/dist/insights/types.d.ts +93 -0
  65. package/dist/insights/types.d.ts.map +1 -0
  66. package/dist/insights/types.js +6 -0
  67. package/dist/insights/types.js.map +1 -0
  68. package/dist/output/timeline-html.d.ts +6 -0
  69. package/dist/output/timeline-html.d.ts.map +1 -0
  70. package/dist/output/timeline-html.js +389 -0
  71. package/dist/output/timeline-html.js.map +1 -0
  72. package/dist/output/timeline-markdown.d.ts +6 -0
  73. package/dist/output/timeline-markdown.d.ts.map +1 -0
  74. package/dist/output/timeline-markdown.js +167 -0
  75. package/dist/output/timeline-markdown.js.map +1 -0
  76. package/dist/output/timeline.d.ts +9 -0
  77. package/dist/output/timeline.d.ts.map +1 -0
  78. package/dist/output/timeline.js +318 -0
  79. package/dist/output/timeline.js.map +1 -0
  80. package/dist/patterns/detour.d.ts +32 -0
  81. package/dist/patterns/detour.d.ts.map +1 -0
  82. package/dist/patterns/detour.js +137 -0
  83. package/dist/patterns/detour.js.map +1 -0
  84. package/dist/patterns/flow-state.d.ts +16 -0
  85. package/dist/patterns/flow-state.d.ts.map +1 -0
  86. package/dist/patterns/flow-state.js +40 -0
  87. package/dist/patterns/flow-state.js.map +1 -0
  88. package/dist/patterns/index.d.ts +8 -0
  89. package/dist/patterns/index.d.ts.map +1 -0
  90. package/dist/patterns/index.js +22 -0
  91. package/dist/patterns/index.js.map +1 -0
  92. package/dist/patterns/intervention-effectiveness.d.ts +42 -0
  93. package/dist/patterns/intervention-effectiveness.d.ts.map +1 -0
  94. package/dist/patterns/intervention-effectiveness.js +196 -0
  95. package/dist/patterns/intervention-effectiveness.js.map +1 -0
  96. package/dist/patterns/late-night.d.ts +30 -0
  97. package/dist/patterns/late-night.d.ts.map +1 -0
  98. package/dist/patterns/late-night.js +141 -0
  99. package/dist/patterns/late-night.js.map +1 -0
  100. package/dist/patterns/post-delete-sprint.d.ts +28 -0
  101. package/dist/patterns/post-delete-sprint.d.ts.map +1 -0
  102. package/dist/patterns/post-delete-sprint.js +85 -0
  103. package/dist/patterns/post-delete-sprint.js.map +1 -0
  104. package/dist/patterns/spiral-regression.d.ts +49 -0
  105. package/dist/patterns/spiral-regression.d.ts.map +1 -0
  106. package/dist/patterns/spiral-regression.js +219 -0
  107. package/dist/patterns/spiral-regression.js.map +1 -0
  108. package/dist/patterns/thrashing.d.ts +25 -0
  109. package/dist/patterns/thrashing.d.ts.map +1 -0
  110. package/dist/patterns/thrashing.js +111 -0
  111. package/dist/patterns/thrashing.js.map +1 -0
  112. package/dist/storage/atomic.d.ts +40 -0
  113. package/dist/storage/atomic.d.ts.map +1 -0
  114. package/dist/storage/atomic.js +155 -0
  115. package/dist/storage/atomic.js.map +1 -0
  116. package/dist/storage/commit-log.d.ts +35 -0
  117. package/dist/storage/commit-log.d.ts.map +1 -0
  118. package/dist/storage/commit-log.js +128 -0
  119. package/dist/storage/commit-log.js.map +1 -0
  120. package/dist/storage/index.d.ts +5 -0
  121. package/dist/storage/index.d.ts.map +1 -0
  122. package/dist/storage/index.js +33 -0
  123. package/dist/storage/index.js.map +1 -0
  124. package/dist/storage/schema.d.ts +32 -0
  125. package/dist/storage/schema.d.ts.map +1 -0
  126. package/dist/storage/schema.js +37 -0
  127. package/dist/storage/schema.js.map +1 -0
  128. package/dist/storage/timeline-store.d.ts +117 -0
  129. package/dist/storage/timeline-store.d.ts.map +1 -0
  130. package/dist/storage/timeline-store.js +438 -0
  131. package/dist/storage/timeline-store.js.map +1 -0
  132. package/dist/types.d.ts +96 -0
  133. package/dist/types.d.ts.map +1 -1
  134. package/docs/ARCHITECTURE.md +458 -0
  135. package/docs/DATA-ARCHITECTURE.md +565 -0
  136. package/docs/GAMIFICATION.md +564 -0
  137. package/docs/JSON-STORAGE-PATTERNS.md +512 -0
  138. package/docs/METRICS-EXPLAINED.md +394 -0
  139. package/docs/UNIFIED-ECOSYSTEM.md +560 -0
  140. package/docs/VIBE-ECOSYSTEM.md +406 -0
  141. package/docs/images/dashboard.png +0 -0
  142. package/feature-list.json +48 -0
  143. package/package.json +2 -1
  144. package/vitest.config.ts +1 -5
  145. package/.vibe-check/calibration.json +0 -38
  146. package/.vibe-check/latest.json +0 -114
  147. package/.vibe-check/sessions.json +0 -44
  148. package/PLAN-ultimate-game.md +0 -1362
@@ -0,0 +1,1130 @@
1
+ # Insight Mining & Dashboard Implementation Plan
2
+
3
+ **Type:** Plan
4
+ **Created:** 2025-11-30
5
+ **Depends On:** insight-mining-dashboard-research-2025-11-30.md
6
+ **Loop:** Middle (bridges research to implementation)
7
+ **Tags:** dashboard, insights, visualization
8
+
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ Create an insight engine that generates prioritized actionable insights, then connect the existing dashboard to real data via a `vibe-check dashboard` command.
14
+
15
+ ## Approach Selected
16
+
17
+ **Hybrid Architecture (Option D from research):**
18
+ 1. Insight engine module generates insights from all data sources
19
+ 2. Dashboard command exports JSON + opens browser
20
+ 3. Static dashboard reads JSON file
21
+
22
+ ## PDC Strategy
23
+
24
+ ### Prevent
25
+ - [x] Research bundle completed
26
+ - [ ] Test JSON loading in browser before full implementation
27
+
28
+ ### Detect
29
+ - [ ] Validate each insight generator produces valid output
30
+ - [ ] Test dashboard renders with real data
31
+
32
+ ### Correct
33
+ - [ ] Rollback: delete new files, revert dashboard/app.js
34
+
35
+ ---
36
+
37
+ ## Files to Create
38
+
39
+ ### 1. `src/insights/types.ts`
40
+
41
+ **Purpose:** Define insight interfaces and categories
42
+
43
+ ```typescript
44
+ /**
45
+ * Insight types for vibe-check dashboard
46
+ */
47
+
48
+ export type InsightCategory =
49
+ | 'productivity' // Peak hours, best days
50
+ | 'patterns' // Problematic scopes, spiral triggers
51
+ | 'growth' // Improvement streaks, trends
52
+ | 'warning' // Regression alerts, risks
53
+ | 'celebration'; // Personal bests, achievements
54
+
55
+ export type InsightSeverity = 'info' | 'warning' | 'critical' | 'success';
56
+
57
+ export interface Insight {
58
+ id: string;
59
+ category: InsightCategory;
60
+ severity: InsightSeverity;
61
+ icon: string;
62
+ title: string;
63
+ message: string;
64
+ metric?: string;
65
+ value?: number;
66
+ comparison?: {
67
+ type: 'baseline' | 'previous' | 'goal';
68
+ label: string;
69
+ value: number;
70
+ change: number;
71
+ };
72
+ action?: string;
73
+ source: string;
74
+ priority: number; // 1-10, higher = more important
75
+ }
76
+
77
+ export interface DashboardData {
78
+ version: string;
79
+ generatedAt: string;
80
+ repo: string;
81
+
82
+ profile: {
83
+ level: number;
84
+ levelName: string;
85
+ levelIcon: string;
86
+ xp: { current: number; next: number; total: number };
87
+ streak: { current: number; longest: number };
88
+ achievementCount: number;
89
+ totalAchievements: number;
90
+ };
91
+
92
+ stats: {
93
+ current: { vibeScore: number; rating: string };
94
+ averages: { day7: number; day30: number; allTime: number };
95
+ totals: { sessions: number; commits: number; spirals: number; features: number };
96
+ };
97
+
98
+ charts: {
99
+ scoreTrend: Array<{ date: string; score: number; rating: string }>;
100
+ ratingDistribution: Record<string, number>;
101
+ hourlyActivity: Record<string, number>;
102
+ scopeHealth: Array<{ scope: string; commits: number; fixRatio: number }>;
103
+ };
104
+
105
+ insights: Insight[];
106
+
107
+ sessions: Array<{
108
+ date: string;
109
+ vibeScore: number;
110
+ rating: string;
111
+ commits: number;
112
+ spirals: number;
113
+ xpEarned: number;
114
+ }>;
115
+
116
+ achievements: Array<{
117
+ id: string;
118
+ name: string;
119
+ icon: string;
120
+ description: string;
121
+ unlockedAt?: string;
122
+ }>;
123
+ }
124
+ ```
125
+
126
+ **Validation:** `npm run build` passes
127
+
128
+ ---
129
+
130
+ ### 2. `src/insights/generators.ts`
131
+
132
+ **Purpose:** Individual insight generation functions
133
+
134
+ ```typescript
135
+ /**
136
+ * Insight generators - each produces insights from specific data sources
137
+ */
138
+
139
+ import { Insight } from './types';
140
+ import { UserProfile } from '../gamification/types';
141
+ import { TimelineStore } from '../storage/timeline-store';
142
+ import { Commit } from '../types';
143
+
144
+ /**
145
+ * Find peak productivity hours from commits
146
+ */
147
+ export function generatePeakHoursInsight(commits: Commit[]): Insight | null {
148
+ if (commits.length < 10) return null;
149
+
150
+ const hourCounts: Record<number, number> = {};
151
+ for (const commit of commits) {
152
+ const hour = commit.date.getHours();
153
+ hourCounts[hour] = (hourCounts[hour] || 0) + 1;
154
+ }
155
+
156
+ const sorted = Object.entries(hourCounts)
157
+ .map(([h, c]) => ({ hour: parseInt(h), count: c }))
158
+ .sort((a, b) => b.count - a.count);
159
+
160
+ if (sorted.length === 0) return null;
161
+
162
+ const peakHour = sorted[0].hour;
163
+ const peakPct = Math.round((sorted[0].count / commits.length) * 100);
164
+ const hourStr = peakHour < 12 ? `${peakHour}am` : peakHour === 12 ? '12pm' : `${peakHour - 12}pm`;
165
+
166
+ return {
167
+ id: 'peak-hours',
168
+ category: 'productivity',
169
+ severity: 'info',
170
+ icon: '⏰',
171
+ title: 'Peak Productivity',
172
+ message: `You're most productive around ${hourStr} (${peakPct}% of commits)`,
173
+ metric: 'peak_hour',
174
+ value: peakHour,
175
+ action: 'Protect this time for deep work',
176
+ source: 'commits',
177
+ priority: 5,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Detect improvement streak from trends
183
+ */
184
+ export function generateImprovementStreakInsight(store: TimelineStore): Insight | null {
185
+ const weeks = store.trends.weekly;
186
+ if (weeks.length < 2) return null;
187
+
188
+ let streak = 0;
189
+ for (let i = weeks.length - 2; i >= 0; i--) {
190
+ const current = weeks[i];
191
+ const next = weeks[i + 1];
192
+ const currentRate = current.sessions > 0 ? current.spirals / current.sessions : 0;
193
+ const nextRate = next.sessions > 0 ? next.spirals / next.sessions : 0;
194
+ if (nextRate <= currentRate) {
195
+ streak++;
196
+ } else {
197
+ break;
198
+ }
199
+ }
200
+
201
+ if (streak < 2) return null;
202
+
203
+ return {
204
+ id: 'improvement-streak',
205
+ category: 'growth',
206
+ severity: 'success',
207
+ icon: '🎯',
208
+ title: 'Improvement Streak',
209
+ message: `${streak}-week improvement streak! Your spiral rate keeps dropping.`,
210
+ metric: 'improvement_weeks',
211
+ value: streak,
212
+ source: 'timeline.trends',
213
+ priority: 7,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Find problematic scopes with high fix ratios
219
+ */
220
+ export function generateProblematicScopesInsight(commits: Commit[]): Insight | null {
221
+ const scopeStats = new Map<string, { total: number; fixes: number }>();
222
+
223
+ for (const commit of commits) {
224
+ const scope = commit.scope || '(no scope)';
225
+ if (!scopeStats.has(scope)) {
226
+ scopeStats.set(scope, { total: 0, fixes: 0 });
227
+ }
228
+ const stats = scopeStats.get(scope)!;
229
+ stats.total++;
230
+ if (commit.type === 'fix') stats.fixes++;
231
+ }
232
+
233
+ const problematic = Array.from(scopeStats.entries())
234
+ .map(([scope, stats]) => ({
235
+ scope,
236
+ ...stats,
237
+ ratio: stats.total > 0 ? stats.fixes / stats.total : 0,
238
+ }))
239
+ .filter(s => s.total >= 3 && s.ratio >= 0.5)
240
+ .sort((a, b) => b.ratio - a.ratio);
241
+
242
+ if (problematic.length === 0) return null;
243
+
244
+ const worst = problematic[0];
245
+ const pct = Math.round(worst.ratio * 100);
246
+
247
+ return {
248
+ id: 'problematic-scope',
249
+ category: 'warning',
250
+ severity: 'warning',
251
+ icon: '⚠️',
252
+ title: 'High-Risk Scope',
253
+ message: `"${worst.scope}" has ${pct}% fix commits (${worst.fixes}/${worst.total})`,
254
+ metric: 'scope_fix_ratio',
255
+ value: worst.ratio,
256
+ action: 'Consider adding tracer tests for this area',
257
+ source: 'commits',
258
+ priority: 8,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Check for streak at risk
264
+ */
265
+ export function generateStreakRiskInsight(profile: UserProfile): Insight | null {
266
+ const { streak } = profile;
267
+ if (streak.current < 3) return null;
268
+
269
+ const today = new Date().toISOString().split('T')[0];
270
+ if (streak.lastActiveDate === today) return null;
271
+
272
+ const lastActive = new Date(streak.lastActiveDate);
273
+ const now = new Date();
274
+ const daysSince = Math.floor((now.getTime() - lastActive.getTime()) / (1000 * 60 * 60 * 24));
275
+
276
+ if (daysSince === 0) return null;
277
+
278
+ return {
279
+ id: 'streak-risk',
280
+ category: 'warning',
281
+ severity: daysSince >= 1 ? 'warning' : 'info',
282
+ icon: '🔥',
283
+ title: 'Streak at Risk',
284
+ message: `Your ${streak.current}-day streak needs a session today!`,
285
+ action: 'Run vibe-check --score to maintain streak',
286
+ source: 'profile',
287
+ priority: 9,
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Celebrate personal best
293
+ */
294
+ export function generatePersonalBestInsight(profile: UserProfile): Insight | null {
295
+ const sessions = profile.sessions;
296
+ if (sessions.length < 2) return null;
297
+
298
+ const latest = sessions[0];
299
+ const previousBest = Math.max(...sessions.slice(1).map(s => s.vibeScore));
300
+
301
+ if (latest.vibeScore > previousBest) {
302
+ return {
303
+ id: 'personal-best',
304
+ category: 'celebration',
305
+ severity: 'success',
306
+ icon: '🏆',
307
+ title: 'New Personal Best!',
308
+ message: `${latest.vibeScore}% beats your previous best of ${previousBest}%`,
309
+ metric: 'vibe_score',
310
+ value: latest.vibeScore,
311
+ comparison: {
312
+ type: 'previous',
313
+ label: 'Previous best',
314
+ value: previousBest,
315
+ change: latest.vibeScore - previousBest,
316
+ },
317
+ source: 'profile',
318
+ priority: 10,
319
+ };
320
+ }
321
+
322
+ return null;
323
+ }
324
+
325
+ /**
326
+ * Show level progress
327
+ */
328
+ export function generateLevelProgressInsight(profile: UserProfile): Insight | null {
329
+ const { xp } = profile;
330
+ const progress = Math.round((xp.currentLevelXP / xp.nextLevelXP) * 100);
331
+
332
+ if (progress >= 80) {
333
+ return {
334
+ id: 'level-close',
335
+ category: 'growth',
336
+ severity: 'info',
337
+ icon: '📈',
338
+ title: 'Level Up Soon!',
339
+ message: `${progress}% to Level ${xp.level + 1} (${xp.nextLevelXP - xp.currentLevelXP} XP to go)`,
340
+ metric: 'level_progress',
341
+ value: progress,
342
+ source: 'profile',
343
+ priority: 6,
344
+ };
345
+ }
346
+
347
+ return null;
348
+ }
349
+
350
+ /**
351
+ * Late night warning
352
+ */
353
+ export function generateLateNightInsight(commits: Commit[]): Insight | null {
354
+ const recentCommits = commits.slice(0, 20);
355
+ const lateNight = recentCommits.filter(c => {
356
+ const hour = c.date.getHours();
357
+ return hour >= 23 || hour < 5;
358
+ });
359
+
360
+ if (lateNight.length < 3) return null;
361
+
362
+ const pct = Math.round((lateNight.length / recentCommits.length) * 100);
363
+
364
+ return {
365
+ id: 'late-night',
366
+ category: 'warning',
367
+ severity: 'warning',
368
+ icon: '🌙',
369
+ title: 'Late Night Sessions',
370
+ message: `${pct}% of recent commits are between 11pm-5am`,
371
+ action: 'Late night coding correlates with more spirals',
372
+ source: 'commits',
373
+ priority: 7,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Recent achievement
379
+ */
380
+ export function generateRecentAchievementInsight(profile: UserProfile): Insight | null {
381
+ const recent = profile.achievements
382
+ .filter(a => a.unlockedAt)
383
+ .sort((a, b) => new Date(b.unlockedAt!).getTime() - new Date(a.unlockedAt!).getTime())
384
+ .slice(0, 1);
385
+
386
+ if (recent.length === 0) return null;
387
+
388
+ const ach = recent[0];
389
+ const unlockedDate = new Date(ach.unlockedAt!);
390
+ const daysSince = Math.floor((Date.now() - unlockedDate.getTime()) / (1000 * 60 * 60 * 24));
391
+
392
+ if (daysSince > 7) return null;
393
+
394
+ return {
395
+ id: 'recent-achievement',
396
+ category: 'celebration',
397
+ severity: 'success',
398
+ icon: ach.icon,
399
+ title: 'Achievement Unlocked!',
400
+ message: `${ach.name}: ${ach.description}`,
401
+ source: 'profile',
402
+ priority: 8,
403
+ };
404
+ }
405
+ ```
406
+
407
+ **Validation:** `npm run build` passes
408
+
409
+ ---
410
+
411
+ ### 3. `src/insights/index.ts`
412
+
413
+ **Purpose:** Main insight engine - aggregates all generators
414
+
415
+ ```typescript
416
+ /**
417
+ * Insight Engine - generates prioritized insights from all data sources
418
+ */
419
+
420
+ import { Insight, DashboardData } from './types';
421
+ import {
422
+ generatePeakHoursInsight,
423
+ generateImprovementStreakInsight,
424
+ generateProblematicScopesInsight,
425
+ generateStreakRiskInsight,
426
+ generatePersonalBestInsight,
427
+ generateLevelProgressInsight,
428
+ generateLateNightInsight,
429
+ generateRecentAchievementInsight,
430
+ } from './generators';
431
+ import { UserProfile, LEVELS } from '../gamification/types';
432
+ import { loadStore, readCommitLog } from '../storage';
433
+ import { loadProfile } from '../gamification/profile';
434
+ import { ACHIEVEMENTS } from '../gamification/achievements';
435
+
436
+ export { Insight, DashboardData } from './types';
437
+
438
+ /**
439
+ * Generate all insights from available data
440
+ */
441
+ export function generateInsights(
442
+ profile: UserProfile,
443
+ commits: ReturnType<typeof readCommitLog>,
444
+ repoPath: string
445
+ ): Insight[] {
446
+ const store = loadStore(repoPath);
447
+ const insights: Insight[] = [];
448
+
449
+ // Run all generators
450
+ const generators = [
451
+ () => generatePeakHoursInsight(commits),
452
+ () => generateImprovementStreakInsight(store),
453
+ () => generateProblematicScopesInsight(commits),
454
+ () => generateStreakRiskInsight(profile),
455
+ () => generatePersonalBestInsight(profile),
456
+ () => generateLevelProgressInsight(profile),
457
+ () => generateLateNightInsight(commits),
458
+ () => generateRecentAchievementInsight(profile),
459
+ ];
460
+
461
+ for (const gen of generators) {
462
+ try {
463
+ const insight = gen();
464
+ if (insight) insights.push(insight);
465
+ } catch {
466
+ // Skip failed generators
467
+ }
468
+ }
469
+
470
+ // Sort by priority (highest first)
471
+ return insights.sort((a, b) => b.priority - a.priority);
472
+ }
473
+
474
+ /**
475
+ * Build complete dashboard data export
476
+ */
477
+ export function buildDashboardData(repoPath: string = process.cwd()): DashboardData {
478
+ const profile = loadProfile();
479
+ const commits = readCommitLog(repoPath);
480
+ const store = loadStore(repoPath);
481
+
482
+ // Level info
483
+ const levelInfo = LEVELS.find(l => l.level === profile.xp.level) || LEVELS[0];
484
+
485
+ // Calculate averages
486
+ const sessions = profile.sessions;
487
+ const day7Sessions = sessions.filter(s => {
488
+ const d = new Date(s.date);
489
+ return Date.now() - d.getTime() < 7 * 24 * 60 * 60 * 1000;
490
+ });
491
+ const day30Sessions = sessions.filter(s => {
492
+ const d = new Date(s.date);
493
+ return Date.now() - d.getTime() < 30 * 24 * 60 * 60 * 1000;
494
+ });
495
+
496
+ const avg = (arr: number[]) => arr.length > 0 ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) : 0;
497
+
498
+ // Rating distribution
499
+ const ratingDistribution: Record<string, number> = { ELITE: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
500
+ for (const s of sessions) {
501
+ ratingDistribution[s.overall] = (ratingDistribution[s.overall] || 0) + 1;
502
+ }
503
+
504
+ // Hourly activity from commits
505
+ const hourlyActivity: Record<string, number> = {};
506
+ for (const commit of commits) {
507
+ const hour = commit.date.getHours().toString();
508
+ hourlyActivity[hour] = (hourlyActivity[hour] || 0) + 1;
509
+ }
510
+
511
+ // Scope health
512
+ const scopeStats = new Map<string, { total: number; fixes: number }>();
513
+ for (const commit of commits) {
514
+ const scope = commit.scope || '(no scope)';
515
+ if (!scopeStats.has(scope)) scopeStats.set(scope, { total: 0, fixes: 0 });
516
+ const stats = scopeStats.get(scope)!;
517
+ stats.total++;
518
+ if (commit.type === 'fix') stats.fixes++;
519
+ }
520
+ const scopeHealth = Array.from(scopeStats.entries())
521
+ .map(([scope, stats]) => ({
522
+ scope,
523
+ commits: stats.total,
524
+ fixRatio: stats.total > 0 ? Math.round((stats.fixes / stats.total) * 100) : 0,
525
+ }))
526
+ .sort((a, b) => b.commits - a.commits)
527
+ .slice(0, 10);
528
+
529
+ // Score trend (last 30 sessions)
530
+ const scoreTrend = sessions.slice(0, 30).reverse().map(s => ({
531
+ date: s.date,
532
+ score: s.vibeScore,
533
+ rating: s.overall,
534
+ }));
535
+
536
+ // Generate insights
537
+ const insights = generateInsights(profile, commits, repoPath);
538
+
539
+ // Map achievements
540
+ const allAchievements = ACHIEVEMENTS.map(a => {
541
+ const unlocked = profile.achievements.find(ua => ua.id === a.id);
542
+ return {
543
+ id: a.id,
544
+ name: a.name,
545
+ icon: a.icon,
546
+ description: a.description,
547
+ unlockedAt: unlocked?.unlockedAt,
548
+ };
549
+ });
550
+
551
+ return {
552
+ version: '1.0.0',
553
+ generatedAt: new Date().toISOString(),
554
+ repo: repoPath,
555
+
556
+ profile: {
557
+ level: profile.xp.level,
558
+ levelName: profile.xp.levelName,
559
+ levelIcon: levelInfo.icon,
560
+ xp: {
561
+ current: profile.xp.currentLevelXP,
562
+ next: profile.xp.nextLevelXP,
563
+ total: profile.xp.total,
564
+ },
565
+ streak: {
566
+ current: profile.streak.current,
567
+ longest: profile.streak.longest,
568
+ },
569
+ achievementCount: profile.achievements.length,
570
+ totalAchievements: ACHIEVEMENTS.length,
571
+ },
572
+
573
+ stats: {
574
+ current: {
575
+ vibeScore: sessions[0]?.vibeScore || 0,
576
+ rating: sessions[0]?.overall || 'N/A',
577
+ },
578
+ averages: {
579
+ day7: avg(day7Sessions.map(s => s.vibeScore)),
580
+ day30: avg(day30Sessions.map(s => s.vibeScore)),
581
+ allTime: avg(sessions.map(s => s.vibeScore)),
582
+ },
583
+ totals: {
584
+ sessions: sessions.length,
585
+ commits: commits.length,
586
+ spirals: sessions.reduce((sum, s) => sum + s.spirals, 0),
587
+ features: store.sessions.reduce((sum, s) => sum + s.commitCount, 0),
588
+ },
589
+ },
590
+
591
+ charts: {
592
+ scoreTrend,
593
+ ratingDistribution,
594
+ hourlyActivity,
595
+ scopeHealth,
596
+ },
597
+
598
+ insights,
599
+
600
+ sessions: sessions.slice(0, 50).map(s => ({
601
+ date: s.date,
602
+ vibeScore: s.vibeScore,
603
+ rating: s.overall,
604
+ commits: s.commits,
605
+ spirals: s.spirals,
606
+ xpEarned: s.xpEarned,
607
+ })),
608
+
609
+ achievements: allAchievements,
610
+ };
611
+ }
612
+ ```
613
+
614
+ **Validation:** `npm run build` passes
615
+
616
+ ---
617
+
618
+ ### 4. `src/commands/dashboard.ts`
619
+
620
+ **Purpose:** CLI command to export data and open dashboard
621
+
622
+ ```typescript
623
+ import { Command } from 'commander';
624
+ import chalk from 'chalk';
625
+ import * as fs from 'fs';
626
+ import * as path from 'path';
627
+ import { exec } from 'child_process';
628
+ import { buildDashboardData } from '../insights';
629
+
630
+ export interface DashboardOptions {
631
+ repo: string;
632
+ open: boolean;
633
+ output?: string;
634
+ }
635
+
636
+ export function createDashboardCommand(): Command {
637
+ const cmd = new Command('dashboard')
638
+ .description('Open the vibe-check dashboard with your stats')
639
+ .option('-r, --repo <path>', 'Repository path', process.cwd())
640
+ .option('--no-open', 'Export data without opening browser')
641
+ .option('-o, --output <file>', 'Custom output path for dashboard-data.json')
642
+ .action(async (options) => {
643
+ await runDashboard(options);
644
+ });
645
+
646
+ return cmd;
647
+ }
648
+
649
+ async function runDashboard(options: DashboardOptions): Promise<void> {
650
+ const { repo, open, output } = options;
651
+
652
+ console.log(chalk.cyan('Building dashboard data...'));
653
+
654
+ try {
655
+ // Build dashboard data
656
+ const data = buildDashboardData(repo);
657
+
658
+ // Determine output path
659
+ const dashboardDir = path.join(__dirname, '../../dashboard');
660
+ const outputPath = output || path.join(dashboardDir, 'dashboard-data.json');
661
+
662
+ // Ensure dashboard directory exists
663
+ if (!fs.existsSync(dashboardDir)) {
664
+ console.error(chalk.red('Dashboard directory not found'));
665
+ process.exit(1);
666
+ }
667
+
668
+ // Write data
669
+ fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
670
+ console.log(chalk.green(`Data exported to ${outputPath}`));
671
+
672
+ // Show summary
673
+ console.log('');
674
+ console.log(chalk.bold('Dashboard Summary:'));
675
+ console.log(` Level: ${data.profile.levelIcon} ${data.profile.level} ${data.profile.levelName}`);
676
+ console.log(` Streak: 🔥 ${data.profile.streak.current} days`);
677
+ console.log(` Sessions: ${data.stats.totals.sessions}`);
678
+ console.log(` Insights: ${data.insights.length} generated`);
679
+ console.log('');
680
+
681
+ // Open browser
682
+ if (open) {
683
+ const htmlPath = path.join(dashboardDir, 'index.html');
684
+ const url = `file://${htmlPath}`;
685
+
686
+ console.log(chalk.cyan(`Opening dashboard...`));
687
+
688
+ // Cross-platform open command
689
+ const openCmd = process.platform === 'darwin' ? 'open' :
690
+ process.platform === 'win32' ? 'start' : 'xdg-open';
691
+
692
+ exec(`${openCmd} "${url}"`, (error) => {
693
+ if (error) {
694
+ console.log(chalk.yellow(`Could not open browser automatically.`));
695
+ console.log(chalk.gray(`Open manually: ${url}`));
696
+ }
697
+ });
698
+ } else {
699
+ const htmlPath = path.join(dashboardDir, 'index.html');
700
+ console.log(chalk.gray(`Open dashboard: file://${htmlPath}`));
701
+ }
702
+ } catch (error) {
703
+ if (error instanceof Error) {
704
+ console.error(chalk.red(`Error: ${error.message}`));
705
+ }
706
+ process.exit(1);
707
+ }
708
+ }
709
+ ```
710
+
711
+ **Validation:** `npm run build` passes, `node dist/cli.js dashboard --help` shows options
712
+
713
+ ---
714
+
715
+ ## Files to Modify
716
+
717
+ ### 1. `src/commands/index.ts:8`
718
+
719
+ **Purpose:** Export dashboard command
720
+
721
+ **Before:**
722
+ ```typescript
723
+ export { createTimelineCommand, runTimeline, TimelineOptions } from './timeline';
724
+ export { createCacheCommand } from './cache';
725
+ ```
726
+
727
+ **After:**
728
+ ```typescript
729
+ export { createTimelineCommand, runTimeline, TimelineOptions } from './timeline';
730
+ export { createCacheCommand } from './cache';
731
+ export { createDashboardCommand } from './dashboard';
732
+ ```
733
+
734
+ **Validation:** `npm run build` passes
735
+
736
+ ---
737
+
738
+ ### 2. `src/cli.ts:3`
739
+
740
+ **Purpose:** Import dashboard command
741
+
742
+ **Before:**
743
+ ```typescript
744
+ import { createAnalyzeCommand, createStartCommand, createProfileCommand, createInitHookCommand, createWatchCommand, createInterveneCommand, createTimelineCommand, createCacheCommand, runAnalyze } from './commands';
745
+ ```
746
+
747
+ **After:**
748
+ ```typescript
749
+ import { createAnalyzeCommand, createStartCommand, createProfileCommand, createInitHookCommand, createWatchCommand, createInterveneCommand, createTimelineCommand, createCacheCommand, createDashboardCommand, runAnalyze } from './commands';
750
+ ```
751
+
752
+ **Validation:** `npm run build` passes
753
+
754
+ ---
755
+
756
+ ### 3. `src/cli.ts:27`
757
+
758
+ **Purpose:** Register dashboard command
759
+
760
+ **Before:**
761
+ ```typescript
762
+ program.addCommand(createCacheCommand());
763
+
764
+ // Default behavior: if no subcommand, run analyze with passed options
765
+ ```
766
+
767
+ **After:**
768
+ ```typescript
769
+ program.addCommand(createCacheCommand());
770
+ program.addCommand(createDashboardCommand());
771
+
772
+ // Default behavior: if no subcommand, run analyze with passed options
773
+ ```
774
+
775
+ **Validation:** `node dist/cli.js --help` shows dashboard command
776
+
777
+ ---
778
+
779
+ ### 4. `dashboard/app.js:59-68`
780
+
781
+ **Purpose:** Load real data instead of mock
782
+
783
+ **Before:**
784
+ ```javascript
785
+ async loadProfile() {
786
+ // In a real implementation, this would fetch from an API or local file
787
+ // For now, we'll use mock data or localStorage
788
+ const stored = localStorage.getItem('vibe-check-profile');
789
+ if (stored) {
790
+ this.profile = JSON.parse(stored);
791
+ } else {
792
+ this.profile = this.getMockProfile();
793
+ }
794
+ }
795
+ ```
796
+
797
+ **After:**
798
+ ```javascript
799
+ async loadProfile() {
800
+ // Try to load dashboard-data.json (generated by vibe-check dashboard)
801
+ try {
802
+ const response = await fetch('dashboard-data.json');
803
+ if (response.ok) {
804
+ const data = await response.json();
805
+ this.dashboardData = data;
806
+ this.profile = this.transformToProfile(data);
807
+ return;
808
+ }
809
+ } catch (e) {
810
+ console.log('No dashboard-data.json found, using mock data');
811
+ }
812
+
813
+ // Fall back to localStorage or mock
814
+ const stored = localStorage.getItem('vibe-check-profile');
815
+ if (stored) {
816
+ this.profile = JSON.parse(stored);
817
+ } else {
818
+ this.profile = this.getMockProfile();
819
+ }
820
+ }
821
+
822
+ transformToProfile(data) {
823
+ // Transform DashboardData to legacy profile format for existing UI
824
+ return {
825
+ version: data.version,
826
+ xp: {
827
+ total: data.profile.xp.total,
828
+ level: data.profile.level,
829
+ levelName: data.profile.levelName,
830
+ currentLevelXP: data.profile.xp.current,
831
+ nextLevelXP: data.profile.xp.next,
832
+ },
833
+ streak: data.profile.streak,
834
+ achievements: data.achievements.filter(a => a.unlockedAt),
835
+ sessions: data.sessions,
836
+ stats: {
837
+ totalSessions: data.stats.totals.sessions,
838
+ totalCommitsAnalyzed: data.stats.totals.commits,
839
+ avgVibeScore: data.stats.averages.allTime,
840
+ bestVibeScore: Math.max(...data.sessions.map(s => s.vibeScore), 0),
841
+ spiralsAvoided: data.sessions.filter(s => s.spirals === 0).length,
842
+ },
843
+ };
844
+ }
845
+ ```
846
+
847
+ **Validation:** Dashboard loads without errors when dashboard-data.json exists
848
+
849
+ ---
850
+
851
+ ### 5. `dashboard/index.html:166` (after recent sessions section)
852
+
853
+ **Purpose:** Add insights section to dashboard
854
+
855
+ **Before:**
856
+ ```html
857
+ </section>
858
+
859
+ <!-- History Page -->
860
+ ```
861
+
862
+ **After:**
863
+ ```html
864
+ <!-- Insights Section -->
865
+ <div class="recent-section" id="insightsSection">
866
+ <div class="section-header">
867
+ <h3>Insights</h3>
868
+ </div>
869
+ <div class="insights-list" id="insightsList">
870
+ <div class="empty-state">
871
+ <span class="empty-icon">💡</span>
872
+ <p>Run <code>vibe-check dashboard</code> to generate insights</p>
873
+ </div>
874
+ </div>
875
+ </div>
876
+ </section>
877
+
878
+ <!-- History Page -->
879
+ ```
880
+
881
+ **Validation:** HTML loads without errors
882
+
883
+ ---
884
+
885
+ ### 6. `dashboard/app.js:191` (after renderRecentSessions)
886
+
887
+ **Purpose:** Add insight rendering function
888
+
889
+ **Before:**
890
+ ```javascript
891
+ initCharts() {
892
+ ```
893
+
894
+ **After:**
895
+ ```javascript
896
+ renderInsights() {
897
+ const container = document.getElementById('insightsList');
898
+ if (!this.dashboardData?.insights?.length) {
899
+ container.innerHTML = `
900
+ <div class="empty-state">
901
+ <span class="empty-icon">💡</span>
902
+ <p>Run <code>vibe-check dashboard</code> to generate insights</p>
903
+ </div>
904
+ `;
905
+ return;
906
+ }
907
+
908
+ const insights = this.dashboardData.insights.slice(0, 5);
909
+ container.innerHTML = insights.map(insight => {
910
+ const severityClass = {
911
+ success: 'insight-success',
912
+ warning: 'insight-warning',
913
+ critical: 'insight-critical',
914
+ info: 'insight-info',
915
+ }[insight.severity] || 'insight-info';
916
+
917
+ return `
918
+ <div class="insight-item ${severityClass}">
919
+ <span class="insight-icon">${insight.icon}</span>
920
+ <div class="insight-content">
921
+ <div class="insight-title">${insight.title}</div>
922
+ <div class="insight-message">${insight.message}</div>
923
+ ${insight.action ? `<div class="insight-action">${insight.action}</div>` : ''}
924
+ </div>
925
+ </div>
926
+ `;
927
+ }).join('');
928
+ }
929
+
930
+ initCharts() {
931
+ ```
932
+
933
+ **Validation:** `renderInsights` method exists
934
+
935
+ ---
936
+
937
+ ### 7. `dashboard/app.js:142`
938
+
939
+ **Purpose:** Call renderInsights in renderDashboard
940
+
941
+ **Before:**
942
+ ```javascript
943
+ renderDashboard() {
944
+ this.updateProfileSummary();
945
+ this.updateStats();
946
+ this.renderRecentSessions();
947
+ }
948
+ ```
949
+
950
+ **After:**
951
+ ```javascript
952
+ renderDashboard() {
953
+ this.updateProfileSummary();
954
+ this.updateStats();
955
+ this.renderRecentSessions();
956
+ this.renderInsights();
957
+ }
958
+ ```
959
+
960
+ **Validation:** Insights render on dashboard load
961
+
962
+ ---
963
+
964
+ ### 8. `dashboard/styles.css` (append at end)
965
+
966
+ **Purpose:** Add insight styling
967
+
968
+ **Append:**
969
+ ```css
970
+ /* Insights Section */
971
+ .insights-list {
972
+ display: flex;
973
+ flex-direction: column;
974
+ gap: var(--spacing-sm);
975
+ }
976
+
977
+ .insight-item {
978
+ display: flex;
979
+ gap: var(--spacing-md);
980
+ padding: var(--spacing-md);
981
+ border-radius: var(--radius-md);
982
+ background: var(--bg-secondary);
983
+ border-left: 3px solid var(--text-muted);
984
+ }
985
+
986
+ .insight-item.insight-success {
987
+ border-left-color: var(--color-success);
988
+ background: rgba(63, 185, 80, 0.1);
989
+ }
990
+
991
+ .insight-item.insight-warning {
992
+ border-left-color: var(--color-warning);
993
+ background: rgba(210, 153, 34, 0.1);
994
+ }
995
+
996
+ .insight-item.insight-critical {
997
+ border-left-color: var(--color-danger);
998
+ background: rgba(248, 81, 73, 0.1);
999
+ }
1000
+
1001
+ .insight-item.insight-info {
1002
+ border-left-color: var(--color-accent);
1003
+ background: rgba(88, 166, 255, 0.1);
1004
+ }
1005
+
1006
+ .insight-icon {
1007
+ font-size: 1.5rem;
1008
+ line-height: 1;
1009
+ }
1010
+
1011
+ .insight-content {
1012
+ flex: 1;
1013
+ }
1014
+
1015
+ .insight-title {
1016
+ font-weight: 600;
1017
+ margin-bottom: 0.25rem;
1018
+ }
1019
+
1020
+ .insight-message {
1021
+ color: var(--text-muted);
1022
+ font-size: 0.875rem;
1023
+ }
1024
+
1025
+ .insight-action {
1026
+ margin-top: 0.5rem;
1027
+ font-size: 0.75rem;
1028
+ color: var(--color-accent);
1029
+ }
1030
+ ```
1031
+
1032
+ **Validation:** Styles apply correctly to insights
1033
+
1034
+ ---
1035
+
1036
+ ## Implementation Order
1037
+
1038
+ **CRITICAL: Sequence matters. Do not reorder.**
1039
+
1040
+ | Step | Action | Validation | Rollback |
1041
+ |------|--------|------------|----------|
1042
+ | 1 | Create `src/insights/types.ts` | `npm run build` | Delete file |
1043
+ | 2 | Create `src/insights/generators.ts` | `npm run build` | Delete file |
1044
+ | 3 | Create `src/insights/index.ts` | `npm run build` | Delete file |
1045
+ | 4 | Create `src/commands/dashboard.ts` | `npm run build` | Delete file |
1046
+ | 5 | Modify `src/commands/index.ts` | `npm run build` | Revert line |
1047
+ | 6 | Modify `src/cli.ts` (2 changes) | `npm run build` | Revert lines |
1048
+ | 7 | Modify `dashboard/app.js` | Browser test | Revert file |
1049
+ | 8 | Modify `dashboard/index.html` | Browser test | Revert file |
1050
+ | 9 | Append `dashboard/styles.css` | Visual check | Remove appended |
1051
+ | 10 | Full test | `vibe-check dashboard` | Revert all |
1052
+
1053
+ ---
1054
+
1055
+ ## Validation Strategy
1056
+
1057
+ ### Build Validation
1058
+ ```bash
1059
+ npm run build
1060
+ # Expected: No errors
1061
+ ```
1062
+
1063
+ ### Unit Validation
1064
+ ```bash
1065
+ npm test
1066
+ # Expected: All tests pass
1067
+ ```
1068
+
1069
+ ### Integration Validation
1070
+ ```bash
1071
+ node dist/cli.js dashboard --help
1072
+ # Expected: Shows dashboard command options
1073
+
1074
+ node dist/cli.js dashboard --no-open
1075
+ # Expected: Creates dashboard/dashboard-data.json
1076
+
1077
+ cat dashboard/dashboard-data.json | head -20
1078
+ # Expected: Valid JSON with profile, stats, insights
1079
+ ```
1080
+
1081
+ ### Browser Validation
1082
+ ```bash
1083
+ open dashboard/index.html
1084
+ # Expected: Dashboard loads with real data and insights
1085
+ ```
1086
+
1087
+ ---
1088
+
1089
+ ## Rollback Procedure
1090
+
1091
+ **Time to rollback:** 5 minutes
1092
+
1093
+ ### Full Rollback
1094
+ ```bash
1095
+ # Delete new files
1096
+ rm -f src/insights/types.ts
1097
+ rm -f src/insights/generators.ts
1098
+ rm -f src/insights/index.ts
1099
+ rm -f src/commands/dashboard.ts
1100
+ rm -f dashboard/dashboard-data.json
1101
+
1102
+ # Revert modified files
1103
+ git checkout -- src/commands/index.ts
1104
+ git checkout -- src/cli.ts
1105
+ git checkout -- dashboard/app.js
1106
+ git checkout -- dashboard/index.html
1107
+ git checkout -- dashboard/styles.css
1108
+
1109
+ # Verify
1110
+ npm run build
1111
+ ```
1112
+
1113
+ ---
1114
+
1115
+ ## Approval Checklist
1116
+
1117
+ **Human must verify before /implement:**
1118
+
1119
+ - [ ] Every file specified precisely (file:line)
1120
+ - [ ] All templates complete (no placeholders)
1121
+ - [ ] Validation commands provided
1122
+ - [ ] Rollback procedure complete
1123
+ - [ ] Implementation order is correct
1124
+ - [ ] Risks identified and mitigated
1125
+
1126
+ ---
1127
+
1128
+ ## Next Step
1129
+
1130
+ Once approved: `/implement insight-dashboard-plan-2025-11-30.md`