@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
@@ -1,1362 +0,0 @@
1
- # Ultimate Vibe-Coding Game - Implementation Plan
2
-
3
- **Type:** Plan
4
- **Created:** 2025-11-29
5
- **Version:** 1.5.0 (target)
6
- **Loop:** Middle (bridges research to implementation)
7
- **Tags:** gamification, leaderboards, challenges, prestige
8
-
9
- ---
10
-
11
- ## Overview
12
-
13
- Transform vibe-check from a metrics tool into the **ultimate gamified coding experience**. This plan adds:
14
-
15
- 1. **Weekly Challenges** - Auto-generated goals based on your weak metrics
16
- 2. **Prestige System** - Endless progression beyond Grandmaster
17
- 3. **Enhanced Streak Display** - Visual progression (🔥→🌟→👑)
18
- 4. **Local Leaderboards** - Personal high scores across repos
19
- 5. **Hall of Fame** - Personal records and bests
20
- 6. **Weekly Stats with Sparklines** - Trend visualization
21
- 7. **Rank Badges** - Bronze → Diamond visual tiers
22
- 8. **Share-to-Clipboard** - Brag on Slack/Discord
23
- 9. **Near-miss Psychology** - "SO CLOSE!" motivation
24
-
25
- **Philosophy:** Maximize FAAFO and user experience. Games are fun. Vibe-coding should be fun too.
26
-
27
- ---
28
-
29
- ## PDC Strategy
30
-
31
- ### Prevent
32
- - [ ] Don't add npm dependencies (chalk already handles everything)
33
- - [ ] Don't overengineer (local JSON storage, no server)
34
- - [ ] Don't gamify bad practices (quality > quantity)
35
-
36
- ### Detect
37
- - [ ] Test each feature independently before integration
38
- - [ ] Verify XP calculations don't break existing logic
39
- - [ ] Check profile migration handles new fields
40
-
41
- ### Correct
42
- - [ ] All features are additive (existing profiles still work)
43
- - [ ] Profile migration auto-adds new fields with defaults
44
-
45
- ---
46
-
47
- ## Sprint 1: Core Gamification Boost (4-5 hours)
48
-
49
- ### 1.1 Weekly Challenges System
50
-
51
- **New File: `src/gamification/challenges.ts`**
52
-
53
- ```typescript
54
- import { UserProfile, SessionRecord, StreakState } from './types';
55
-
56
- // Challenge types
57
- export type ChallengeType =
58
- | 'TRUST_STREAK' // Get 90%+ trust N times
59
- | 'ZERO_SPIRALS' // 0 spirals for N days
60
- | 'ELITE_COUNT' // Get N ELITE sessions
61
- | 'COMMIT_VOLUME' // Analyze N commits
62
- | 'STREAK_EXTEND'; // Extend streak by N days
63
-
64
- export interface Challenge {
65
- id: string;
66
- type: ChallengeType;
67
- name: string;
68
- description: string;
69
- icon: string;
70
- target: number; // Goal to reach
71
- progress: number; // Current progress
72
- reward: number; // XP reward
73
- weekStart: string; // ISO date of week start
74
- completed: boolean;
75
- completedAt?: string;
76
- }
77
-
78
- export const CHALLENGE_DEFINITIONS: Record<ChallengeType, {
79
- name: string;
80
- description: (target: number) => string;
81
- icon: string;
82
- targets: number[]; // Easy, medium, hard
83
- rewards: number[]; // Corresponding rewards
84
- }> = {
85
- TRUST_STREAK: {
86
- name: 'Trust Gauntlet',
87
- description: (n) => `Get 90%+ trust in ${n} sessions`,
88
- icon: '🎯',
89
- targets: [3, 5, 7],
90
- rewards: [50, 100, 150],
91
- },
92
- ZERO_SPIRALS: {
93
- name: 'Zen Mode',
94
- description: (n) => `${n} sessions with 0 spirals`,
95
- icon: '🧘',
96
- targets: [3, 5, 7],
97
- rewards: [50, 100, 150],
98
- },
99
- ELITE_COUNT: {
100
- name: 'Elite Streak',
101
- description: (n) => `Get ${n} ELITE ratings this week`,
102
- icon: '✨',
103
- targets: [2, 4, 6],
104
- rewards: [50, 100, 150],
105
- },
106
- COMMIT_VOLUME: {
107
- name: 'Commit Champion',
108
- description: (n) => `Analyze ${n}+ commits this week`,
109
- icon: '📊',
110
- targets: [50, 100, 200],
111
- rewards: [30, 60, 100],
112
- },
113
- STREAK_EXTEND: {
114
- name: 'Streak Builder',
115
- description: (n) => `Extend your streak by ${n} days`,
116
- icon: '🔥',
117
- targets: [3, 5, 7],
118
- rewards: [40, 80, 120],
119
- },
120
- };
121
-
122
- /**
123
- * Generate weekly challenges based on user's weak metrics
124
- */
125
- export function generateWeeklyChallenges(profile: UserProfile): Challenge[] {
126
- const weekStart = getWeekStartISO(new Date());
127
- const recentSessions = getSessionsThisWeek(profile.sessions, weekStart);
128
-
129
- // Analyze weak areas
130
- const avgTrust = recentSessions.length > 0
131
- ? recentSessions.reduce((sum, s) => sum + (s.vibeScore || 0), 0) / recentSessions.length
132
- : 50;
133
- const spiralCount = recentSessions.reduce((sum, s) => sum + s.spirals, 0);
134
- const eliteCount = recentSessions.filter(s => s.overall === 'ELITE').length;
135
-
136
- // Pick 3 challenges (1 based on weakness, 2 random)
137
- const challenges: Challenge[] = [];
138
- const usedTypes = new Set<ChallengeType>();
139
-
140
- // Challenge 1: Based on weakness
141
- if (avgTrust < 80) {
142
- challenges.push(createChallenge('TRUST_STREAK', weekStart, 1)); // Medium
143
- usedTypes.add('TRUST_STREAK');
144
- } else if (spiralCount > 3) {
145
- challenges.push(createChallenge('ZERO_SPIRALS', weekStart, 1));
146
- usedTypes.add('ZERO_SPIRALS');
147
- } else if (eliteCount < 2) {
148
- challenges.push(createChallenge('ELITE_COUNT', weekStart, 1));
149
- usedTypes.add('ELITE_COUNT');
150
- } else {
151
- challenges.push(createChallenge('STREAK_EXTEND', weekStart, 1));
152
- usedTypes.add('STREAK_EXTEND');
153
- }
154
-
155
- // Challenge 2-3: Random from remaining
156
- const remainingTypes = (Object.keys(CHALLENGE_DEFINITIONS) as ChallengeType[])
157
- .filter(t => !usedTypes.has(t));
158
-
159
- for (let i = 0; i < 2 && remainingTypes.length > 0; i++) {
160
- const idx = Math.floor(Math.random() * remainingTypes.length);
161
- const type = remainingTypes.splice(idx, 1)[0];
162
- challenges.push(createChallenge(type, weekStart, 0)); // Easy difficulty
163
- }
164
-
165
- return challenges;
166
- }
167
-
168
- function createChallenge(type: ChallengeType, weekStart: string, difficultyIdx: number): Challenge {
169
- const def = CHALLENGE_DEFINITIONS[type];
170
- const target = def.targets[difficultyIdx];
171
- const reward = def.rewards[difficultyIdx];
172
-
173
- return {
174
- id: `${type}-${weekStart}`,
175
- type,
176
- name: def.name,
177
- description: def.description(target),
178
- icon: def.icon,
179
- target,
180
- progress: 0,
181
- reward,
182
- weekStart,
183
- completed: false,
184
- };
185
- }
186
-
187
- /**
188
- * Update challenge progress after a session
189
- */
190
- export function updateChallengeProgress(
191
- challenges: Challenge[],
192
- session: SessionRecord,
193
- streak: StreakState
194
- ): { challenges: Challenge[]; completed: Challenge[] } {
195
- const completed: Challenge[] = [];
196
-
197
- for (const challenge of challenges) {
198
- if (challenge.completed) continue;
199
-
200
- switch (challenge.type) {
201
- case 'TRUST_STREAK':
202
- if (session.vibeScore >= 90) challenge.progress++;
203
- break;
204
- case 'ZERO_SPIRALS':
205
- if (session.spirals === 0 && session.commits >= 10) challenge.progress++;
206
- break;
207
- case 'ELITE_COUNT':
208
- if (session.overall === 'ELITE') challenge.progress++;
209
- break;
210
- case 'COMMIT_VOLUME':
211
- challenge.progress += session.commits;
212
- break;
213
- case 'STREAK_EXTEND':
214
- challenge.progress = streak.current - (challenge.progress || 0);
215
- break;
216
- }
217
-
218
- if (challenge.progress >= challenge.target) {
219
- challenge.completed = true;
220
- challenge.completedAt = new Date().toISOString();
221
- completed.push(challenge);
222
- }
223
- }
224
-
225
- return { challenges, completed };
226
- }
227
-
228
- /**
229
- * Get current week's challenges, generating if needed
230
- */
231
- export function getCurrentChallenges(profile: UserProfile): Challenge[] {
232
- const weekStart = getWeekStartISO(new Date());
233
- const existingChallenges = profile.challenges || [];
234
-
235
- // Check if we have challenges for this week
236
- const thisWeekChallenges = existingChallenges.filter(c => c.weekStart === weekStart);
237
-
238
- if (thisWeekChallenges.length >= 3) {
239
- return thisWeekChallenges;
240
- }
241
-
242
- // Generate new challenges
243
- return generateWeeklyChallenges(profile);
244
- }
245
-
246
- /**
247
- * Format challenges for display
248
- */
249
- export function formatChallenges(challenges: Challenge[]): string {
250
- const lines: string[] = [];
251
-
252
- for (const c of challenges) {
253
- const progressBar = createProgressBar(c.progress, c.target, 10);
254
- const status = c.completed ? '✓ COMPLETE' : `${c.progress}/${c.target}`;
255
- lines.push(`${c.icon} ${c.name}: ${progressBar} ${status}`);
256
- }
257
-
258
- return lines.join('\n');
259
- }
260
-
261
- function createProgressBar(current: number, total: number, length: number): string {
262
- const pct = Math.min(current / total, 1);
263
- const filled = Math.round(pct * length);
264
- const empty = length - filled;
265
- return '█'.repeat(filled) + '░'.repeat(empty);
266
- }
267
-
268
- function getWeekStartISO(date: Date): string {
269
- const d = new Date(date);
270
- const day = d.getDay();
271
- const diff = d.getDate() - day + (day === 0 ? -6 : 1);
272
- d.setDate(diff);
273
- d.setHours(0, 0, 0, 0);
274
- return d.toISOString().split('T')[0];
275
- }
276
-
277
- function getSessionsThisWeek(sessions: SessionRecord[], weekStart: string): SessionRecord[] {
278
- const start = new Date(weekStart);
279
- return sessions.filter(s => new Date(s.date) >= start);
280
- }
281
- ```
282
-
283
- **Modify: `src/gamification/types.ts`**
284
-
285
- Add after line 78 (after `stats` block):
286
-
287
- ```typescript
288
- // Weekly challenges (v1.5.0)
289
- challenges?: Challenge[];
290
- ```
291
-
292
- Add import at top:
293
- ```typescript
294
- import { Challenge } from './challenges';
295
- ```
296
-
297
- **Validation:** `npm test` passes, `npm run build` succeeds
298
-
299
- ---
300
-
301
- ### 1.2 Prestige System
302
-
303
- **Modify: `src/gamification/types.ts:150-158`**
304
-
305
- Replace LEVELS constant:
306
-
307
- ```typescript
308
- // Level progression (including prestige)
309
- export const LEVELS = [
310
- { level: 1, name: 'Novice', icon: '🌱', minXP: 0, maxXP: 100 },
311
- { level: 2, name: 'Apprentice', icon: '🌿', minXP: 100, maxXP: 300 },
312
- { level: 3, name: 'Practitioner', icon: '🌳', minXP: 300, maxXP: 600 },
313
- { level: 4, name: 'Expert', icon: '🌲', minXP: 600, maxXP: 1000 },
314
- { level: 5, name: 'Master', icon: '🎋', minXP: 1000, maxXP: 2000 },
315
- { level: 6, name: 'Grandmaster', icon: '🏔️', minXP: 2000, maxXP: 5000 },
316
- ] as const;
317
-
318
- // Prestige tiers (after Grandmaster)
319
- export const PRESTIGE_TIERS = [
320
- { tier: 1, name: 'Archmage', icon: '🔮', minXP: 5000, maxXP: 10000 },
321
- { tier: 2, name: 'Sage', icon: '📿', minXP: 10000, maxXP: 20000 },
322
- { tier: 3, name: 'Zenmester', icon: '☯️', minXP: 20000, maxXP: 40000 },
323
- { tier: 4, name: 'Transcendent', icon: '🌟', minXP: 40000, maxXP: 80000 },
324
- { tier: 5, name: 'Legendary', icon: '💫', minXP: 80000, maxXP: Infinity },
325
- ] as const;
326
- ```
327
-
328
- **Modify: `src/gamification/types.ts:12-20`**
329
-
330
- Update XPState interface:
331
-
332
- ```typescript
333
- export interface XPState {
334
- total: number; // Lifetime XP
335
- level: number; // Current level (1-6)
336
- levelName: string; // "Novice", "Apprentice", etc.
337
- currentLevelXP: number; // XP in current level
338
- nextLevelXP: number; // XP needed for next level
339
- lastSessionXP: number; // XP earned in last session
340
- prestigeTier?: number; // Prestige tier (0 = none, 1-5 = Archmage to Legendary)
341
- prestigeName?: string; // Prestige tier name
342
- }
343
- ```
344
-
345
- **Modify: `src/gamification/xp.ts:88-95`**
346
-
347
- Update `getLevelForXP` to handle prestige:
348
-
349
- ```typescript
350
- import { LEVELS, PRESTIGE_TIERS } from './types';
351
-
352
- /**
353
- * Get level for given XP amount (including prestige)
354
- */
355
- export function getLevelForXP(xp: number): {
356
- level: number;
357
- levelInfo: typeof LEVELS[number];
358
- prestigeTier?: number;
359
- prestigeInfo?: typeof PRESTIGE_TIERS[number];
360
- } {
361
- // Check prestige tiers first
362
- if (xp >= 5000) {
363
- for (let i = PRESTIGE_TIERS.length - 1; i >= 0; i--) {
364
- if (xp >= PRESTIGE_TIERS[i].minXP) {
365
- return {
366
- level: 6,
367
- levelInfo: LEVELS[5],
368
- prestigeTier: PRESTIGE_TIERS[i].tier,
369
- prestigeInfo: PRESTIGE_TIERS[i],
370
- };
371
- }
372
- }
373
- }
374
-
375
- // Regular levels
376
- for (let i = LEVELS.length - 1; i >= 0; i--) {
377
- if (xp >= LEVELS[i].minXP) {
378
- return { level: LEVELS[i].level, levelInfo: LEVELS[i] };
379
- }
380
- }
381
- return { level: 1, levelInfo: LEVELS[0] };
382
- }
383
- ```
384
-
385
- **Modify: `src/gamification/xp.ts:63-83`**
386
-
387
- Update `addXP` to handle prestige:
388
-
389
- ```typescript
390
- export function addXP(currentXP: XPState, earnedXP: number): {
391
- xp: XPState;
392
- leveledUp: boolean;
393
- newLevel?: typeof LEVELS[number];
394
- prestigedUp?: boolean;
395
- newPrestige?: typeof PRESTIGE_TIERS[number];
396
- } {
397
- const newTotal = currentXP.total + earnedXP;
398
- const { level, levelInfo, prestigeTier, prestigeInfo } = getLevelForXP(newTotal);
399
-
400
- const leveledUp = level > currentXP.level;
401
- const prestigedUp = prestigeTier !== undefined &&
402
- (currentXP.prestigeTier === undefined || prestigeTier > currentXP.prestigeTier);
403
-
404
- const maxXP = prestigeInfo ? prestigeInfo.maxXP : levelInfo.maxXP;
405
- const minXP = prestigeInfo ? prestigeInfo.minXP : levelInfo.minXP;
406
-
407
- const newXP: XPState = {
408
- total: newTotal,
409
- level,
410
- levelName: prestigeInfo ? prestigeInfo.name : levelInfo.name,
411
- currentLevelXP: newTotal - minXP,
412
- nextLevelXP: maxXP === Infinity ? Infinity : maxXP - minXP,
413
- lastSessionXP: earnedXP,
414
- prestigeTier,
415
- prestigeName: prestigeInfo?.name,
416
- };
417
-
418
- return {
419
- xp: newXP,
420
- leveledUp,
421
- newLevel: leveledUp ? levelInfo : undefined,
422
- prestigedUp,
423
- newPrestige: prestigedUp ? prestigeInfo : undefined,
424
- };
425
- }
426
- ```
427
-
428
- **Validation:** `npm test` passes, profile shows prestige tier
429
-
430
- ---
431
-
432
- ### 1.3 Enhanced Streak Display
433
-
434
- **Modify: `src/gamification/streaks.ts:102-118`**
435
-
436
- Replace `formatStreak` function:
437
-
438
- ```typescript
439
- /**
440
- * Format streak for display with visual progression
441
- */
442
- export function formatStreak(streak: StreakState): string {
443
- if (streak.current === 0) {
444
- return 'No active streak';
445
- }
446
-
447
- const isPersonalBest = streak.current === streak.longest && streak.current > 1;
448
-
449
- // Visual progression: 🔥 (1-5) → 🌟 (6-14) → 👑 (15+)
450
- let icon: string;
451
- if (streak.current >= 15) {
452
- icon = '👑';
453
- } else if (streak.current >= 6) {
454
- icon = '🌟';
455
- } else {
456
- icon = '🔥';
457
- }
458
-
459
- // Multiple icons for emphasis
460
- const repeatCount = Math.min(Math.ceil(streak.current / 5), 3);
461
- const icons = icon.repeat(repeatCount);
462
-
463
- let text = `${icons} ${streak.current}-day streak`;
464
-
465
- if (isPersonalBest && streak.current > 7) {
466
- text += ' 🏆 (Personal Best!)';
467
- }
468
-
469
- return text;
470
- }
471
-
472
- /**
473
- * Format streak with risk indicator
474
- */
475
- export function formatStreakWithRisk(streak: StreakState): string {
476
- const base = formatStreak(streak);
477
- const daysUntil = getDaysUntilExpiry(streak);
478
-
479
- if (streak.current > 0 && daysUntil <= 1) {
480
- return `${base}\n⚠️ Streak at risk! Check in today to keep it alive`;
481
- }
482
-
483
- return base;
484
- }
485
-
486
- /**
487
- * Format freezes display
488
- */
489
- export function formatFreezes(streak: StreakState): string {
490
- const freezeIcon = '❄️';
491
- return `${freezeIcon} ${streak.freezesRemaining} freeze${streak.freezesRemaining !== 1 ? 's' : ''} available`;
492
- }
493
- ```
494
-
495
- **Validation:** `vibe-check profile` shows enhanced streak display
496
-
497
- ---
498
-
499
- ## Sprint 2: Leaderboards & Records (3-4 hours)
500
-
501
- ### 2.1 Local Leaderboards
502
-
503
- **New File: `src/gamification/leaderboards.ts`**
504
-
505
- ```typescript
506
- import * as fs from 'fs';
507
- import * as path from 'path';
508
- import * as os from 'os';
509
- import { SessionRecord } from './types';
510
-
511
- const LEADERBOARD_FILE = 'leaderboards.json';
512
-
513
- export interface LeaderboardEntry {
514
- date: string;
515
- repoPath: string;
516
- repoName: string;
517
- vibeScore: number;
518
- overall: string;
519
- commits: number;
520
- xpEarned: number;
521
- }
522
-
523
- export interface Leaderboards {
524
- version: string;
525
- entries: LeaderboardEntry[]; // All-time entries (top 100)
526
- byRepo: Record<string, LeaderboardEntry[]>; // Per-repo top 10
527
- personalBests: {
528
- highestScore: LeaderboardEntry | null;
529
- longestStreak: number;
530
- bestWeekXP: { week: string; xp: number } | null;
531
- mostCommits: LeaderboardEntry | null;
532
- };
533
- }
534
-
535
- /**
536
- * Get leaderboards file path
537
- */
538
- export function getLeaderboardsPath(): string {
539
- return path.join(os.homedir(), '.vibe-check', LEADERBOARD_FILE);
540
- }
541
-
542
- /**
543
- * Load leaderboards from disk
544
- */
545
- export function loadLeaderboards(): Leaderboards {
546
- const filePath = getLeaderboardsPath();
547
-
548
- if (fs.existsSync(filePath)) {
549
- try {
550
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
551
- } catch {
552
- return createInitialLeaderboards();
553
- }
554
- }
555
-
556
- return createInitialLeaderboards();
557
- }
558
-
559
- /**
560
- * Save leaderboards to disk
561
- */
562
- export function saveLeaderboards(leaderboards: Leaderboards): void {
563
- const filePath = getLeaderboardsPath();
564
- const dir = path.dirname(filePath);
565
-
566
- if (!fs.existsSync(dir)) {
567
- fs.mkdirSync(dir, { recursive: true });
568
- }
569
-
570
- fs.writeFileSync(filePath, JSON.stringify(leaderboards, null, 2));
571
- }
572
-
573
- /**
574
- * Create initial leaderboards
575
- */
576
- export function createInitialLeaderboards(): Leaderboards {
577
- return {
578
- version: '1.0.0',
579
- entries: [],
580
- byRepo: {},
581
- personalBests: {
582
- highestScore: null,
583
- longestStreak: 0,
584
- bestWeekXP: null,
585
- mostCommits: null,
586
- },
587
- };
588
- }
589
-
590
- /**
591
- * Record a session to leaderboards
592
- */
593
- export function recordToLeaderboard(
594
- session: SessionRecord,
595
- repoPath: string,
596
- xpEarned: number,
597
- streak: number
598
- ): Leaderboards {
599
- const leaderboards = loadLeaderboards();
600
- const repoName = path.basename(repoPath);
601
-
602
- const entry: LeaderboardEntry = {
603
- date: session.date,
604
- repoPath,
605
- repoName,
606
- vibeScore: session.vibeScore,
607
- overall: session.overall,
608
- commits: session.commits,
609
- xpEarned,
610
- };
611
-
612
- // Add to global entries (sorted by score, top 100)
613
- leaderboards.entries.push(entry);
614
- leaderboards.entries.sort((a, b) => b.vibeScore - a.vibeScore);
615
- leaderboards.entries = leaderboards.entries.slice(0, 100);
616
-
617
- // Add to repo-specific entries (top 10)
618
- if (!leaderboards.byRepo[repoPath]) {
619
- leaderboards.byRepo[repoPath] = [];
620
- }
621
- leaderboards.byRepo[repoPath].push(entry);
622
- leaderboards.byRepo[repoPath].sort((a, b) => b.vibeScore - a.vibeScore);
623
- leaderboards.byRepo[repoPath] = leaderboards.byRepo[repoPath].slice(0, 10);
624
-
625
- // Update personal bests
626
- if (!leaderboards.personalBests.highestScore ||
627
- session.vibeScore > leaderboards.personalBests.highestScore.vibeScore) {
628
- leaderboards.personalBests.highestScore = entry;
629
- }
630
-
631
- if (streak > leaderboards.personalBests.longestStreak) {
632
- leaderboards.personalBests.longestStreak = streak;
633
- }
634
-
635
- if (!leaderboards.personalBests.mostCommits ||
636
- session.commits > leaderboards.personalBests.mostCommits.commits) {
637
- leaderboards.personalBests.mostCommits = entry;
638
- }
639
-
640
- // Track weekly XP
641
- const weekStart = getWeekStartISO(new Date(session.date));
642
- const currentWeekXP = leaderboards.personalBests.bestWeekXP;
643
- if (!currentWeekXP || currentWeekXP.week !== weekStart) {
644
- // New week - check if we beat previous best
645
- // (simplified - in practice would need to sum week's XP)
646
- leaderboards.personalBests.bestWeekXP = { week: weekStart, xp: xpEarned };
647
- } else {
648
- currentWeekXP.xp += xpEarned;
649
- }
650
-
651
- saveLeaderboards(leaderboards);
652
- return leaderboards;
653
- }
654
-
655
- /**
656
- * Format leaderboard for display
657
- */
658
- export function formatLeaderboard(leaderboards: Leaderboards, repoPath?: string): string {
659
- const lines: string[] = [];
660
-
661
- if (repoPath && leaderboards.byRepo[repoPath]) {
662
- const repoEntries = leaderboards.byRepo[repoPath];
663
- lines.push(`📊 Top Scores - ${path.basename(repoPath)}`);
664
- lines.push('');
665
-
666
- for (let i = 0; i < Math.min(repoEntries.length, 5); i++) {
667
- const e = repoEntries[i];
668
- const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : ' ';
669
- lines.push(`${medal} ${e.vibeScore}% ${e.overall.padEnd(6)} ${e.date}`);
670
- }
671
- } else {
672
- lines.push('🏆 All-Time Top Scores');
673
- lines.push('');
674
-
675
- for (let i = 0; i < Math.min(leaderboards.entries.length, 10); i++) {
676
- const e = leaderboards.entries[i];
677
- const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
678
- lines.push(`${medal} ${e.vibeScore}% ${e.overall.padEnd(6)} ${e.repoName} (${e.date})`);
679
- }
680
- }
681
-
682
- return lines.join('\n');
683
- }
684
-
685
- function getWeekStartISO(date: Date): string {
686
- const d = new Date(date);
687
- const day = d.getDay();
688
- const diff = d.getDate() - day + (day === 0 ? -6 : 1);
689
- d.setDate(diff);
690
- return d.toISOString().split('T')[0];
691
- }
692
- ```
693
-
694
- **Validation:** `npm run build` succeeds
695
-
696
- ---
697
-
698
- ### 2.2 Hall of Fame (Personal Bests)
699
-
700
- **New File: `src/gamification/hall-of-fame.ts`**
701
-
702
- ```typescript
703
- import { Leaderboards } from './leaderboards';
704
-
705
- export interface HallOfFameRecord {
706
- category: string;
707
- icon: string;
708
- value: string;
709
- date: string;
710
- context: string;
711
- }
712
-
713
- /**
714
- * Get Hall of Fame records from leaderboards
715
- */
716
- export function getHallOfFame(leaderboards: Leaderboards): HallOfFameRecord[] {
717
- const records: HallOfFameRecord[] = [];
718
- const pb = leaderboards.personalBests;
719
-
720
- if (pb.highestScore) {
721
- records.push({
722
- category: 'Best Vibe Score',
723
- icon: '🏆',
724
- value: `${pb.highestScore.vibeScore}%`,
725
- date: pb.highestScore.date,
726
- context: `${pb.highestScore.repoName} - ${pb.highestScore.overall}`,
727
- });
728
- }
729
-
730
- if (pb.longestStreak > 0) {
731
- records.push({
732
- category: 'Longest Streak',
733
- icon: '🔥',
734
- value: `${pb.longestStreak} days`,
735
- date: 'All time',
736
- context: 'Consecutive daily check-ins',
737
- });
738
- }
739
-
740
- if (pb.bestWeekXP) {
741
- records.push({
742
- category: 'Best Week XP',
743
- icon: '⚡',
744
- value: `${pb.bestWeekXP.xp} XP`,
745
- date: pb.bestWeekXP.week,
746
- context: 'Most XP earned in a single week',
747
- });
748
- }
749
-
750
- if (pb.mostCommits) {
751
- records.push({
752
- category: 'Most Commits',
753
- icon: '📊',
754
- value: `${pb.mostCommits.commits}`,
755
- date: pb.mostCommits.date,
756
- context: `${pb.mostCommits.repoName} - single session`,
757
- });
758
- }
759
-
760
- return records;
761
- }
762
-
763
- /**
764
- * Format Hall of Fame for display
765
- */
766
- export function formatHallOfFame(records: HallOfFameRecord[]): string {
767
- if (records.length === 0) {
768
- return 'No records yet. Keep coding!';
769
- }
770
-
771
- const lines: string[] = ['🏛️ HALL OF FAME', ''];
772
-
773
- for (const record of records) {
774
- lines.push(`${record.icon} ${record.category}: ${record.value}`);
775
- lines.push(` ${record.date} - ${record.context}`);
776
- lines.push('');
777
- }
778
-
779
- return lines.join('\n');
780
- }
781
- ```
782
-
783
- ---
784
-
785
- ### 2.3 Weekly Stats with Sparklines
786
-
787
- **New File: `src/gamification/stats.ts`**
788
-
789
- ```typescript
790
- import { SessionRecord } from './types';
791
-
792
- /**
793
- * Create a sparkline from values
794
- */
795
- export function createSparkline(values: number[]): string {
796
- if (values.length === 0) return '';
797
-
798
- const chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
799
- const min = Math.min(...values);
800
- const max = Math.max(...values);
801
- const range = max - min || 1;
802
-
803
- return values
804
- .map(v => chars[Math.floor(((v - min) / range) * 7)])
805
- .join('');
806
- }
807
-
808
- /**
809
- * Get trend indicator
810
- */
811
- export function getTrend(values: number[]): { direction: 'up' | 'down' | 'stable'; emoji: string } {
812
- if (values.length < 2) return { direction: 'stable', emoji: '➡️' };
813
-
814
- const recent = values.slice(-3);
815
- const older = values.slice(0, 3);
816
-
817
- const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length;
818
- const olderAvg = older.reduce((a, b) => a + b, 0) / older.length;
819
-
820
- const diff = recentAvg - olderAvg;
821
-
822
- if (diff > 5) return { direction: 'up', emoji: '↑' };
823
- if (diff < -5) return { direction: 'down', emoji: '↓' };
824
- return { direction: 'stable', emoji: '→' };
825
- }
826
-
827
- export interface WeeklyStats {
828
- avgScore: number;
829
- sessions: number;
830
- totalCommits: number;
831
- eliteCount: number;
832
- spiralCount: number;
833
- sparkline: string;
834
- trend: { direction: string; emoji: string };
835
- xpEarned: number;
836
- }
837
-
838
- /**
839
- * Calculate weekly statistics
840
- */
841
- export function getWeeklyStats(sessions: SessionRecord[]): WeeklyStats {
842
- const weekStart = getWeekStartDate(new Date());
843
- const weekSessions = sessions.filter(s => new Date(s.date) >= weekStart);
844
-
845
- if (weekSessions.length === 0) {
846
- return {
847
- avgScore: 0,
848
- sessions: 0,
849
- totalCommits: 0,
850
- eliteCount: 0,
851
- spiralCount: 0,
852
- sparkline: '',
853
- trend: { direction: 'stable', emoji: '→' },
854
- xpEarned: 0,
855
- };
856
- }
857
-
858
- const scores = weekSessions.map(s => s.vibeScore);
859
-
860
- return {
861
- avgScore: Math.round(scores.reduce((a, b) => a + b, 0) / scores.length),
862
- sessions: weekSessions.length,
863
- totalCommits: weekSessions.reduce((sum, s) => sum + s.commits, 0),
864
- eliteCount: weekSessions.filter(s => s.overall === 'ELITE').length,
865
- spiralCount: weekSessions.reduce((sum, s) => sum + s.spirals, 0),
866
- sparkline: createSparkline(scores),
867
- trend: getTrend(scores),
868
- xpEarned: weekSessions.reduce((sum, s) => sum + s.xpEarned, 0),
869
- };
870
- }
871
-
872
- /**
873
- * Format weekly stats for display
874
- */
875
- export function formatWeeklyStats(stats: WeeklyStats): string {
876
- const lines: string[] = [];
877
-
878
- lines.push('📅 THIS WEEK');
879
- lines.push(` Avg Score: ${stats.avgScore}% ${stats.trend.emoji}`);
880
- lines.push(` Sessions: ${stats.sessions}`);
881
- lines.push(` XP Earned: ${stats.xpEarned}`);
882
- lines.push(` ELITE: ${stats.eliteCount} | Spirals: ${stats.spiralCount}`);
883
-
884
- if (stats.sparkline) {
885
- lines.push(` Trend: ${stats.sparkline}`);
886
- }
887
-
888
- return lines.join('\n');
889
- }
890
-
891
- function getWeekStartDate(date: Date): Date {
892
- const d = new Date(date);
893
- const day = d.getDay();
894
- const diff = d.getDate() - day + (day === 0 ? -6 : 1);
895
- d.setDate(diff);
896
- d.setHours(0, 0, 0, 0);
897
- return d;
898
- }
899
- ```
900
-
901
- ---
902
-
903
- ## Sprint 3: Polish & Social (3-4 hours)
904
-
905
- ### 3.1 Rank Badges
906
-
907
- **New File: `src/gamification/badges.ts`**
908
-
909
- ```typescript
910
- export interface RankBadge {
911
- id: string;
912
- name: string;
913
- icon: string;
914
- color: string;
915
- requirement: string;
916
- threshold: {
917
- type: 'sessions' | 'streak' | 'xp' | 'score';
918
- value: number;
919
- };
920
- }
921
-
922
- export const RANK_BADGES: RankBadge[] = [
923
- {
924
- id: 'bronze',
925
- name: 'Bronze',
926
- icon: '🥉',
927
- color: '#CD7F32',
928
- requirement: '10 sessions',
929
- threshold: { type: 'sessions', value: 10 },
930
- },
931
- {
932
- id: 'silver',
933
- name: 'Silver',
934
- icon: '🥈',
935
- color: '#C0C0C0',
936
- requirement: '50 sessions',
937
- threshold: { type: 'sessions', value: 50 },
938
- },
939
- {
940
- id: 'gold',
941
- name: 'Gold',
942
- icon: '🥇',
943
- color: '#FFD700',
944
- requirement: '100 sessions',
945
- threshold: { type: 'sessions', value: 100 },
946
- },
947
- {
948
- id: 'platinum',
949
- name: 'Platinum',
950
- icon: '💎',
951
- color: '#E5E4E2',
952
- requirement: '14-day streak',
953
- threshold: { type: 'streak', value: 14 },
954
- },
955
- {
956
- id: 'diamond',
957
- name: 'Diamond',
958
- icon: '🔷',
959
- color: '#B9F2FF',
960
- requirement: '5000+ XP',
961
- threshold: { type: 'xp', value: 5000 },
962
- },
963
- ];
964
-
965
- /**
966
- * Get current rank badge
967
- */
968
- export function getCurrentBadge(
969
- sessions: number,
970
- streak: number,
971
- xp: number
972
- ): RankBadge | null {
973
- // Check from highest to lowest
974
- for (let i = RANK_BADGES.length - 1; i >= 0; i--) {
975
- const badge = RANK_BADGES[i];
976
- const { type, value } = badge.threshold;
977
-
978
- switch (type) {
979
- case 'sessions':
980
- if (sessions >= value) return badge;
981
- break;
982
- case 'streak':
983
- if (streak >= value) return badge;
984
- break;
985
- case 'xp':
986
- if (xp >= value) return badge;
987
- break;
988
- }
989
- }
990
-
991
- return null;
992
- }
993
-
994
- /**
995
- * Get next badge and progress
996
- */
997
- export function getNextBadge(
998
- sessions: number,
999
- streak: number,
1000
- xp: number
1001
- ): { badge: RankBadge; progress: number; remaining: string } | null {
1002
- const current = getCurrentBadge(sessions, streak, xp);
1003
- const currentIdx = current ? RANK_BADGES.findIndex(b => b.id === current.id) : -1;
1004
-
1005
- if (currentIdx >= RANK_BADGES.length - 1) return null;
1006
-
1007
- const next = RANK_BADGES[currentIdx + 1];
1008
- const { type, value } = next.threshold;
1009
-
1010
- let currentValue: number;
1011
- let unit: string;
1012
-
1013
- switch (type) {
1014
- case 'sessions':
1015
- currentValue = sessions;
1016
- unit = 'sessions';
1017
- break;
1018
- case 'streak':
1019
- currentValue = streak;
1020
- unit = 'day streak';
1021
- break;
1022
- case 'xp':
1023
- currentValue = xp;
1024
- unit = 'XP';
1025
- break;
1026
- default:
1027
- return null;
1028
- }
1029
-
1030
- const progress = Math.min((currentValue / value) * 100, 99);
1031
- const remaining = `${value - currentValue} ${unit}`;
1032
-
1033
- return { badge: next, progress, remaining };
1034
- }
1035
-
1036
- /**
1037
- * Format badge display
1038
- */
1039
- export function formatBadge(badge: RankBadge | null): string {
1040
- if (!badge) return 'No badge yet';
1041
- return `${badge.icon} ${badge.name} Tier`;
1042
- }
1043
- ```
1044
-
1045
- ---
1046
-
1047
- ### 3.2 Share-to-Clipboard
1048
-
1049
- **New File: `src/gamification/share.ts`**
1050
-
1051
- ```typescript
1052
- import { UserProfile } from './types';
1053
- import { getCurrentBadge } from './badges';
1054
- import { LEVELS, PRESTIGE_TIERS } from './types';
1055
-
1056
- export interface ShareableProfile {
1057
- username?: string;
1058
- level: number;
1059
- levelName: string;
1060
- totalXP: number;
1061
- badge: string | null;
1062
- streak: number;
1063
- longestStreak: number;
1064
- totalSessions: number;
1065
- avgScore: number;
1066
- bestScore: number;
1067
- achievementCount: number;
1068
- prestigeTier?: number;
1069
- generatedAt: string;
1070
- }
1071
-
1072
- /**
1073
- * Create shareable profile data
1074
- */
1075
- export function createShareableProfile(
1076
- profile: UserProfile,
1077
- username?: string
1078
- ): ShareableProfile {
1079
- const badge = getCurrentBadge(
1080
- profile.stats.totalSessions,
1081
- profile.streak.longest,
1082
- profile.xp.total
1083
- );
1084
-
1085
- return {
1086
- username,
1087
- level: profile.xp.level,
1088
- levelName: profile.xp.levelName,
1089
- totalXP: profile.xp.total,
1090
- badge: badge?.name || null,
1091
- streak: profile.streak.current,
1092
- longestStreak: profile.streak.longest,
1093
- totalSessions: profile.stats.totalSessions,
1094
- avgScore: profile.stats.avgVibeScore,
1095
- bestScore: profile.stats.bestVibeScore,
1096
- achievementCount: profile.achievements.length,
1097
- prestigeTier: profile.xp.prestigeTier,
1098
- generatedAt: new Date().toISOString(),
1099
- };
1100
- }
1101
-
1102
- /**
1103
- * Format profile for sharing (text format)
1104
- */
1105
- export function formatShareText(shareable: ShareableProfile): string {
1106
- const lines: string[] = [];
1107
-
1108
- const name = shareable.username || 'Anonymous Coder';
1109
- const badgeStr = shareable.badge ? ` ${shareable.badge}` : '';
1110
- const prestigeStr = shareable.prestigeTier
1111
- ? ` (${PRESTIGE_TIERS[shareable.prestigeTier - 1].name})`
1112
- : '';
1113
-
1114
- lines.push(`🎮 ${name}'s Vibe-Check Profile`);
1115
- lines.push('');
1116
- lines.push(`Level ${shareable.level} ${shareable.levelName}${prestigeStr}${badgeStr}`);
1117
- lines.push(`${shareable.totalXP.toLocaleString()} Total XP`);
1118
- lines.push('');
1119
- lines.push(`🔥 ${shareable.streak}-day streak (Best: ${shareable.longestStreak})`);
1120
- lines.push(`📊 ${shareable.totalSessions} sessions | Avg: ${shareable.avgScore}% | Best: ${shareable.bestScore}%`);
1121
- lines.push(`🏆 ${shareable.achievementCount} achievements unlocked`);
1122
- lines.push('');
1123
- lines.push('Track your coding vibes: npx @boshu2/vibe-check');
1124
-
1125
- return lines.join('\n');
1126
- }
1127
-
1128
- /**
1129
- * Format profile as JSON for clipboard
1130
- */
1131
- export function formatShareJSON(shareable: ShareableProfile): string {
1132
- return JSON.stringify(shareable, null, 2);
1133
- }
1134
- ```
1135
-
1136
- ---
1137
-
1138
- ### 3.3 Near-Miss Psychology
1139
-
1140
- **Modify: `src/output/terminal.ts`**
1141
-
1142
- Add after line 126 (after opportunities section):
1143
-
1144
- ```typescript
1145
- // Near-miss psychology - motivational close calls
1146
- const nearMisses = getNearMisses(result, metrics);
1147
- if (nearMisses.length > 0) {
1148
- lines.push('');
1149
- lines.push(chalk.bold.magenta(' 🎯 SO CLOSE!'));
1150
- for (const miss of nearMisses) {
1151
- lines.push(chalk.magenta(` ${miss}`));
1152
- }
1153
- }
1154
- ```
1155
-
1156
- Add function at end of file:
1157
-
1158
- ```typescript
1159
- /**
1160
- * Find near-miss motivational messages
1161
- */
1162
- function getNearMisses(result: VibeCheckResult | VibeCheckResultV2, metrics: any[]): string[] {
1163
- const misses: string[] = [];
1164
-
1165
- // Check for near-ELITE overall
1166
- if (result.overall === 'HIGH') {
1167
- const eliteCount = metrics.filter(m => m.metric.rating === 'elite').length;
1168
- if (eliteCount >= 3) {
1169
- misses.push('Just 1-2 metrics away from ELITE overall!');
1170
- }
1171
- }
1172
-
1173
- // Check for near-90% score
1174
- if (isV2Result(result) && result.vibeScore) {
1175
- const score = result.vibeScore.value * 100;
1176
- if (score >= 85 && score < 90) {
1177
- misses.push(`${Math.round(score)}% vibe score - just ${90 - Math.round(score)}% from the Ninety Club!`);
1178
- }
1179
- }
1180
-
1181
- // Check for near-perfect trust
1182
- const trust = result.metrics.trustPassRate;
1183
- if (trust.value >= 90 && trust.value < 100 && trust.rating !== 'elite') {
1184
- misses.push(`${trust.value}% trust - ${100 - trust.value}% from Perfect Trust!`);
1185
- }
1186
-
1187
- // Check for near-zero spirals
1188
- if (result.fixChains.length === 1 && result.commits.total >= 20) {
1189
- misses.push('Only 1 spiral! Next time could be Zen Master territory.');
1190
- }
1191
-
1192
- return misses.slice(0, 2); // Max 2 near-misses
1193
- }
1194
- ```
1195
-
1196
- ---
1197
-
1198
- ## Integration: Update Profile Command
1199
-
1200
- **Modify: `src/commands/profile.ts`**
1201
-
1202
- Add new imports:
1203
-
1204
- ```typescript
1205
- import { getCurrentChallenges, formatChallenges } from '../gamification/challenges';
1206
- import { loadLeaderboards, formatLeaderboard } from '../gamification/leaderboards';
1207
- import { getHallOfFame, formatHallOfFame } from '../gamification/hall-of-fame';
1208
- import { getWeeklyStats, formatWeeklyStats } from '../gamification/stats';
1209
- import { getCurrentBadge, getNextBadge, formatBadge } from '../gamification/badges';
1210
- import { createShareableProfile, formatShareText } from '../gamification/share';
1211
- import { formatStreakWithRisk, formatFreezes } from '../gamification/streaks';
1212
- import { PRESTIGE_TIERS } from '../gamification/types';
1213
- ```
1214
-
1215
- Add new options:
1216
-
1217
- ```typescript
1218
- .option('--challenges', 'Show weekly challenges', false)
1219
- .option('--leaderboard', 'Show personal leaderboard', false)
1220
- .option('--hall-of-fame', 'Show Hall of Fame records', false)
1221
- .option('--weekly', 'Show this week stats', false)
1222
- .option('--share', 'Copy shareable profile to stdout', false)
1223
- ```
1224
-
1225
- Update display in `runProfile` to show new elements (challenges, badge, prestige).
1226
-
1227
- ---
1228
-
1229
- ## Validation Strategy
1230
-
1231
- ### Syntax Validation
1232
- ```bash
1233
- npm run build
1234
- # Expected: Compiles without errors
1235
- ```
1236
-
1237
- ### Unit Tests
1238
- ```bash
1239
- npm test
1240
- # Expected: All tests pass
1241
- ```
1242
-
1243
- ### Integration Testing
1244
- ```bash
1245
- # Test prestige
1246
- node dist/cli.js profile
1247
- # Expected: Shows prestige tier if XP > 5000
1248
-
1249
- # Test challenges
1250
- node dist/cli.js profile --challenges
1251
- # Expected: Shows 3 weekly challenges with progress
1252
-
1253
- # Test leaderboard
1254
- node dist/cli.js profile --leaderboard
1255
- # Expected: Shows personal high scores
1256
-
1257
- # Test share
1258
- node dist/cli.js profile --share
1259
- # Expected: Outputs shareable JSON
1260
- ```
1261
-
1262
- ---
1263
-
1264
- ## Implementation Order
1265
-
1266
- **CRITICAL: Follow this sequence**
1267
-
1268
- | Step | Action | Validation | Rollback |
1269
- |------|--------|------------|----------|
1270
- | 1 | Create `challenges.ts` | `npm run build` | Delete file |
1271
- | 2 | Update `types.ts` (prestige + challenges) | `npm run build` | Revert changes |
1272
- | 3 | Update `xp.ts` (prestige logic) | `npm test` | Revert changes |
1273
- | 4 | Update `streaks.ts` (enhanced display) | `npm test` | Revert changes |
1274
- | 5 | Create `leaderboards.ts` | `npm run build` | Delete file |
1275
- | 6 | Create `hall-of-fame.ts` | `npm run build` | Delete file |
1276
- | 7 | Create `stats.ts` | `npm run build` | Delete file |
1277
- | 8 | Create `badges.ts` | `npm run build` | Delete file |
1278
- | 9 | Create `share.ts` | `npm run build` | Delete file |
1279
- | 10 | Update `terminal.ts` (near-miss) | `npm run build` | Revert changes |
1280
- | 11 | Update `profile.ts` command | `npm test` | Revert changes |
1281
- | 12 | Full integration test | `npm test && npm run build` | Revert all |
1282
-
1283
- ---
1284
-
1285
- ## Rollback Procedure
1286
-
1287
- **Time to rollback:** 5 minutes
1288
-
1289
- ### Full Rollback
1290
- ```bash
1291
- git checkout HEAD -- src/
1292
- rm -f src/gamification/challenges.ts
1293
- rm -f src/gamification/leaderboards.ts
1294
- rm -f src/gamification/hall-of-fame.ts
1295
- rm -f src/gamification/stats.ts
1296
- rm -f src/gamification/badges.ts
1297
- rm -f src/gamification/share.ts
1298
- npm run build
1299
- npm test
1300
- ```
1301
-
1302
- ---
1303
-
1304
- ## Risk Assessment
1305
-
1306
- ### Low Risk
1307
- - All features are additive (no breaking changes)
1308
- - Existing profiles auto-migrate
1309
- - No new npm dependencies
1310
-
1311
- ### Medium Risk
1312
- - XP/prestige calculation changes could affect existing users
1313
- - **Mitigation:** Prestige only kicks in at 5000+ XP (most users below)
1314
-
1315
- ---
1316
-
1317
- ## Approval Checklist
1318
-
1319
- **Human must verify before `/implement`:**
1320
-
1321
- - [ ] Every file specified precisely (file:line)
1322
- - [ ] All templates complete (no placeholders)
1323
- - [ ] Validation commands provided
1324
- - [ ] Rollback procedure complete
1325
- - [ ] Implementation order is correct
1326
- - [ ] No new dependencies needed
1327
-
1328
- ---
1329
-
1330
- ## Target Output
1331
-
1332
- After implementation, `vibe-check profile` will show:
1333
-
1334
- ```
1335
- ╔════════════════════════════════════════════════════════════╗
1336
- ║ 🏔️ ARCHMAGE I (Prestige 1) 💎 Platinum Tier ║
1337
- ╠════════════════════════════════════════════════════════════╣
1338
- ║ XP: ████████████░░░░ 1,250 / 2,000 ║
1339
- ║ 👑👑 12-day streak 🏆 (Personal Best!) ║
1340
- ║ ❄️ 2 freezes available ║
1341
- ╠════════════════════════════════════════════════════════════╣
1342
- ║ WEEKLY CHALLENGES ║
1343
- ║ 🎯 Trust Gauntlet: ████████░░ 4/5 ║
1344
- ║ 🧘 Zen Mode: ██████████ ✓ COMPLETE (+100 XP) ║
1345
- ║ 🔥 Streak Builder: ██░░░░░░░░ 1/5 ║
1346
- ╠════════════════════════════════════════════════════════════╣
1347
- ║ 📅 THIS WEEK ║
1348
- ║ Avg Score: 87% ↑ ║
1349
- ║ Trend: ▂▄▃▆▅██ Improving! ║
1350
- ╠════════════════════════════════════════════════════════════╣
1351
- ║ 🏛️ HALL OF FAME ║
1352
- ║ 🏆 Best Score: 96% (Nov 15) ║
1353
- ║ 🔥 Longest Streak: 15 days ║
1354
- ║ ⚡ Best Week: 847 XP ║
1355
- ╚════════════════════════════════════════════════════════════╝
1356
- ```
1357
-
1358
- ---
1359
-
1360
- ## Next Step
1361
-
1362
- Once approved: `/implement PLAN-ultimate-game.md`