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