ocs-stats 1.1.1 → 1.1.4

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/README.md CHANGED
@@ -177,11 +177,11 @@ The testing agent helps you write tests with an XP-based leveling system:
177
177
  | Level | Title | XP Required | Focus |
178
178
  |-------|-------|-------------|-------|
179
179
  | 1 | Novice | 0 | Basic unit tests |
180
- | 2 | Apprentice | 100 | Integration tests |
181
- | 3 | Practitioner | 300 | E2E tests |
182
- | 4 | Expert | 600 | Test patterns & mocking |
183
- | 5 | Master | 1,200 | Full coverage strategies |
184
- | 6 | Grandmaster | 2,500 | Testing excellence |
180
+ | 2 | Apprentice | 150 | Integration tests |
181
+ | 3 | Practitioner | 450 | E2E tests |
182
+ | 4 | Expert | 900 | Test patterns & mocking |
183
+ | 5 | Master | 1,500 | Full coverage strategies |
184
+ | 6 | Grandmaster | 3,000 | Testing excellence |
185
185
 
186
186
  ### XP Awards
187
187
 
package/bin/cli.js CHANGED
@@ -7,6 +7,8 @@ const args = process.argv.slice(2);
7
7
  const command = args[0];
8
8
  const isGlobal = args.includes('--global') || args.includes('-g');
9
9
  const isHelp = args.includes('--help') || args.includes('-h');
10
+ const isForce = args.includes('--force') || args.includes('-f');
11
+ const isCheck = args.includes('--check') || args.includes('-c');
10
12
 
11
13
  if (isHelp) {
12
14
  console.log(`
@@ -15,7 +17,9 @@ ocs-stats - Install OpenCode skills and agents
15
17
  Usage:
16
18
  npx ocs-stats Install to current project
17
19
  npx ocs-stats --global Install globally (~/.opencode)
18
- npx ocs-stats update Update skills (removes existing)
20
+ npx ocs-stats update Update skills (smart merge)
21
+ npx ocs-stats update --check Check for updates
22
+ npx ocs-stats update --force Force fresh install
19
23
  npx ocs-stats stats Show security agent progress
20
24
  npx ocs-stats stats testing Show testing agent progress
21
25
  npx ocs-stats display-xp <amount> "<reason>"
@@ -23,11 +27,15 @@ Usage:
23
27
 
24
28
  Options:
25
29
  -g, --global Install to user home directory
30
+ -f, --force Force fresh install (delete + copy)
31
+ -c, --check Check for updates without applying
26
32
  -h, --help Show this help message
27
33
 
28
34
  Examples:
29
35
  npx ocs-stats
30
36
  npx ocs-stats update
37
+ npx ocs-stats update --check
38
+ npx ocs-stats update --force
31
39
  npx ocs-stats stats
32
40
  npx ocs-stats stats testing
33
41
  npx ocs-stats display-xp 35 "Fixed high issue"
@@ -36,21 +44,22 @@ Examples:
36
44
  process.exit(0);
37
45
  }
38
46
 
39
- if (command === 'update') {
40
- update({ isGlobal }).catch((err) => {
41
- console.error('Error:', err.message);
42
- process.exit(1);
43
- });
44
- process.exit(0);
47
+ else if (command === 'update') {
48
+ update({ isGlobal, force: isForce, checkOnly: isCheck })
49
+ .then(() => process.exit(0))
50
+ .catch((err) => {
51
+ console.error('Error:', err.message);
52
+ process.exit(1);
53
+ });
45
54
  }
46
55
 
47
- if (command === 'stats') {
56
+ else if (command === 'stats') {
48
57
  const category = args[1] || 'security';
49
58
  stats(category);
50
59
  process.exit(0);
51
60
  }
52
61
 
53
- if (command === 'display-xp') {
62
+ else if (command === 'display-xp') {
54
63
  const amount = args[1];
55
64
  const reason = args.slice(2).join(' ') || 'XP earned';
56
65
  const category = reason.includes('[testing]') ? 'testing' : 'security';
@@ -59,7 +68,9 @@ if (command === 'display-xp') {
59
68
  process.exit(0);
60
69
  }
61
70
 
62
- init({ isGlobal }).catch((err) => {
63
- console.error('Error:', err.message);
64
- process.exit(1);
65
- });
71
+ else {
72
+ init({ isGlobal }).catch((err) => {
73
+ console.error('Error:', err.message);
74
+ process.exit(1);
75
+ });
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocs-stats",
3
- "version": "1.1.1",
3
+ "version": "1.1.4",
4
4
  "description": "OpenCode Skills - One-click installer with gamified XP stats",
5
5
  "type": "module",
6
6
  "bin": {
package/src/init.js CHANGED
@@ -1,12 +1,32 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { performUpdate, checkForUpdates, formatUpdateSummary } from './merge.js';
4
5
 
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = path.dirname(__filename);
7
8
 
8
9
  const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
9
10
 
11
+ function copyDir(src, dest) {
12
+ fs.mkdirSync(dest, { recursive: true });
13
+
14
+ const entries = fs.readdirSync(src, { withFileTypes: true });
15
+
16
+ for (const entry of entries) {
17
+ if (entry.name === '.git') continue;
18
+
19
+ const srcPath = path.join(src, entry.name);
20
+ const destPath = path.join(dest, entry.name);
21
+
22
+ if (entry.isDirectory()) {
23
+ copyDir(srcPath, destPath);
24
+ } else {
25
+ fs.copyFileSync(srcPath, destPath);
26
+ }
27
+ }
28
+ }
29
+
10
30
  export async function init({ isGlobal = false } = {}) {
11
31
  const targetDir = isGlobal
12
32
  ? path.join(process.env.HOME || process.env.USERPROFILE, '.opencode')
@@ -43,7 +63,7 @@ export async function init({ isGlobal = false } = {}) {
43
63
  }
44
64
  }
45
65
 
46
- export async function update({ isGlobal = false } = {}) {
66
+ export async function update({ isGlobal = false, force = false, checkOnly = false } = {}) {
47
67
  const targetDir = isGlobal
48
68
  ? path.join(process.env.HOME || process.env.USERPROFILE, '.opencode')
49
69
  : path.join(process.cwd(), '.opencode');
@@ -55,35 +75,32 @@ export async function update({ isGlobal = false } = {}) {
55
75
  process.exit(1);
56
76
  }
57
77
 
58
- // Remove existing
59
- fs.rmSync(targetDir, { recursive: true, force: true });
60
- console.log(' Removed existing .opencode folder');
61
-
62
- // Reinstall
63
- copyDir(TEMPLATES_DIR, targetDir);
64
- console.log(' Installed fresh copy\n');
78
+ if (force) {
79
+ console.log(' Force mode: Removing existing .opencode folder');
80
+ fs.rmSync(targetDir, { recursive: true, force: true });
81
+ copyDir(TEMPLATES_DIR, targetDir);
82
+ console.log(' Fresh install complete\n');
83
+ return;
84
+ }
65
85
 
66
- console.log('Updated successfully!\n');
67
- console.log('What was installed:');
68
- console.log(' * Agents: security, testing');
69
- console.log(' * Skills: commit, memories, mobile, security, testing, webapp');
70
- console.log(' * Security: XP tracking, knowledge base');
71
- console.log(' * Testing: XP tracking, knowledge base\n');
72
- }
86
+ if (checkOnly) {
87
+ console.log(' Checking for updates...\n');
88
+ const updateInfo = await checkForUpdates(targetDir);
89
+ if (!updateInfo.hasUpdates) {
90
+ console.log(` Already up to date (v${updateInfo.currentVersion})\n`);
91
+ return;
92
+ }
93
+ formatUpdateSummary({ ...updateInfo, results: updateInfo.changes }, true);
94
+ return;
95
+ }
73
96
 
74
- function copyDir(src, dest) {
75
- fs.mkdirSync(dest, { recursive: true });
76
-
77
- const entries = fs.readdirSync(src, { withFileTypes: true });
97
+ console.log(' Updating...\n');
98
+ const updateResult = await performUpdate(targetDir, { force, checkOnly });
78
99
 
79
- for (const entry of entries) {
80
- const srcPath = path.join(src, entry.name);
81
- const destPath = path.join(dest, entry.name);
82
-
83
- if (entry.isDirectory()) {
84
- copyDir(srcPath, destPath);
85
- } else {
86
- fs.copyFileSync(srcPath, destPath);
87
- }
100
+ if (!updateResult.updated) {
101
+ console.log(` Already up to date (v${updateResult.currentVersion})\n`);
102
+ return;
88
103
  }
104
+
105
+ formatUpdateSummary(updateResult, false);
89
106
  }
package/src/merge.js ADDED
@@ -0,0 +1,351 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import crypto from 'crypto';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
10
+
11
+ export const FILE_STATE = {
12
+ NEW: 'new',
13
+ UNMODIFIED: 'unmodified',
14
+ MODIFIED: 'modified',
15
+ DELETED: 'deleted',
16
+ };
17
+
18
+ export const FILE_CATEGORY = {
19
+ XP_DATA: 'xp_data',
20
+ MEMORIES: 'memories',
21
+ KNOWLEDGE: 'knowledge',
22
+ SKILL: 'skill',
23
+ AGENT: 'agent',
24
+ };
25
+
26
+ const PROTECTED_CATEGORIES = [FILE_CATEGORY.XP_DATA, FILE_CATEGORY.MEMORIES];
27
+ const MERGE_CATEGORIES = [FILE_CATEGORY.KNOWLEDGE];
28
+
29
+ function hashFile(filePath) {
30
+ if (!fs.existsSync(filePath)) return null;
31
+ const content = fs.readFileSync(filePath, 'utf-8');
32
+ return crypto.createHash('md5').update(content).digest('hex');
33
+ }
34
+
35
+ function getCategory(relativePath) {
36
+ const pathLower = relativePath.toLowerCase();
37
+
38
+ if (pathLower.includes('/xp.json') || pathLower.endsWith('xp.json')) {
39
+ return FILE_CATEGORY.XP_DATA;
40
+ }
41
+ if (pathLower.includes('/memories/') || pathLower.includes('memories/')) {
42
+ return FILE_CATEGORY.MEMORIES;
43
+ }
44
+ if (pathLower.includes('/knowledge.md')) {
45
+ return FILE_CATEGORY.KNOWLEDGE;
46
+ }
47
+ if (pathLower.includes('/skills/')) {
48
+ return FILE_CATEGORY.SKILL;
49
+ }
50
+ if (pathLower.includes('/agents/')) {
51
+ return FILE_CATEGORY.AGENT;
52
+ }
53
+ return FILE_CATEGORY.SKILL;
54
+ }
55
+
56
+ function compareVersion(v1, v2) {
57
+ const parts1 = v1.split('.').map(Number);
58
+ const parts2 = v2.split('.').map(Number);
59
+ for (let i = 0; i < 3; i++) {
60
+ if (parts1[i] > parts2[i]) return 1;
61
+ if (parts1[i] < parts2[i]) return -1;
62
+ }
63
+ return 0;
64
+ }
65
+
66
+ function getAllTemplateFiles(srcDir, baseDir = '') {
67
+ const files = [];
68
+ if (!fs.existsSync(srcDir)) return files;
69
+
70
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ if (entry.name === '.git') continue;
73
+
74
+ const relativePath = path.join(baseDir, entry.name);
75
+ const srcPath = path.join(srcDir, entry.name);
76
+
77
+ if (entry.isDirectory()) {
78
+ files.push(...getAllTemplateFiles(srcPath, relativePath));
79
+ } else {
80
+ files.push(relativePath);
81
+ }
82
+ }
83
+ return files;
84
+ }
85
+
86
+ function findNewSections(oldContent, newContent) {
87
+ const newLines = newContent.split('\n');
88
+ const oldLines = oldContent.split('\n');
89
+ const newSections = [];
90
+
91
+ let inNewSection = false;
92
+ let newSectionStart = -1;
93
+
94
+ for (let i = 0; i < newLines.length; i++) {
95
+ const line = newLines[i];
96
+ const isHeader = /^#{1,3}\s+/.test(line);
97
+
98
+ if (isHeader && !oldContent.includes(line)) {
99
+ if (inNewSection && newSectionStart > -1) {
100
+ newSections.push(newLines.slice(newSectionStart, i).join('\n'));
101
+ }
102
+ newSectionStart = i;
103
+ inNewSection = true;
104
+ }
105
+ }
106
+
107
+ if (inNewSection && newSectionStart > -1) {
108
+ newSections.push(newLines.slice(newSectionStart).join('\n'));
109
+ }
110
+
111
+ return newSections;
112
+ }
113
+
114
+ function mergeKnowledgeFiles(userContent, templateContent) {
115
+ const newSections = findNewSections(userContent, templateContent);
116
+
117
+ if (newSections.length === 0) {
118
+ return { action: 'unchanged', content: userContent };
119
+ }
120
+
121
+ let merged = userContent;
122
+
123
+ for (const section of newSections) {
124
+ merged += '\n\n---\n\n' + section;
125
+ }
126
+
127
+ return { action: 'merged', content: merged };
128
+ }
129
+
130
+ export async function checkForUpdates(targetDir) {
131
+ const versionFile = path.join(targetDir, '.ocs-version');
132
+ const currentVersion = fs.existsSync(versionFile)
133
+ ? fs.readFileSync(versionFile, 'utf-8').trim()
134
+ : '0.0.0';
135
+
136
+ const templateVersion = fs.readFileSync(path.join(TEMPLATES_DIR, '.ocs-version'), 'utf-8').trim();
137
+
138
+ const comparison = compareVersion(templateVersion, currentVersion);
139
+
140
+ if (comparison <= 0) {
141
+ return {
142
+ hasUpdates: false,
143
+ currentVersion,
144
+ templateVersion,
145
+ changes: []
146
+ };
147
+ }
148
+
149
+ const templateFiles = getAllTemplateFiles(TEMPLATES_DIR);
150
+ const changes = [];
151
+
152
+ for (const relativePath of templateFiles) {
153
+ const templatePath = path.join(TEMPLATES_DIR, relativePath);
154
+ const targetPath = path.join(targetDir, relativePath);
155
+
156
+ const templateHash = hashFile(templatePath);
157
+ const targetHash = hashFile(targetPath);
158
+ const category = getCategory(relativePath);
159
+
160
+ let state;
161
+ if (!targetHash) {
162
+ state = FILE_STATE.NEW;
163
+ } else if (templateHash === targetHash) {
164
+ state = FILE_STATE.UNMODIFIED;
165
+ } else {
166
+ state = FILE_STATE.MODIFIED;
167
+ }
168
+
169
+ changes.push({
170
+ relativePath,
171
+ category,
172
+ state,
173
+ templateHash,
174
+ targetHash
175
+ });
176
+ }
177
+
178
+ return {
179
+ hasUpdates: comparison > 0,
180
+ currentVersion,
181
+ templateVersion,
182
+ changes
183
+ };
184
+ }
185
+
186
+ export async function performUpdate(targetDir, options = {}) {
187
+ const { force = false, checkOnly = false } = options;
188
+
189
+ const versionFile = path.join(targetDir, '.ocs-version');
190
+ const currentVersion = fs.existsSync(versionFile)
191
+ ? fs.readFileSync(versionFile, 'utf-8').trim()
192
+ : '0.0.0';
193
+
194
+ const templateVersion = fs.readFileSync(path.join(TEMPLATES_DIR, '.ocs-version'), 'utf-8').trim();
195
+
196
+ if (compareVersion(templateVersion, currentVersion) <= 0 && !force) {
197
+ console.log(` Already up to date (v${currentVersion})\n`);
198
+ return { updated: false, changes: [] };
199
+ }
200
+
201
+ const templateFiles = getAllTemplateFiles(TEMPLATES_DIR);
202
+
203
+ const results = {
204
+ updated: [],
205
+ added: [],
206
+ conflicts: [],
207
+ skipped: [],
208
+ merged: []
209
+ };
210
+
211
+ for (const relativePath of templateFiles) {
212
+ const templatePath = path.join(TEMPLATES_DIR, relativePath);
213
+ const targetPath = path.join(targetDir, relativePath);
214
+ const category = getCategory(relativePath);
215
+
216
+ const isVersionFile = relativePath === '.ocs-version';
217
+ const templateHash = hashFile(templatePath);
218
+ const targetHash = hashFile(targetPath);
219
+
220
+ if (!targetHash) {
221
+ if (!checkOnly) {
222
+ const destDir = path.dirname(targetPath);
223
+ if (!fs.existsSync(destDir)) {
224
+ fs.mkdirSync(destDir, { recursive: true });
225
+ }
226
+ fs.copyFileSync(templatePath, targetPath);
227
+ }
228
+ results.added.push(relativePath);
229
+ continue;
230
+ }
231
+
232
+ if (isVersionFile) {
233
+ if (!checkOnly) {
234
+ fs.copyFileSync(templatePath, targetPath);
235
+ }
236
+ results.updated.push(relativePath);
237
+ continue;
238
+ }
239
+
240
+ if (PROTECTED_CATEGORIES.includes(category)) {
241
+ results.skipped.push({ path: relativePath, reason: 'protected (user data)' });
242
+ continue;
243
+ }
244
+
245
+ if (templateHash === targetHash) {
246
+ if (!checkOnly) {
247
+ fs.copyFileSync(templatePath, targetPath);
248
+ }
249
+ results.updated.push(relativePath);
250
+ continue;
251
+ }
252
+
253
+ if (MERGE_CATEGORIES.includes(category)) {
254
+ const userContent = fs.readFileSync(targetPath, 'utf-8');
255
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
256
+
257
+ const mergeResult = mergeKnowledgeFiles(userContent, templateContent);
258
+
259
+ if (mergeResult.action === 'merged' && !checkOnly) {
260
+ fs.writeFileSync(targetPath, mergeResult.content, 'utf-8');
261
+ }
262
+
263
+ results.merged.push({ path: relativePath, sectionsAdded: mergeResult.action === 'merged' ? 1 : 0 });
264
+ continue;
265
+ }
266
+
267
+ if (checkOnly) {
268
+ results.conflicts.push({ path: relativePath, category });
269
+ continue;
270
+ }
271
+
272
+ results.conflicts.push({ path: relativePath, category, needsPrompt: true });
273
+ }
274
+
275
+ if (!checkOnly) {
276
+ fs.writeFileSync(versionFile, templateVersion, 'utf-8');
277
+ }
278
+
279
+ return {
280
+ updated: true,
281
+ currentVersion,
282
+ templateVersion,
283
+ results
284
+ };
285
+ }
286
+
287
+ export function formatUpdateSummary(updateResult, checkOnly = false) {
288
+ const { results, templateVersion } = updateResult;
289
+
290
+ console.log('');
291
+
292
+ if (checkOnly) {
293
+ console.log(` Template version: v${templateVersion}`);
294
+ console.log(` Status: ${updateResult.hasUpdates ? 'Update available' : 'Up to date'}`);
295
+ } else {
296
+ console.log(` Updated to v${templateVersion}`);
297
+ }
298
+
299
+ if (results.added.length > 0) {
300
+ console.log(`\n Added (${results.added.length}):`);
301
+ for (const file of results.added.slice(0, 5)) {
302
+ console.log(` + ${file}`);
303
+ }
304
+ if (results.added.length > 5) {
305
+ console.log(` ... and ${results.added.length - 5} more`);
306
+ }
307
+ }
308
+
309
+ if (results.updated.length > 0) {
310
+ console.log(`\n Updated (${results.updated.length}):`);
311
+ for (const file of results.updated.slice(0, 5)) {
312
+ console.log(` ~ ${file}`);
313
+ }
314
+ if (results.updated.length > 5) {
315
+ console.log(` ... and ${results.updated.length - 5} more`);
316
+ }
317
+ }
318
+
319
+ if (results.merged.length > 0) {
320
+ console.log(`\n Merged (${results.merged.length}):`);
321
+ for (const file of results.merged.slice(0, 3)) {
322
+ console.log(` ◊ ${file.path}`);
323
+ }
324
+ if (results.merged.length > 3) {
325
+ console.log(` ... and ${results.merged.length - 3} more`);
326
+ }
327
+ }
328
+
329
+ if (results.skipped.length > 0) {
330
+ console.log(`\n Skipped - protected (${results.skipped.length}):`);
331
+ for (const file of results.skipped.slice(0, 3)) {
332
+ console.log(` ⊘ ${file.path} (${file.reason})`);
333
+ }
334
+ if (results.skipped.length > 3) {
335
+ console.log(` ... and ${results.skipped.length - 3} more`);
336
+ }
337
+ }
338
+
339
+ if (results.conflicts.length > 0) {
340
+ console.log(`\n Conflicts (${results.conflicts.length}):`);
341
+ for (const file of results.conflicts.slice(0, 5)) {
342
+ console.log(` ⚠ ${file.path}`);
343
+ }
344
+ if (results.conflicts.length > 5) {
345
+ console.log(` ... and ${results.conflicts.length - 5} more`);
346
+ }
347
+ console.log(`\n Run without --check to resolve conflicts`);
348
+ }
349
+
350
+ console.log('');
351
+ }
@@ -0,0 +1 @@
1
+ 1.1.4
@@ -55,11 +55,11 @@ All mistakes are recorded in:
55
55
  | Level | Title | XP Required | Focus |
56
56
  |-------|-------|-------------|-------|
57
57
  | 1 | Novice | 0 | Basic unit tests |
58
- | 2 | Apprentice | 100 | Integration tests |
59
- | 3 | Practitioner | 300 | E2E tests |
60
- | 4 | Expert | 600 | Test patterns & mocking |
61
- | 5 | Master | 1200 | Full coverage strategies |
62
- | 6 | Grandmaster | 2500 | Testing excellence |
58
+ | 2 | Apprentice | 150 | Integration tests |
59
+ | 3 | Practitioner | 450 | E2E tests |
60
+ | 4 | Expert | 900 | Test patterns & mocking |
61
+ | 5 | Master | 1500 | Full coverage strategies |
62
+ | 6 | Grandmaster | 3000 | Testing excellence |
63
63
 
64
64
  ## Level-Specific Focus
65
65
 
@@ -69,31 +69,31 @@ Focus on:
69
69
  - Simple function testing
70
70
  - Common matchers
71
71
 
72
- ### Level 2 - Apprentice (100 XP)
72
+ ### Level 2 - Apprentice (150 XP)
73
73
  Adds:
74
74
  - Integration tests
75
75
  - API testing with mocked context
76
76
  - Database testing patterns
77
77
 
78
- ### Level 3 - Practitioner (300 XP)
78
+ ### Level 3 - Practitioner (450 XP)
79
79
  Adds:
80
80
  - E2E testing with Playwright
81
81
  - Browser automation
82
82
  - User flow testing
83
83
 
84
- ### Level 4 - Expert (600 XP)
84
+ ### Level 4 - Expert (900 XP)
85
85
  Adds:
86
86
  - Advanced mocking patterns
87
87
  - Test utilities and factories
88
88
  - Test organization
89
89
 
90
- ### Level 5 - Master (1200 XP)
90
+ ### Level 5 - Master (1500 XP)
91
91
  Adds:
92
92
  - Coverage strategies
93
93
  - Flaky test prevention
94
94
  - Performance testing
95
95
 
96
- ### Level 6 - Grandmaster (2500 XP)
96
+ ### Level 6 - Grandmaster (3000 XP)
97
97
  Adds:
98
98
  - Testing architecture
99
99
  - CI/CD integration
@@ -249,6 +249,16 @@ Display XP gain:
249
249
  npx ocs-stats display-xp 80 "Wrote 8 unit tests [testing]"
250
250
  ```
251
251
 
252
+ This will display:
253
+ ```
254
+ ╔══════════════════════════════════════╗
255
+ ║ +80 XP Wrote 8 unit tests ║
256
+ ╠══════════════════════════════════════╣
257
+ ║ Level 1 - Novice ║
258
+ ║ [████████████░░░░░░░] 80/100 ║
259
+ ╚══════════════════════════════════════╝
260
+ ```
261
+
252
262
  ## Mistake Recording
253
263
 
254
264
  If you introduce a flaky test or make a mistake:
@@ -51,10 +51,10 @@
51
51
  },
52
52
  "levelThresholds": [
53
53
  { "level": 1, "title": "Novice", "xpRequired": 0, "focus": "Basic unit tests" },
54
- { "level": 2, "title": "Apprentice", "xpRequired": 100, "focus": "Integration tests" },
55
- { "level": 3, "title": "Practitioner", "xpRequired": 300, "focus": "E2E tests" },
56
- { "level": 4, "title": "Expert", "xpRequired": 600, "focus": "Test patterns & mocking" },
57
- { "level": 5, "title": "Master", "xpRequired": 1200, "focus": "Full coverage strategies" },
58
- { "level": 6, "title": "Grandmaster", "xpRequired": 2500, "focus": "Testing excellence" }
54
+ { "level": 2, "title": "Apprentice", "xpRequired": 150, "focus": "Integration tests" },
55
+ { "level": 3, "title": "Practitioner", "xpRequired": 450, "focus": "E2E tests" },
56
+ { "level": 4, "title": "Expert", "xpRequired": 900, "focus": "Test patterns & mocking" },
57
+ { "level": 5, "title": "Master", "xpRequired": 1500, "focus": "Full coverage strategies" },
58
+ { "level": 6, "title": "Grandmaster", "xpRequired": 3000, "focus": "Testing excellence" }
59
59
  ]
60
60
  }