@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
package/PLAN-ultimate-game.md
DELETED
|
@@ -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`
|