@boshu2/vibe-check 1.6.2 → 1.8.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 (156) hide show
  1. package/.agents/bundles/actionable-coaching-plan-2025-12-02.md +209 -0
  2. package/.agents/bundles/automatic-learning-cadence-plan-2025-12-02.md +1297 -0
  3. package/.agents/bundles/automatic-learning-cadence-research-2025-12-02.md +481 -0
  4. package/.agents/bundles/dashboard-data-quality-plan.md +458 -0
  5. package/.agents/bundles/rating-scoring-alignment-plan.md +427 -0
  6. package/.agents/bundles/rpi-session-capture-plan-2025-12-02.md +693 -0
  7. package/.agents/bundles/rpi-session-capture-research-2025-12-02.md +433 -0
  8. package/.agents/bundles/session-integration-plan-2025-12-02.md +144 -0
  9. package/.agents/plans/git-forensics-enhancement-2025-12-05.md +493 -0
  10. package/.claude/skills/typescript-review.md +152 -0
  11. package/CHANGELOG.md +53 -0
  12. package/CLAUDE.md +79 -3
  13. package/Makefile +160 -0
  14. package/README.md +141 -155
  15. package/SECURITY.md +5 -1
  16. package/assets/logo-dark.svg +47 -0
  17. package/assets/logo.svg +47 -0
  18. package/claude-progress.json +54 -4
  19. package/claude-progress.txt +114 -0
  20. package/dashboard/app.js +699 -66
  21. package/dashboard/chart.min.js +20 -0
  22. package/dashboard/dashboard-data.js +764 -0
  23. package/dashboard/dashboard-data.json +182 -71
  24. package/dashboard/index.html +139 -14
  25. package/dashboard/styles.css +579 -4
  26. package/dist/analyzers/patterns.d.ts +62 -0
  27. package/dist/analyzers/patterns.d.ts.map +1 -0
  28. package/dist/analyzers/patterns.js +103 -0
  29. package/dist/analyzers/patterns.js.map +1 -0
  30. package/dist/analyzers/quality.d.ts +58 -0
  31. package/dist/analyzers/quality.d.ts.map +1 -0
  32. package/dist/analyzers/quality.js +114 -0
  33. package/dist/analyzers/quality.js.map +1 -0
  34. package/dist/analyzers/sessions.d.ts +45 -0
  35. package/dist/analyzers/sessions.d.ts.map +1 -0
  36. package/dist/analyzers/sessions.js +123 -0
  37. package/dist/analyzers/sessions.js.map +1 -0
  38. package/dist/cli.js +5 -1
  39. package/dist/cli.js.map +1 -1
  40. package/dist/commands/analyze.d.ts.map +1 -1
  41. package/dist/commands/analyze.js +43 -2
  42. package/dist/commands/analyze.js.map +1 -1
  43. package/dist/commands/dashboard.js +4 -1
  44. package/dist/commands/dashboard.js.map +1 -1
  45. package/dist/commands/forensics.d.ts +29 -0
  46. package/dist/commands/forensics.d.ts.map +1 -0
  47. package/dist/commands/forensics.js +213 -0
  48. package/dist/commands/forensics.js.map +1 -0
  49. package/dist/commands/index.d.ts +5 -1
  50. package/dist/commands/index.d.ts.map +1 -1
  51. package/dist/commands/index.js +13 -3
  52. package/dist/commands/index.js.map +1 -1
  53. package/dist/commands/insights.d.ts +3 -0
  54. package/dist/commands/insights.d.ts.map +1 -0
  55. package/dist/commands/insights.js +120 -0
  56. package/dist/commands/insights.js.map +1 -0
  57. package/dist/commands/learn.d.ts +3 -0
  58. package/dist/commands/learn.d.ts.map +1 -0
  59. package/dist/commands/learn.js +161 -0
  60. package/dist/commands/learn.js.map +1 -0
  61. package/dist/commands/lesson.d.ts +8 -0
  62. package/dist/commands/lesson.d.ts.map +1 -0
  63. package/dist/commands/lesson.js +206 -0
  64. package/dist/commands/lesson.js.map +1 -0
  65. package/dist/commands/pipeline.d.ts +3 -0
  66. package/dist/commands/pipeline.d.ts.map +1 -0
  67. package/dist/commands/pipeline.js +485 -0
  68. package/dist/commands/pipeline.js.map +1 -0
  69. package/dist/commands/profile.d.ts +0 -1
  70. package/dist/commands/profile.d.ts.map +1 -1
  71. package/dist/commands/profile.js +3 -206
  72. package/dist/commands/profile.js.map +1 -1
  73. package/dist/commands/session.d.ts +51 -0
  74. package/dist/commands/session.d.ts.map +1 -0
  75. package/dist/commands/session.js +599 -0
  76. package/dist/commands/session.js.map +1 -0
  77. package/dist/commands/sessions.d.ts +20 -0
  78. package/dist/commands/sessions.d.ts.map +1 -0
  79. package/dist/commands/sessions.js +201 -0
  80. package/dist/commands/sessions.js.map +1 -0
  81. package/dist/commands/watch.d.ts.map +1 -1
  82. package/dist/commands/watch.js +48 -7
  83. package/dist/commands/watch.js.map +1 -1
  84. package/dist/gamification/index.d.ts +1 -3
  85. package/dist/gamification/index.d.ts.map +1 -1
  86. package/dist/gamification/index.js +2 -5
  87. package/dist/gamification/index.js.map +1 -1
  88. package/dist/gamification/pattern-memory.d.ts +1 -1
  89. package/dist/gamification/pattern-memory.d.ts.map +1 -1
  90. package/dist/gamification/pattern-memory.js.map +1 -1
  91. package/dist/gamification/profile.d.ts +2 -2
  92. package/dist/gamification/profile.d.ts.map +1 -1
  93. package/dist/gamification/profile.js +2 -15
  94. package/dist/gamification/profile.js.map +1 -1
  95. package/dist/gamification/types.d.ts +8 -2
  96. package/dist/gamification/types.d.ts.map +1 -1
  97. package/dist/gamification/types.js.map +1 -1
  98. package/dist/insights/index.d.ts.map +1 -1
  99. package/dist/insights/index.js +16 -4
  100. package/dist/insights/index.js.map +1 -1
  101. package/dist/insights/types.d.ts +14 -0
  102. package/dist/insights/types.d.ts.map +1 -1
  103. package/dist/learning/cadence.d.ts +15 -0
  104. package/dist/learning/cadence.d.ts.map +1 -0
  105. package/dist/learning/cadence.js +130 -0
  106. package/dist/learning/cadence.js.map +1 -0
  107. package/dist/learning/index.d.ts +19 -0
  108. package/dist/learning/index.d.ts.map +1 -0
  109. package/dist/learning/index.js +35 -0
  110. package/dist/learning/index.js.map +1 -0
  111. package/dist/learning/lessons-storage.d.ts +48 -0
  112. package/dist/learning/lessons-storage.d.ts.map +1 -0
  113. package/dist/learning/lessons-storage.js +266 -0
  114. package/dist/learning/lessons-storage.js.map +1 -0
  115. package/dist/learning/lessons-types.d.ts +83 -0
  116. package/dist/learning/lessons-types.d.ts.map +1 -0
  117. package/dist/learning/lessons-types.js +15 -0
  118. package/dist/learning/lessons-types.js.map +1 -0
  119. package/dist/learning/nudges.d.ts +20 -0
  120. package/dist/learning/nudges.d.ts.map +1 -0
  121. package/dist/learning/nudges.js +68 -0
  122. package/dist/learning/nudges.js.map +1 -0
  123. package/dist/learning/retrospective.d.ts +27 -0
  124. package/dist/learning/retrospective.d.ts.map +1 -0
  125. package/dist/learning/retrospective.js +184 -0
  126. package/dist/learning/retrospective.js.map +1 -0
  127. package/dist/learning/storage.d.ts +44 -0
  128. package/dist/learning/storage.d.ts.map +1 -0
  129. package/dist/learning/storage.js +194 -0
  130. package/dist/learning/storage.js.map +1 -0
  131. package/dist/learning/surfacing.d.ts +36 -0
  132. package/dist/learning/surfacing.d.ts.map +1 -0
  133. package/dist/learning/surfacing.js +255 -0
  134. package/dist/learning/surfacing.js.map +1 -0
  135. package/dist/learning/synthesis.d.ts +17 -0
  136. package/dist/learning/synthesis.d.ts.map +1 -0
  137. package/dist/learning/synthesis.js +293 -0
  138. package/dist/learning/synthesis.js.map +1 -0
  139. package/dist/learning/types.d.ts +60 -0
  140. package/dist/learning/types.d.ts.map +1 -0
  141. package/dist/learning/types.js +17 -0
  142. package/dist/learning/types.js.map +1 -0
  143. package/dist/storage/index.d.ts +1 -0
  144. package/dist/storage/index.d.ts.map +1 -1
  145. package/dist/storage/index.js +11 -1
  146. package/dist/storage/index.js.map +1 -1
  147. package/dist/storage/spiral-history.d.ts +62 -0
  148. package/dist/storage/spiral-history.d.ts.map +1 -0
  149. package/dist/storage/spiral-history.js +265 -0
  150. package/dist/storage/spiral-history.js.map +1 -0
  151. package/docs/ARCHITECTURE.md +2 -10
  152. package/docs/GAMIFICATION.md +19 -266
  153. package/docs/METRICS.md +528 -0
  154. package/docs/VIBE-ECOSYSTEM.md +12 -78
  155. package/feature-list.json +141 -68
  156. package/package.json +1 -1
package/dashboard/app.js CHANGED
@@ -4,6 +4,11 @@ class VibeDashboard {
4
4
  this.profile = null;
5
5
  this.charts = {};
6
6
  this.currentPage = 'dashboard';
7
+ this.historyFilters = {
8
+ rating: 'all',
9
+ sortBy: 'date-desc',
10
+ range: 'all'
11
+ };
7
12
  }
8
13
 
9
14
  async init() {
@@ -50,14 +55,77 @@ class VibeDashboard {
50
55
  closeModal.addEventListener('click', () => this.closeModal());
51
56
  }
52
57
 
58
+ // Session modal close
59
+ const closeSessionModal = document.getElementById('closeSessionModal');
60
+ if (closeSessionModal) {
61
+ closeSessionModal.addEventListener('click', () => this.closeSessionModal());
62
+ }
63
+
64
+ // History filters
65
+ ['filterRating', 'sortBy', 'filterRange'].forEach(id => {
66
+ const el = document.getElementById(id);
67
+ if (el) {
68
+ el.addEventListener('change', (e) => {
69
+ const key = id === 'filterRating' ? 'rating' :
70
+ id === 'sortBy' ? 'sortBy' : 'range';
71
+ this.historyFilters[key] = e.target.value;
72
+ this.renderHistory();
73
+ });
74
+ }
75
+ });
76
+
77
+ // View All link
78
+ const viewAllLink = document.querySelector('.view-all');
79
+ if (viewAllLink) {
80
+ viewAllLink.addEventListener('click', (e) => {
81
+ e.preventDefault();
82
+ this.navigateTo('history');
83
+ });
84
+ }
85
+
53
86
  // Keyboard shortcuts
54
87
  document.addEventListener('keydown', (e) => {
55
- if (e.key === 'Escape') this.closeModal();
88
+ if (e.key === 'Escape') {
89
+ this.closeModal();
90
+ this.closeSessionModal();
91
+ }
92
+ });
93
+
94
+ // Click outside modal to close
95
+ document.getElementById('sessionModal')?.addEventListener('click', (e) => {
96
+ if (e.target.classList.contains('modal-overlay')) {
97
+ this.closeSessionModal();
98
+ }
99
+ });
100
+
101
+ // Metrics help buttons
102
+ document.getElementById('codeHealthHelp')?.addEventListener('click', () => {
103
+ this.showMetricsHelp('codeHealth');
104
+ });
105
+ document.getElementById('patternScoreHelp')?.addEventListener('click', () => {
106
+ this.showMetricsHelp('patternScore');
107
+ });
108
+
109
+ // Metrics modal close
110
+ document.getElementById('closeMetricsModal')?.addEventListener('click', () => {
111
+ this.closeMetricsModal();
112
+ });
113
+ document.getElementById('metricsModal')?.addEventListener('click', (e) => {
114
+ if (e.target.classList.contains('modal-overlay')) {
115
+ this.closeMetricsModal();
116
+ }
56
117
  });
57
118
  }
58
119
 
59
120
  async loadProfile() {
60
- // Try to load dashboard-data.json (generated by vibe-check dashboard)
121
+ // First check for global variable (works with file:// URLs)
122
+ if (window.VIBE_CHECK_DATA) {
123
+ this.dashboardData = window.VIBE_CHECK_DATA;
124
+ this.profile = this.transformToProfile(window.VIBE_CHECK_DATA);
125
+ return;
126
+ }
127
+
128
+ // Try to fetch dashboard-data.json (works with http:// URLs)
61
129
  try {
62
130
  const response = await fetch('dashboard-data.json');
63
131
  if (response.ok) {
@@ -75,7 +143,7 @@ class VibeDashboard {
75
143
  if (stored) {
76
144
  this.profile = JSON.parse(stored);
77
145
  } else {
78
- this.profile = this.getMockProfile();
146
+ this.profile = this.getEmptyProfile();
79
147
  }
80
148
  }
81
149
 
@@ -92,7 +160,11 @@ class VibeDashboard {
92
160
  },
93
161
  streak: data.profile.streak,
94
162
  achievements: data.achievements.filter(a => a.unlockedAt),
95
- sessions: data.sessions,
163
+ // Map 'rating' to 'overall' for UI compatibility
164
+ sessions: data.sessions.map(s => ({
165
+ ...s,
166
+ overall: s.rating || s.overall,
167
+ })),
96
168
  stats: {
97
169
  totalSessions: data.stats.totals.sessions,
98
170
  totalCommitsAnalyzed: data.stats.totals.commits,
@@ -103,43 +175,31 @@ class VibeDashboard {
103
175
  };
104
176
  }
105
177
 
106
- getMockProfile() {
107
- // Mock profile for demonstration
178
+ getEmptyProfile() {
179
+ // Empty profile for new users - no fake data
108
180
  return {
109
181
  version: '1.0.0',
110
182
  xp: {
111
- total: 456,
112
- level: 3,
113
- levelName: 'Practitioner',
114
- currentLevelXP: 156,
115
- nextLevelXP: 300,
183
+ total: 0,
184
+ level: 1,
185
+ levelName: 'Novice',
186
+ currentLevelXP: 0,
187
+ nextLevelXP: 100,
116
188
  },
117
189
  streak: {
118
- current: 5,
119
- longest: 12,
120
- weeklyProgress: 3,
190
+ current: 0,
191
+ longest: 0,
192
+ weeklyProgress: 0,
121
193
  weeklyGoal: 5,
122
194
  },
123
- achievements: [
124
- { id: 'first_check', name: 'First Blood', icon: '🩸', unlockedAt: '2025-11-20' },
125
- { id: 'elite_vibes', name: 'Elite Vibes', icon: '✨', unlockedAt: '2025-11-25' },
126
- { id: 'zen_master', name: 'Zen Master', icon: '🧘', unlockedAt: '2025-11-28' },
127
- ],
128
- sessions: [
129
- { date: '2025-11-29', vibeScore: 89, overall: 'ELITE', commits: 14, spirals: 0 },
130
- { date: '2025-11-28', vibeScore: 85, overall: 'HIGH', commits: 23, spirals: 1 },
131
- { date: '2025-11-27', vibeScore: 91, overall: 'ELITE', commits: 31, spirals: 0 },
132
- { date: '2025-11-26', vibeScore: 78, overall: 'HIGH', commits: 18, spirals: 2 },
133
- { date: '2025-11-25', vibeScore: 82, overall: 'HIGH', commits: 27, spirals: 1 },
134
- { date: '2025-11-24', vibeScore: 70, overall: 'MEDIUM', commits: 12, spirals: 3 },
135
- { date: '2025-11-23', vibeScore: 88, overall: 'ELITE', commits: 35, spirals: 0 },
136
- ],
195
+ achievements: [],
196
+ sessions: [],
137
197
  stats: {
138
- totalSessions: 45,
139
- totalCommitsAnalyzed: 847,
140
- avgVibeScore: 82,
141
- bestVibeScore: 95,
142
- spiralsAvoided: 15,
198
+ totalSessions: 0,
199
+ totalCommitsAnalyzed: 0,
200
+ avgVibeScore: 0,
201
+ bestVibeScore: 0,
202
+ spiralsAvoided: 0,
143
203
  },
144
204
  };
145
205
  }
@@ -172,12 +232,28 @@ class VibeDashboard {
172
232
  }
173
233
 
174
234
  renderDashboard() {
235
+ this.updateRepoName();
175
236
  this.updateProfileSummary();
176
237
  this.updateStats();
177
238
  this.renderRecentSessions();
178
239
  this.renderInsights();
179
240
  }
180
241
 
242
+ updateRepoName() {
243
+ const repoEl = document.querySelector('#repoName .repo-path');
244
+ if (!repoEl) return;
245
+
246
+ if (this.dashboardData?.repo) {
247
+ // Show just the repo name, not full path
248
+ const parts = this.dashboardData.repo.split('/');
249
+ const repoName = parts[parts.length - 1] || this.dashboardData.repo;
250
+ repoEl.textContent = repoName;
251
+ repoEl.title = this.dashboardData.repo; // Full path on hover
252
+ } else {
253
+ repoEl.textContent = 'No repo data';
254
+ }
255
+ }
256
+
181
257
  updateProfileSummary() {
182
258
  const { xp, streak } = this.profile;
183
259
 
@@ -198,10 +274,17 @@ class VibeDashboard {
198
274
  const { sessions, stats } = this.profile;
199
275
  const latest = sessions[0];
200
276
 
277
+ // Pattern Score (vibeScore)
201
278
  document.getElementById('currentScore').textContent = latest ? `${latest.vibeScore}%` : '--';
202
- document.getElementById('avgScore').textContent = `${stats.avgVibeScore}%`;
203
279
  document.getElementById('achievementCount').textContent =
204
280
  `${this.profile.achievements.length}/24`;
281
+
282
+ // Code Health Rating (metric-based quality grade)
283
+ const codeHealthEl = document.getElementById('codeHealthRating');
284
+ if (codeHealthEl && latest?.rating) {
285
+ codeHealthEl.textContent = latest.rating;
286
+ codeHealthEl.className = 'stat-value rating-badge rating-' + latest.rating.toLowerCase();
287
+ }
205
288
  }
206
289
 
207
290
  renderRecentSessions() {
@@ -218,13 +301,26 @@ class VibeDashboard {
218
301
  return;
219
302
  }
220
303
 
221
- container.innerHTML = sessions.map(session => `
222
- <div class="session-item">
223
- <div class="session-date">${this.formatDate(session.date)}</div>
304
+ container.innerHTML = sessions.map((session, idx) => {
305
+ const rating = this.scoreToRating(session.vibeScore);
306
+ return `
307
+ <div class="session-item" data-session-idx="${idx}">
308
+ <div>
309
+ <div class="session-date">${this.formatDate(session.date)}</div>
310
+ <div class="session-commits">${session.commits} commits · ${session.spirals} spirals</div>
311
+ </div>
224
312
  <div class="session-score">${session.vibeScore}%</div>
225
- <span class="session-rating ${session.overall.toLowerCase()}">${session.overall}</span>
313
+ <span class="session-rating ${rating.toLowerCase()}">${rating}</span>
226
314
  </div>
227
- `).join('');
315
+ `}).join('');
316
+
317
+ // Add click handlers
318
+ container.querySelectorAll('.session-item').forEach(item => {
319
+ item.addEventListener('click', () => {
320
+ const idx = parseInt(item.dataset.sessionIdx);
321
+ this.showSessionDetail(this.profile.sessions[idx]);
322
+ });
323
+ });
228
324
  }
229
325
 
230
326
  renderInsights() {
@@ -264,18 +360,48 @@ class VibeDashboard {
264
360
  }
265
361
 
266
362
  initCharts() {
267
- this.initTrendChart();
268
- this.initRadarChart();
269
- this.initRatingsChart();
363
+ // Check if Chart.js is loaded
364
+ if (typeof Chart === 'undefined') {
365
+ console.error('Chart.js not loaded');
366
+ return;
367
+ }
368
+
369
+ console.log('Initializing charts with sessions:', this.profile?.sessions?.length);
370
+
371
+ try {
372
+ this.initTrendChart();
373
+ this.initRadarChart();
374
+ this.initRatingsChart();
375
+ console.log('All charts initialized successfully');
376
+ } catch (e) {
377
+ console.error('Error initializing charts:', e);
378
+ }
270
379
  }
271
380
 
272
381
  initTrendChart() {
273
- const ctx = document.getElementById('trendCanvas');
274
- if (!ctx) return;
382
+ const canvas = document.getElementById('trendCanvas');
383
+ if (!canvas) {
384
+ console.error('trendCanvas element not found');
385
+ return;
386
+ }
275
387
 
276
- const sessions = this.profile.sessions.slice().reverse();
277
- const labels = sessions.map(s => this.formatDate(s.date));
278
- const data = sessions.map(s => s.vibeScore);
388
+ const ctx = canvas.getContext('2d');
389
+ if (!ctx) {
390
+ console.error('Could not get 2D context for trendCanvas');
391
+ return;
392
+ }
393
+
394
+ const sessions = this.profile.sessions || [];
395
+ console.log('Trend chart sessions:', sessions.length, sessions.slice(0, 2));
396
+ if (sessions.length === 0) {
397
+ console.log('No sessions data for trend chart');
398
+ return;
399
+ }
400
+
401
+ const reversed = sessions.slice().reverse();
402
+ const labels = reversed.map(s => this.formatDate(s.date));
403
+ const scores = reversed.map(s => s.vibeScore);
404
+ console.log('Trend chart data - labels:', labels.length, 'scores:', scores.slice(0, 5));
279
405
 
280
406
  this.charts.trend = new Chart(ctx, {
281
407
  type: 'line',
@@ -283,7 +409,7 @@ class VibeDashboard {
283
409
  labels,
284
410
  datasets: [{
285
411
  label: 'Vibe Score',
286
- data,
412
+ data: scores,
287
413
  borderColor: '#58a6ff',
288
414
  backgroundColor: 'rgba(88, 166, 255, 0.1)',
289
415
  fill: true,
@@ -315,17 +441,42 @@ class VibeDashboard {
315
441
  }
316
442
 
317
443
  initRadarChart() {
318
- const ctx = document.getElementById('radarCanvas');
444
+ const canvas = document.getElementById('radarCanvas');
445
+ if (!canvas) return;
446
+ const ctx = canvas.getContext('2d');
319
447
  if (!ctx) return;
320
448
 
321
- // Mock metrics data
449
+ // Use real metrics from dashboard data, or show defaults
450
+ const metrics = this.dashboardData?.charts?.avgMetrics;
451
+
452
+ // Only show real data - no fake placeholder values
453
+ if (!metrics) {
454
+ // Show empty state message instead of fake data
455
+ const container = canvas.parentElement;
456
+ container.innerHTML = `
457
+ <div class="empty-chart-state">
458
+ <span class="empty-icon">📊</span>
459
+ <p>Run <code>vibe-check --score</code> to see your metrics</p>
460
+ </div>
461
+ `;
462
+ return;
463
+ }
464
+
465
+ const data = [
466
+ Math.min(100, metrics.trustPassRate),
467
+ Math.min(100, metrics.iterationVelocity * 10), // scale velocity (commits/hr) to 0-100
468
+ Math.min(100, metrics.flowEfficiency),
469
+ Math.max(0, 100 - metrics.reworkRatio), // invert: lower rework = higher stability
470
+ Math.max(0, 100 - metrics.debugSpiralDuration * 2), // scale spirals: 0 = 100, 50min = 0
471
+ ];
472
+
322
473
  this.charts.radar = new Chart(ctx, {
323
474
  type: 'radar',
324
475
  data: {
325
476
  labels: ['Trust Pass', 'Velocity', 'Flow', 'Stability', 'No Spirals'],
326
477
  datasets: [{
327
- label: 'Current',
328
- data: [92, 78, 85, 88, 95],
478
+ label: 'Average (Last 10)',
479
+ data: data,
329
480
  borderColor: '#58a6ff',
330
481
  backgroundColor: 'rgba(88, 166, 255, 0.2)',
331
482
  pointBackgroundColor: '#58a6ff',
@@ -352,7 +503,9 @@ class VibeDashboard {
352
503
  }
353
504
 
354
505
  initRatingsChart() {
355
- const ctx = document.getElementById('ratingsCanvas');
506
+ const canvas = document.getElementById('ratingsCanvas');
507
+ if (!canvas) return;
508
+ const ctx = canvas.getContext('2d');
356
509
  if (!ctx) return;
357
510
 
358
511
  // Count ratings
@@ -444,25 +597,198 @@ class VibeDashboard {
444
597
 
445
598
  renderHistory() {
446
599
  const container = document.getElementById('historyContainer');
600
+ let sessions = [...this.profile.sessions];
601
+
602
+ // Apply filters
603
+ const { rating, sortBy, range } = this.historyFilters;
604
+
605
+ // Rating filter
606
+ if (rating !== 'all') {
607
+ const ratingOrder = ['ELITE', 'HIGH', 'MEDIUM', 'LOW'];
608
+ const minIdx = ratingOrder.indexOf(rating);
609
+ sessions = sessions.filter(s => ratingOrder.indexOf(s.overall) <= minIdx);
610
+ }
611
+
612
+ // Time range filter
613
+ if (range !== 'all') {
614
+ const daysAgo = parseInt(range);
615
+ const cutoff = new Date();
616
+ cutoff.setDate(cutoff.getDate() - daysAgo);
617
+ sessions = sessions.filter(s => new Date(s.date) >= cutoff);
618
+ }
619
+
620
+ // Sorting
621
+ sessions.sort((a, b) => {
622
+ switch (sortBy) {
623
+ case 'date-asc': return new Date(a.date) - new Date(b.date);
624
+ case 'date-desc': return new Date(b.date) - new Date(a.date);
625
+ case 'score-desc': return b.vibeScore - a.vibeScore;
626
+ case 'score-asc': return a.vibeScore - b.vibeScore;
627
+ case 'commits-desc': return b.commits - a.commits;
628
+ default: return 0;
629
+ }
630
+ });
631
+
632
+ // Update stats
633
+ const avgScore = sessions.length > 0
634
+ ? Math.round(sessions.reduce((sum, s) => sum + s.vibeScore, 0) / sessions.length)
635
+ : 0;
636
+ document.getElementById('historyCount').textContent = `${sessions.length} sessions`;
637
+ document.getElementById('historyAvg').textContent = `Avg: ${avgScore}%`;
638
+
639
+ // Render heatmap
640
+ this.renderHeatmap();
641
+
642
+ // Render scope health chart
643
+ this.renderScopeHealthChart();
447
644
 
645
+ // Render session list
448
646
  container.innerHTML = `
449
- <div class="sessions-list">
450
- ${this.profile.sessions.map(session => `
451
- <div class="session-item">
452
- <div>
453
- <div class="session-date">${this.formatDate(session.date)}</div>
454
- <div style="font-size: 0.75rem; color: var(--text-muted)">
455
- ${session.commits} commits · ${session.spirals} spirals
456
- </div>
647
+ <div class="recent-section">
648
+ <div class="section-header">
649
+ <h3>All Sessions</h3>
650
+ </div>
651
+ <div class="sessions-list">
652
+ ${sessions.length === 0 ? `
653
+ <div class="empty-state">
654
+ <span class="empty-icon">🔍</span>
655
+ <p>No sessions match your filters</p>
457
656
  </div>
458
- <div class="session-score">${session.vibeScore}%</div>
459
- <span class="session-rating ${session.overall.toLowerCase()}">${session.overall}</span>
460
- </div>
461
- `).join('')}
657
+ ` : sessions.map((session, idx) => {
658
+ const trend = this.getSessionTrend(session, idx, sessions);
659
+ const rating = this.scoreToRating(session.vibeScore);
660
+ return `
661
+ <div class="session-item" data-session-idx="${this.profile.sessions.indexOf(session)}">
662
+ <div>
663
+ <div class="session-date">${this.formatDateLong(session.date)}</div>
664
+ <div class="session-commits">${session.commits} commits · ${session.spirals} spirals · +${session.xpEarned || 0} XP</div>
665
+ </div>
666
+ ${trend}
667
+ <div class="session-score">${session.vibeScore}%</div>
668
+ <span class="session-rating ${rating.toLowerCase()}">${rating}</span>
669
+ </div>
670
+ `;
671
+ }).join('')}
672
+ </div>
673
+ </div>
674
+ `;
675
+
676
+ // Add click handlers
677
+ container.querySelectorAll('.session-item').forEach(item => {
678
+ item.addEventListener('click', () => {
679
+ const idx = parseInt(item.dataset.sessionIdx);
680
+ this.showSessionDetail(this.profile.sessions[idx]);
681
+ });
682
+ });
683
+ }
684
+
685
+ getSessionTrend(session, idx, sessions) {
686
+ if (idx >= sessions.length - 1) return '<span class="trend-indicator neutral">—</span>';
687
+ const prev = sessions[idx + 1];
688
+ const diff = session.vibeScore - prev.vibeScore;
689
+ if (diff > 5) return `<span class="trend-indicator up">↑ +${diff}</span>`;
690
+ if (diff < -5) return `<span class="trend-indicator down">↓ ${diff}</span>`;
691
+ return '<span class="trend-indicator neutral">→</span>';
692
+ }
693
+
694
+ renderHeatmap() {
695
+ const container = document.getElementById('heatmapContainer');
696
+ if (!container || !this.dashboardData?.charts?.hourlyActivity) return;
697
+
698
+ const activity = this.dashboardData.charts.hourlyActivity;
699
+ const maxActivity = Math.max(...Object.values(activity), 1);
700
+ const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
701
+
702
+ // Build hour headers (0-23 simplified to key hours)
703
+ let hourLabels = '';
704
+ for (let h = 0; h < 24; h += 3) {
705
+ hourLabels += `<div class="heatmap-hour" style="grid-column: span 3">${h}:00</div>`;
706
+ }
707
+
708
+ // Build grid (simplified - just show hourly data as a single row)
709
+ let cells = '';
710
+ for (let h = 0; h < 24; h++) {
711
+ const count = activity[h] || 0;
712
+ const level = Math.ceil((count / maxActivity) * 5);
713
+ cells += `<div class="heatmap-cell level-${level}" title="${count} commits at ${h}:00"></div>`;
714
+ }
715
+
716
+ container.innerHTML = `
717
+ <div class="heatmap-grid" style="grid-template-columns: 50px repeat(24, 1fr);">
718
+ <div class="heatmap-label">Hours</div>
719
+ ${Array.from({length: 24}, (_, h) =>
720
+ `<div class="heatmap-hour">${h}</div>`
721
+ ).join('')}
722
+ <div class="heatmap-label">Activity</div>
723
+ ${cells}
724
+ </div>
725
+ <div class="heatmap-legend">
726
+ <span>Less</span>
727
+ ${[0,1,2,3,4,5].map(l => `<div class="heatmap-legend-cell level-${l}"></div>`).join('')}
728
+ <span>More</span>
462
729
  </div>
463
730
  `;
464
731
  }
465
732
 
733
+ renderScopeHealthChart() {
734
+ const canvas = document.getElementById('scopeHealthCanvas');
735
+ if (!canvas || !this.dashboardData?.charts?.scopeHealth) return;
736
+
737
+ // Destroy existing chart
738
+ if (this.charts.scopeHealth) {
739
+ this.charts.scopeHealth.destroy();
740
+ }
741
+
742
+ const ctx = canvas.getContext('2d');
743
+ if (!ctx) return;
744
+
745
+ const scopeData = this.dashboardData.charts.scopeHealth.slice(0, 8);
746
+ const labels = scopeData.map(s => s.scope || '(none)');
747
+ const commits = scopeData.map(s => s.commits);
748
+ const fixRatios = scopeData.map(s => s.fixRatio);
749
+
750
+ this.charts.scopeHealth = new Chart(ctx, {
751
+ type: 'bar',
752
+ data: {
753
+ labels,
754
+ datasets: [
755
+ {
756
+ label: 'Commits',
757
+ data: commits,
758
+ backgroundColor: '#58a6ff',
759
+ borderRadius: 4,
760
+ },
761
+ {
762
+ label: 'Fix Ratio %',
763
+ data: fixRatios,
764
+ backgroundColor: '#f85149',
765
+ borderRadius: 4,
766
+ }
767
+ ]
768
+ },
769
+ options: {
770
+ responsive: true,
771
+ maintainAspectRatio: false,
772
+ plugins: {
773
+ legend: {
774
+ position: 'bottom',
775
+ labels: { color: '#7d8590' }
776
+ }
777
+ },
778
+ scales: {
779
+ x: {
780
+ grid: { display: false },
781
+ ticks: { color: '#7d8590' }
782
+ },
783
+ y: {
784
+ grid: { color: '#30363d' },
785
+ ticks: { color: '#7d8590' }
786
+ }
787
+ }
788
+ }
789
+ });
790
+ }
791
+
466
792
  renderProfile() {
467
793
  const container = document.getElementById('profileContainer');
468
794
  const { stats, xp, streak } = this.profile;
@@ -544,6 +870,293 @@ class VibeDashboard {
544
870
  document.getElementById('achievementModal').classList.remove('show');
545
871
  }
546
872
 
873
+ closeSessionModal() {
874
+ document.getElementById('sessionModal').classList.remove('show');
875
+ // Destroy detail chart to free memory
876
+ if (this.charts.detailRadar) {
877
+ this.charts.detailRadar.destroy();
878
+ this.charts.detailRadar = null;
879
+ }
880
+ }
881
+
882
+ closeMetricsModal() {
883
+ document.getElementById('metricsModal')?.classList.remove('show');
884
+ }
885
+
886
+ showMetricsHelp(type) {
887
+ const modal = document.getElementById('metricsModal');
888
+ const title = document.getElementById('metricsModalTitle');
889
+ const content = document.getElementById('metricsHelpContent');
890
+ if (!modal || !content) return;
891
+
892
+ if (type === 'codeHealth') {
893
+ title.textContent = 'Code Health Rating';
894
+ content.innerHTML = `
895
+ <div class="metrics-help-section">
896
+ <p class="metrics-intro">Code Health grades your <strong>actual coding outcomes</strong>. It's calculated from 5 metrics that measure whether your code works on first try.</p>
897
+
898
+ <h3>The 5 Quality Metrics</h3>
899
+
900
+ <div class="metric-card">
901
+ <div class="metric-header">
902
+ <span class="metric-name">Iteration Velocity</span>
903
+ <span class="metric-formula">commits / active hours</span>
904
+ </div>
905
+ <p>How fast you're committing. Tight feedback loops catch issues early.</p>
906
+ <div class="metric-thresholds">
907
+ <span class="threshold elite">ELITE: >5/hr</span>
908
+ <span class="threshold high">HIGH: >=3/hr</span>
909
+ <span class="threshold medium">MEDIUM: >=1/hr</span>
910
+ <span class="threshold low">LOW: <1/hr</span>
911
+ </div>
912
+ </div>
913
+
914
+ <div class="metric-card">
915
+ <div class="metric-header">
916
+ <span class="metric-name">Rework Ratio</span>
917
+ <span class="metric-formula">fix commits / total commits</span>
918
+ </div>
919
+ <p>How much time you spend debugging vs building. Lower is better.</p>
920
+ <div class="metric-thresholds">
921
+ <span class="threshold elite">ELITE: <30%</span>
922
+ <span class="threshold high">HIGH: <50%</span>
923
+ <span class="threshold medium">MEDIUM: <70%</span>
924
+ <span class="threshold low">LOW: >=70%</span>
925
+ </div>
926
+ </div>
927
+
928
+ <div class="metric-card">
929
+ <div class="metric-header">
930
+ <span class="metric-name">Trust Pass Rate</span>
931
+ <span class="metric-formula">commits without immediate fix / total</span>
932
+ </div>
933
+ <p>Does your code stick on first try? A commit "fails" if followed by a fix within 30min.</p>
934
+ <div class="metric-thresholds">
935
+ <span class="threshold elite">ELITE: >95%</span>
936
+ <span class="threshold high">HIGH: >=80%</span>
937
+ <span class="threshold medium">MEDIUM: >=60%</span>
938
+ <span class="threshold low">LOW: <60%</span>
939
+ </div>
940
+ </div>
941
+
942
+ <div class="metric-card">
943
+ <div class="metric-header">
944
+ <span class="metric-name">Debug Spiral Duration</span>
945
+ <span class="metric-formula">avg minutes in 3+ consecutive fix chains</span>
946
+ </div>
947
+ <p>How long you stay stuck when debugging. Spirals = 3+ fixes to same component.</p>
948
+ <div class="metric-thresholds">
949
+ <span class="threshold elite">ELITE: <15m</span>
950
+ <span class="threshold high">HIGH: <30m</span>
951
+ <span class="threshold medium">MEDIUM: <60m</span>
952
+ <span class="threshold low">LOW: >=60m</span>
953
+ </div>
954
+ </div>
955
+
956
+ <div class="metric-card">
957
+ <div class="metric-header">
958
+ <span class="metric-name">Flow Efficiency</span>
959
+ <span class="metric-formula">(active time - spiral time) / active time</span>
960
+ </div>
961
+ <p>Percentage of time spent building vs stuck debugging.</p>
962
+ <div class="metric-thresholds">
963
+ <span class="threshold elite">ELITE: >90%</span>
964
+ <span class="threshold high">HIGH: >=75%</span>
965
+ <span class="threshold medium">MEDIUM: >=50%</span>
966
+ <span class="threshold low">LOW: <50%</span>
967
+ </div>
968
+ </div>
969
+
970
+ <h3>How It's Calculated</h3>
971
+ <p>Each metric gets rated (ELITE=4, HIGH=3, MEDIUM=2, LOW=1). Your Code Health is the average:</p>
972
+ <div class="formula-box">
973
+ avg >= 3.5 → ELITE | avg >= 2.5 → HIGH | avg >= 1.5 → MEDIUM | else → LOW
974
+ </div>
975
+ </div>
976
+ `;
977
+ } else if (type === 'patternScore') {
978
+ title.textContent = 'Pattern Score';
979
+ content.innerHTML = `
980
+ <div class="metrics-help-section">
981
+ <p class="metrics-intro">Pattern Score detects <strong>workflow problems</strong> even without conventional commit messages. It's an early warning system for trouble.</p>
982
+
983
+ <h3>The 4 Pattern Metrics</h3>
984
+
985
+ <div class="metric-card">
986
+ <div class="metric-header">
987
+ <span class="metric-name">File Churn</span>
988
+ <span class="metric-weight">30% weight</span>
989
+ </div>
990
+ <p>Files touched 3+ times in 1 hour. High churn suggests thrashing or incomplete understanding.</p>
991
+ <div class="metric-formula">score = (1 - churned_files / total_files) × 100</div>
992
+ <div class="metric-thresholds">
993
+ <span class="threshold elite">ELITE: >90%</span>
994
+ <span class="threshold high">HIGH: 75-90%</span>
995
+ <span class="threshold medium">MEDIUM: 60-75%</span>
996
+ <span class="threshold low">LOW: <60%</span>
997
+ </div>
998
+ </div>
999
+
1000
+ <div class="metric-card">
1001
+ <div class="metric-header">
1002
+ <span class="metric-name">Time Spiral</span>
1003
+ <span class="metric-weight">25% weight</span>
1004
+ </div>
1005
+ <p>Commits less than 5 minutes apart. Rapid commits suggest frustrated iteration.</p>
1006
+ <div class="metric-formula">score = (1 - rapid_commits / total_commits) × 100</div>
1007
+ <div class="metric-thresholds">
1008
+ <span class="threshold elite">ELITE: >85%</span>
1009
+ <span class="threshold high">HIGH: 70-85%</span>
1010
+ <span class="threshold medium">MEDIUM: 50-70%</span>
1011
+ <span class="threshold low">LOW: <50%</span>
1012
+ </div>
1013
+ </div>
1014
+
1015
+ <div class="metric-card">
1016
+ <div class="metric-header">
1017
+ <span class="metric-name">Velocity Anomaly</span>
1018
+ <span class="metric-weight">20% weight</span>
1019
+ </div>
1020
+ <p>How far from your personal baseline. Unusual velocity (too fast/slow) signals problems.</p>
1021
+ <div class="metric-formula">z-score = |velocity - baseline| / stdDev</div>
1022
+ <div class="metric-thresholds">
1023
+ <span class="threshold elite">ELITE: <1σ</span>
1024
+ <span class="threshold high">HIGH: <1.5σ</span>
1025
+ <span class="threshold medium">MEDIUM: <2σ</span>
1026
+ <span class="threshold low">LOW: >=2σ</span>
1027
+ </div>
1028
+ </div>
1029
+
1030
+ <div class="metric-card">
1031
+ <div class="metric-header">
1032
+ <span class="metric-name">Code Stability</span>
1033
+ <span class="metric-weight">25% weight</span>
1034
+ </div>
1035
+ <p>What percentage of added code survives. High churn = building on wrong assumptions.</p>
1036
+ <div class="metric-formula">score = (1 - deletions/additions × 0.5) × 100</div>
1037
+ <div class="metric-thresholds">
1038
+ <span class="threshold elite">ELITE: >=85%</span>
1039
+ <span class="threshold high">HIGH: >=70%</span>
1040
+ <span class="threshold medium">MEDIUM: >=50%</span>
1041
+ <span class="threshold low">LOW: <50%</span>
1042
+ </div>
1043
+ </div>
1044
+
1045
+ <h3>How It's Calculated</h3>
1046
+ <p>Weighted sum of all pattern metrics:</p>
1047
+ <div class="formula-box">
1048
+ Pattern Score = (fileChurn × 0.30) + (timeSpiral × 0.25) + (velocityAnomaly × 0.20) + (codeStability × 0.25)
1049
+ </div>
1050
+ </div>
1051
+ `;
1052
+ }
1053
+
1054
+ modal.classList.add('show');
1055
+ }
1056
+
1057
+ // Derive rating from vibeScore to ensure consistency
1058
+ scoreToRating(score) {
1059
+ if (score >= 85) return 'ELITE';
1060
+ if (score >= 70) return 'HIGH';
1061
+ if (score >= 50) return 'MEDIUM';
1062
+ return 'LOW';
1063
+ }
1064
+
1065
+ showSessionDetail(session) {
1066
+ if (!session) return;
1067
+
1068
+ // Update modal content
1069
+ const scoreRing = document.querySelector('.detail-score-ring');
1070
+ scoreRing.style.setProperty('--score', session.vibeScore);
1071
+
1072
+ // Calculate rating from score to ensure badge matches displayed percentage
1073
+ const rating = this.scoreToRating(session.vibeScore);
1074
+ document.getElementById('detailScore').textContent = `${session.vibeScore}%`;
1075
+ document.getElementById('detailRating').textContent = rating;
1076
+ document.getElementById('detailDate').textContent = this.formatDateLong(session.date);
1077
+ document.getElementById('detailXP').textContent = session.xpEarned || 0;
1078
+ document.getElementById('detailCommits').textContent = session.commits;
1079
+ document.getElementById('detailSpirals').textContent = session.spirals;
1080
+
1081
+ // Use real metrics if available, otherwise estimate
1082
+ const metrics = session.metrics;
1083
+ const velocity = metrics?.iterationVelocity?.toFixed(1) || `~${Math.round(session.commits / 2)}`;
1084
+ const trustRate = metrics?.trustPassRate?.toFixed(0) || Math.max(0, 100 - (session.spirals * 10));
1085
+ document.getElementById('detailVelocity').textContent = `${velocity}/hr`;
1086
+ document.getElementById('detailTrust').textContent = `${trustRate}%`;
1087
+
1088
+ // Render detail radar chart
1089
+ this.renderDetailRadar(session);
1090
+
1091
+ // Show modal
1092
+ document.getElementById('sessionModal').classList.add('show');
1093
+ }
1094
+
1095
+ renderDetailRadar(session) {
1096
+ const canvas = document.getElementById('detailRadarCanvas');
1097
+ if (!canvas) return;
1098
+
1099
+ // Destroy existing chart
1100
+ if (this.charts.detailRadar) {
1101
+ this.charts.detailRadar.destroy();
1102
+ }
1103
+
1104
+ const ctx = canvas.getContext('2d');
1105
+ if (!ctx) return;
1106
+
1107
+ // Use real metrics if available, otherwise estimate from session data
1108
+ const m = session.metrics;
1109
+ let data;
1110
+ if (m) {
1111
+ data = [
1112
+ Math.min(100, m.trustPassRate),
1113
+ Math.min(100, m.iterationVelocity * 10),
1114
+ Math.min(100, m.flowEfficiency),
1115
+ Math.max(0, 100 - m.reworkRatio),
1116
+ Math.max(0, 100 - m.debugSpiralDuration * 2),
1117
+ ];
1118
+ } else {
1119
+ // Fallback to estimates
1120
+ const trustPass = Math.max(0, 100 - (session.spirals * 15));
1121
+ const velocity = Math.min(100, (session.commits / 50) * 100);
1122
+ const flow = session.vibeScore;
1123
+ const stability = session.spirals === 0 ? 100 : Math.max(0, 100 - (session.spirals * 20));
1124
+ const noSpirals = session.spirals === 0 ? 100 : Math.max(0, 100 - (session.spirals * 25));
1125
+ data = [trustPass, velocity, flow, stability, noSpirals];
1126
+ }
1127
+
1128
+ this.charts.detailRadar = new Chart(ctx, {
1129
+ type: 'radar',
1130
+ data: {
1131
+ labels: ['Trust Pass', 'Velocity', 'Flow', 'Stability', 'No Spirals'],
1132
+ datasets: [{
1133
+ label: m ? 'Session Metrics' : 'Estimated',
1134
+ data: data,
1135
+ borderColor: '#58a6ff',
1136
+ backgroundColor: 'rgba(88, 166, 255, 0.2)',
1137
+ pointBackgroundColor: '#58a6ff',
1138
+ }]
1139
+ },
1140
+ options: {
1141
+ responsive: true,
1142
+ maintainAspectRatio: true,
1143
+ plugins: {
1144
+ legend: { display: false },
1145
+ },
1146
+ scales: {
1147
+ r: {
1148
+ min: 0,
1149
+ max: 100,
1150
+ grid: { color: '#30363d' },
1151
+ angleLines: { color: '#30363d' },
1152
+ pointLabels: { color: '#7d8590', font: { size: 10 } },
1153
+ ticks: { display: false },
1154
+ }
1155
+ }
1156
+ }
1157
+ });
1158
+ }
1159
+
547
1160
  showAchievementModal(achievement) {
548
1161
  document.getElementById('modalAchievementIcon').textContent = achievement.icon;
549
1162
  document.getElementById('modalAchievementName').textContent = achievement.name;
@@ -560,8 +1173,28 @@ class VibeDashboard {
560
1173
  const date = new Date(dateStr);
561
1174
  return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
562
1175
  }
1176
+
1177
+ formatDateLong(dateStr) {
1178
+ const date = new Date(dateStr);
1179
+ return date.toLocaleDateString('en-US', {
1180
+ weekday: 'short',
1181
+ month: 'short',
1182
+ day: 'numeric',
1183
+ hour: 'numeric',
1184
+ minute: '2-digit'
1185
+ });
1186
+ }
563
1187
  }
564
1188
 
565
1189
  // Initialize dashboard
566
1190
  const dashboard = new VibeDashboard();
567
- document.addEventListener('DOMContentLoaded', () => dashboard.init());
1191
+ document.addEventListener('DOMContentLoaded', () => {
1192
+ dashboard.init();
1193
+ // Retry chart init after a delay in case Chart.js loaded late
1194
+ if (!dashboard.charts.trend) {
1195
+ setTimeout(() => {
1196
+ console.log('Retrying chart initialization...');
1197
+ dashboard.initCharts();
1198
+ }, 500);
1199
+ }
1200
+ });