chekk 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/chekk.js CHANGED
@@ -4,7 +4,7 @@ import { execSync, spawn } from 'child_process';
4
4
  import { Command } from 'commander';
5
5
  import { run } from '../src/index.js';
6
6
 
7
- const LOCAL_VERSION = '0.2.5';
7
+ const LOCAL_VERSION = '0.3.0';
8
8
 
9
9
  // ── Auto-update check ──
10
10
  // If running from a cached npx install, check if there's a newer version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chekk",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "See how you prompt. Chekk analyzes your AI coding workflow and tells you what kind of engineer you are.",
5
5
  "bin": {
6
6
  "chekk": "./bin/chekk.js"
package/src/display.js CHANGED
@@ -6,6 +6,7 @@ const purple = chalk.hex('#A855F7');
6
6
  const blue = chalk.hex('#3B82F6');
7
7
  const green = chalk.hex('#22C55E');
8
8
  const cyan = chalk.hex('#06B6D4');
9
+ const orange = chalk.hex('#F97316');
9
10
  const dim = chalk.dim;
10
11
  const bold = chalk.bold;
11
12
  const white = chalk.white;
@@ -22,7 +23,7 @@ function scoreColor(score) {
22
23
  if (score >= 85) return green;
23
24
  if (score >= 70) return cyan;
24
25
  if (score >= 55) return chalk.hex('#EAB308');
25
- if (score >= 40) return chalk.hex('#F97316');
26
+ if (score >= 40) return orange;
26
27
  return chalk.hex('#EF4444');
27
28
  }
28
29
 
@@ -42,9 +43,28 @@ function pad(str, len) {
42
43
  return str + ' '.repeat(Math.max(0, len - visible.length));
43
44
  }
44
45
 
45
- function snippet(prompt, maxLen = 70) {
46
+ // ── Qualitative tier labels for dimensions ──
47
+ function dimTierLabel(score) {
48
+ if (score >= 80) return 'exceptional';
49
+ if (score >= 65) return 'strong';
50
+ if (score >= 50) return 'solid';
51
+ if (score >= 35) return 'developing';
52
+ return 'needs work';
53
+ }
54
+
55
+ function dimTierColor(score) {
56
+ if (score >= 80) return green;
57
+ if (score >= 65) return cyan;
58
+ if (score >= 50) return chalk.hex('#EAB308');
59
+ if (score >= 35) return orange;
60
+ return chalk.hex('#EF4444');
61
+ }
62
+
63
+ // ── Snippet helpers ──
64
+ // Longer snippets with context labels instead of truncated decorations
65
+
66
+ function cleanPrompt(prompt, maxLen = 120) {
46
67
  if (!prompt) return null;
47
- // Clean up whitespace / newlines
48
68
  let clean = prompt.replace(/\s+/g, ' ').trim();
49
69
  if (clean.length > maxLen) {
50
70
  clean = clean.slice(0, maxLen - 1) + '\u2026';
@@ -52,10 +72,10 @@ function snippet(prompt, maxLen = 70) {
52
72
  return clean;
53
73
  }
54
74
 
55
- function displaySnippet(prompt, maxLen = 70) {
56
- const s = snippet(prompt, maxLen);
75
+ function displayLabeledSnippet(label, prompt, maxLen = 120) {
76
+ const s = cleanPrompt(prompt, maxLen);
57
77
  if (!s) return;
58
- console.log(` ${dim('\u201C')}${dim.italic(s)}${dim('\u201D')}`);
78
+ console.log(` ${dim('\u21B3')} ${dim(label + ':')} ${dim.italic('\u201C' + s + '\u201D')}`);
59
79
  }
60
80
 
61
81
  function pickExample(examples, type) {
@@ -90,7 +110,7 @@ export function displayHeader() {
90
110
  console.log();
91
111
  const lines = [
92
112
  '',
93
- ` ${bold.white('chekk')}${dim(' v0.2.5')}`,
113
+ ` ${bold.white('chekk')}${dim(' v0.3.0')}`,
94
114
  ` ${dim('the engineering capability score')}`,
95
115
  '',
96
116
  ];
@@ -147,8 +167,9 @@ export async function displayProgressBar(durationMs = 2000) {
147
167
  // MAIN SCORE DISPLAY
148
168
  // ══════════════════════════════════════════════
149
169
 
150
- export function displayScore(result, prose) {
170
+ export function displayScore(result, prose, extra = {}) {
151
171
  const { overall, scores, archetype, tier, tierBadge, tierPercentile } = result;
172
+ const { scoreDelta, perToolScores } = extra;
152
173
  const tc = tierColor(tier);
153
174
  const sc = scoreColor(overall);
154
175
 
@@ -157,10 +178,19 @@ export function displayScore(result, prose) {
157
178
  console.log();
158
179
  console.log(dim(' YOUR CHEKK SCORE'));
159
180
  console.log();
181
+
182
+ // Score + delta
183
+ let deltaStr = '';
184
+ if (scoreDelta !== null && scoreDelta !== undefined) {
185
+ if (scoreDelta > 0) deltaStr = ` ${green('\u2191 +' + scoreDelta + ' since last scan')}`;
186
+ else if (scoreDelta < 0) deltaStr = ` ${orange('\u2193 ' + scoreDelta + ' since last scan')}`;
187
+ else deltaStr = ` ${dim('\u2192 same as last scan')}`;
188
+ }
160
189
  console.log(` ${sc.bold(String(overall))}`);
161
190
  const badgeStr = tierBadge ? ` ${tierBadge}` : '';
162
191
  const pctStr = tierPercentile ? ` ${dim(tierPercentile)}` : '';
163
192
  console.log(` ${tc('\u2500\u2500 ' + tier + ' \u2500\u2500')}${badgeStr}${pctStr}`);
193
+ if (deltaStr) console.log(` ${deltaStr}`);
164
194
  console.log();
165
195
  console.log(` ${dim('Archetype:')} ${bold.white(archetype.name)}`);
166
196
 
@@ -168,22 +198,25 @@ export function displayScore(result, prose) {
168
198
  console.log(` ${dim('"' + prose.tagline + '"')}`);
169
199
  }
170
200
  console.log();
201
+
202
+ // ── Score weights (transparency) ──
203
+ console.log(` ${dim('Score = Thinking (25%) + Debugging (25%) + AI Leverage (30%) + Workflow (20%)')}`);
204
+ console.log();
171
205
  console.log(doubleRule());
172
206
  console.log();
173
207
 
174
- // Dimensions box
208
+ // Dimensions box with qualitative labels
175
209
  console.log(dim(' DIMENSIONS\n'));
176
210
 
177
- // Build dimension lines with fixed-width layout
178
- // Format: " Label ██████████████░░░░ XX "
179
- // Visible: 2 + 15 + 18 + 2 + 3 + 1 = ~41 chars
180
211
  function dimLine(label, score) {
181
212
  const labelStr = bold(label);
182
213
  const labelVisible = label.length;
183
214
  const labelPad = ' '.repeat(Math.max(0, 15 - labelVisible));
184
215
  const scoreStr = String(score);
185
216
  const scorePad = score < 10 ? ' ' : score < 100 ? ' ' : '';
186
- return ` ${labelStr}${labelPad}${progressBar(score)} ${scorePad}${bold(scoreStr)} `;
217
+ const tierLabel = dimTierLabel(score);
218
+ const tierStr = dimTierColor(score)(tierLabel);
219
+ return ` ${labelStr}${labelPad}${progressBar(score)} ${scorePad}${bold(scoreStr)} ${dim('\u2500\u2500')} ${tierStr} `;
187
220
  }
188
221
 
189
222
  const dimLines = [
@@ -194,8 +227,36 @@ export function displayScore(result, prose) {
194
227
  dimLine('Workflow', scores.sessionStructure),
195
228
  '',
196
229
  ];
197
- for (const l of box(dimLines, 45)) console.log(l);
230
+ for (const l of box(dimLines, 53)) console.log(l);
198
231
  console.log();
232
+
233
+ // ── Cross-platform breakdown ──
234
+ if (perToolScores && Object.keys(perToolScores).length > 1) {
235
+ console.log(dim(' CROSS-PLATFORM\n'));
236
+ // Sort by score descending, null scores last
237
+ const sorted = Object.entries(perToolScores).sort((a, b) => {
238
+ if (a[1].score === null) return 1;
239
+ if (b[1].score === null) return -1;
240
+ return b[1].score - a[1].score;
241
+ });
242
+ // Find primary (most sessions)
243
+ const maxSessions = Math.max(...sorted.map(([, v]) => v.sessions));
244
+
245
+ for (const [tool, data] of sorted) {
246
+ if (data.score !== null) {
247
+ const isPrimary = data.sessions === maxSessions;
248
+ const label = isPrimary ? 'primary tool' : data.sessions < 5 ? 'limited usage' : (data.label || 'active');
249
+ const barWidth = 18;
250
+ const filled = Math.round((data.score / 100) * barWidth);
251
+ const empty = barWidth - filled;
252
+ const bar = scoreColor(data.score)('\u2588'.repeat(filled)) + dim('\u2591'.repeat(empty));
253
+ console.log(` ${pad(bold(tool), 18)} ${bold(String(data.score))} ${bar} ${dim(label)}`);
254
+ } else {
255
+ console.log(` ${pad(bold(tool), 18)} ${dim('--')} ${dim('\u2591'.repeat(18))} ${dim('limited data')}`);
256
+ }
257
+ }
258
+ console.log();
259
+ }
199
260
  }
200
261
 
201
262
  // ══════════════════════════════════════════════
@@ -203,23 +264,21 @@ export function displayScore(result, prose) {
203
264
  // ══════════════════════════════════════════════
204
265
 
205
266
  export function displayNarratives(metrics, prose) {
206
- // Track shown snippets globally to avoid duplicates across sections
207
267
  const shownSnippets = new Set();
208
- function showUniqueSnippet(prompt) {
268
+ function showLabeledSnippet(label, prompt) {
209
269
  if (!prompt) return;
210
- const s = snippet(prompt, 70);
270
+ const s = cleanPrompt(prompt, 120);
211
271
  if (shownSnippets.has(s)) return;
212
272
  shownSnippets.add(s);
213
- displaySnippet(prompt);
273
+ displayLabeledSnippet(label, prompt);
214
274
  }
215
275
 
216
276
  if (prose && prose.sections) {
217
- // Use DeepSeek-generated prose with inline prompt snippets per section
218
277
  const sectionSnippetMap = {
219
- 'thinking': pickExample(metrics.decomposition.examples, 'decomposition'),
220
- 'debugging': pickExample(metrics.debugCycles.examples, 'specific_report') || pickExample(metrics.debugCycles.examples, 'quick_fix'),
221
- 'ai leverage': pickExample(metrics.aiLeverage.examples, 'architectural') || pickExample(metrics.aiLeverage.examples, 'planning'),
222
- 'workflow': pickExample(metrics.sessionStructure.examples, 'context_setting') || pickExample(metrics.sessionStructure.examples, 'refinement'),
278
+ 'thinking': { label: 'Best decomposition prompt', prompt: pickExample(metrics.decomposition.examples, 'decomposition') },
279
+ 'debugging': { label: 'Best debug prompt', prompt: pickExample(metrics.debugCycles.examples, 'specific_report') || pickExample(metrics.debugCycles.examples, 'quick_fix') },
280
+ 'ai leverage': { label: 'Best leverage prompt', prompt: pickExample(metrics.aiLeverage.examples, 'architectural') || pickExample(metrics.aiLeverage.examples, 'planning') },
281
+ 'workflow': { label: 'Best workflow prompt', prompt: pickExample(metrics.sessionStructure.examples, 'context_setting') || pickExample(metrics.sessionStructure.examples, 'refinement') },
223
282
  };
224
283
 
225
284
  for (const section of prose.sections) {
@@ -228,14 +287,12 @@ export function displayNarratives(metrics, prose) {
228
287
  for (const line of lines) {
229
288
  console.log(` ${dim(line.trim())}`);
230
289
  }
231
- // Show relevant prompt snippet under this section
232
290
  const titleLower = section.title.toLowerCase();
233
- const matchedSnippet = sectionSnippetMap[titleLower];
234
- if (matchedSnippet) showUniqueSnippet(matchedSnippet);
291
+ const matched = sectionSnippetMap[titleLower];
292
+ if (matched && matched.prompt) showLabeledSnippet(matched.label, matched.prompt);
235
293
  console.log();
236
294
  }
237
295
  } else {
238
- // Fallback: data-driven bullet points with inline snippets
239
296
  displayDataNarrativesWithTracker(metrics, shownSnippets);
240
297
  }
241
298
  }
@@ -250,12 +307,12 @@ function displayDataNarrativesWithTracker(metrics, shownSnippets) {
250
307
  const aiEx = metrics.aiLeverage.examples || [];
251
308
  const ssEx = metrics.sessionStructure.examples || [];
252
309
 
253
- function showUniqueSnippet(prompt) {
310
+ function showLabeledSnippet(label, prompt) {
254
311
  if (!prompt) return;
255
- const s = snippet(prompt, 70);
312
+ const s = cleanPrompt(prompt, 120);
256
313
  if (shownSnippets.has(s)) return;
257
314
  shownSnippets.add(s);
258
- displaySnippet(prompt);
315
+ displayLabeledSnippet(label, prompt);
259
316
  }
260
317
 
261
318
  // Thinking
@@ -264,16 +321,16 @@ function displayDataNarrativesWithTracker(metrics, shownSnippets) {
264
321
  console.log(` ${dim(exchPerSession > 20 ? `${exchPerSession} avg exchanges/session \u2014 marathon builder` : exchPerSession > 8 ? `${exchPerSession} avg exchanges/session \u2014 iterative` : `${exchPerSession} avg exchanges/session \u2014 concise`)}`);
265
322
  console.log(` ${dim(d.avgPromptLength > 500 ? `${numberFormat(d.avgPromptLength)} char avg prompt \u2014 thinks out loud` : `${d.avgPromptLength} char avg prompt \u2014 concise communicator`)}`);
266
323
  console.log(` ${dim(d.multiStepSessions > d.singleShotSessions * 2 ? 'Multi-step decomposition over single-shot' : 'Mix of multi-step and single-shot sessions')}`);
267
- showUniqueSnippet(pickExample(dEx, 'decomposition'));
324
+ showLabeledSnippet('Best decomposition prompt', pickExample(dEx, 'decomposition'));
268
325
  console.log();
269
326
 
270
327
  // Debugging
271
328
  console.log(` ${bold('\u26A1 DEBUGGING')}`);
272
329
  const turns = db.avgTurnsToResolve;
273
330
  console.log(` ${dim(turns <= 2 ? `${turns} turns to resolve \u2014 surgical` : turns <= 4 ? `${turns} turns to resolve \u2014 efficient` : `${turns} turns to resolve \u2014 iterative`)}`);
274
- console.log(` ${dim(`${db.specificReportRatio}% specific error reports`)}`);
275
- console.log(` ${dim(db.longLoops === 0 ? 'Zero extended debug loops detected' : `${db.longLoops} extended debug loops`)}`);
276
- showUniqueSnippet(pickExample(dbEx, 'specific_report') || pickExample(dbEx, 'quick_fix'));
331
+ console.log(` ${dim(`${db.specificReportRatio}% specific error reports \u2014 ${db.specificReportRatio >= 80 ? 'precise' : db.specificReportRatio >= 50 ? 'decent' : 'vague'}`)}`);
332
+ console.log(` ${dim(db.longLoops === 0 ? 'Zero long loops \u2014 efficient debugger' : `${db.longLoops} extended debug loops`)}`);
333
+ showLabeledSnippet('Best debug prompt', pickExample(dbEx, 'specific_report') || pickExample(dbEx, 'quick_fix'));
277
334
  console.log();
278
335
 
279
336
  // AI Leverage
@@ -282,7 +339,7 @@ function displayDataNarrativesWithTracker(metrics, shownSnippets) {
282
339
  const codingRatio = ai.toolDiversity.coding > ai.toolDiversity.research ? 'Coding-heavy' : 'Research-heavy';
283
340
  console.log(` ${dim(`${codingRatio} over ${ai.toolDiversity.coding > ai.toolDiversity.research ? 'research' : 'coding'}-heavy`)}`);
284
341
  console.log(` ${dim(`${ai.architecturalPrompts} architectural, ${ai.planningPrompts} planning, ${ai.exploratoryPrompts} exploratory`)}`);
285
- showUniqueSnippet(pickExample(aiEx, 'architectural') || pickExample(aiEx, 'planning'));
342
+ showLabeledSnippet('Best leverage prompt', pickExample(aiEx, 'architectural') || pickExample(aiEx, 'planning'));
286
343
  console.log();
287
344
 
288
345
  // Workflow
@@ -290,7 +347,43 @@ function displayDataNarrativesWithTracker(metrics, shownSnippets) {
290
347
  console.log(` ${dim(`${ss.contextSetRatio}% context-setting rate \u2014 ${ss.contextSetRatio > 50 ? 'deliberate' : ss.contextSetRatio > 25 ? 'moderate' : 'low'}`)}`);
291
348
  console.log(` ${dim(`${ss.reviewEndRatio}% sessions end with review`)}`);
292
349
  console.log(` ${dim(`${ss.refinementRatio}% refinement rate \u2014 ${ss.refinementRatio > 20 ? 'critical eye' : 'accepts readily'}`)}`);
293
- showUniqueSnippet(pickExample(ssEx, 'context_setting') || pickExample(ssEx, 'refinement'));
350
+ showLabeledSnippet('Best workflow prompt', pickExample(ssEx, 'context_setting') || pickExample(ssEx, 'refinement'));
351
+ console.log();
352
+ }
353
+
354
+
355
+ // ══════════════════════════════════════════════
356
+ // COACHING RECOMMENDATION
357
+ // ══════════════════════════════════════════════
358
+
359
+ function displayCoaching(result, metrics) {
360
+ const { scores } = result;
361
+ // Find the weakest dimension
362
+ const dims = [
363
+ { key: 'decomposition', label: 'Thinking', score: scores.decomposition },
364
+ { key: 'debugCycles', label: 'Debugging', score: scores.debugCycles },
365
+ { key: 'aiLeverage', label: 'AI Leverage', score: scores.aiLeverage },
366
+ { key: 'sessionStructure', label: 'Workflow', score: scores.sessionStructure },
367
+ ];
368
+ dims.sort((a, b) => a.score - b.score);
369
+ const weakest = dims[0];
370
+
371
+ // Only show coaching if the weakest is actually dragging things down
372
+ if (weakest.score >= 70) return;
373
+
374
+ const tips = {
375
+ decomposition: 'Break complex tasks into subtasks before prompting.\nEngineers who decompose problems score 15+ points higher.',
376
+ debugCycles: 'Include specific error messages and stack traces in debug prompts.\nPrecise reports resolve 2x faster than "it\'s broken, fix it."',
377
+ aiLeverage: 'Use AI for architecture and planning, not just code generation.\nAsk "what\'s the best approach" before "write the code."',
378
+ sessionStructure: 'Set context at the start of each session and review at the end.\nEngineers who do this score 12 points higher on average.',
379
+ };
380
+
381
+ console.log(` ${bold('\uD83D\uDCA1 TO LEVEL UP')}`);
382
+ console.log(` ${dim(`Your ${weakest.label} score (${weakest.score}) is your biggest opportunity.`)}`);
383
+ const tipLines = tips[weakest.key].split('\n');
384
+ for (const line of tipLines) {
385
+ console.log(` ${dim(line)}`);
386
+ }
294
387
  console.log();
295
388
  }
296
389
 
@@ -335,8 +428,15 @@ export function displayEnding(result) {
335
428
 
336
429
  console.log(doubleRule());
337
430
  console.log();
338
- console.log(` ${dim('\u2192 Claim your profile:')} ${cyan.underline('chekk.dev/claim')}`);
339
- console.log(` ${dim('\u2192 Leaderboard:')} ${cyan.underline('chekk.dev/leaderboard')}`);
431
+
432
+ // Value-driven claim CTA
433
+ console.log(` ${dim('\u2192 Claim your profile to:')}`);
434
+ console.log(` ${dim(' \u00B7 See how you compare across 10K+ engineers')}`);
435
+ console.log(` ${dim(' \u00B7 Get matched with roles at top startups')}`);
436
+ console.log(` ${dim(' \u00B7 Track your score over time')}`);
437
+ console.log(` ${cyan.underline(' chekk.dev/claim')}`);
438
+ console.log();
439
+ console.log(` ${dim('\u2192 Leaderboard:')} ${cyan.underline('chekk.dev/leaderboard')}`);
340
440
  console.log();
341
441
 
342
442
  // Copy-paste share line
@@ -351,7 +451,7 @@ export function displayEnding(result) {
351
451
  }
352
452
 
353
453
  // ══════════════════════════════════════════════
354
- // VERBOSE: DETAILED BREAKDOWN
454
+ // VERBOSE: DETAILED BREAKDOWN (with interpretation)
355
455
  // ══════════════════════════════════════════════
356
456
 
357
457
  export function displayVerbose(metrics, sessions) {
@@ -375,40 +475,57 @@ export function displayVerbose(metrics, sessions) {
375
475
  }
376
476
  console.log();
377
477
 
378
- // Full metric details
478
+ // Decomposition details with interpretation
479
+ const d = metrics.decomposition.details;
379
480
  console.log(bold(' DECOMPOSITION DETAILS'));
380
- for (const [k, v] of Object.entries(metrics.decomposition.details)) {
381
- console.log(` ${dim(pad(k, 30))} ${dim(String(v) + (typeof v === 'number' && k.includes('Ratio') ? '%' : ''))}`);
382
- }
481
+ console.log(` ${dim(pad('totalSessions', 30))} ${dim(String(d.totalSessions))}`);
482
+ console.log(` ${dim(pad('multiStepSessions', 30))} ${dim(String(d.multiStepSessions))} ${dim(d.multiStepSessions > d.totalSessions * 0.5 ? '\u2014 strong iterative work' : '\u2014 room for more iteration')}`);
483
+ console.log(` ${dim(pad('singleShotSessions', 30))} ${dim(String(d.singleShotSessions))} ${dim(d.singleShotSessions < d.totalSessions * 0.3 ? '\u2014 good, not over-relying on one-shots' : '\u2014 consider breaking tasks down more')}`);
484
+ console.log(` ${dim(pad('avgExchangesPerSession', 30))} ${dim(String(d.avgExchangesPerSession))} ${dim(d.avgExchangesPerSession > 12 ? '\u2014 deep sessions' : d.avgExchangesPerSession > 5 ? '\u2014 moderate depth' : '\u2014 shallow sessions')}`);
485
+ console.log(` ${dim(pad('avgPromptLength', 30))} ${dim(String(d.avgPromptLength) + ' chars')} ${dim(d.avgPromptLength > 300 ? '\u2014 detailed communicator' : d.avgPromptLength > 100 ? '\u2014 moderate detail' : '\u2014 terse')}`);
486
+ console.log(` ${dim(pad('longPromptRatio', 30))} ${dim(d.longPromptRatio + '%')}`);
487
+ console.log(` ${dim(pad('contextualFollowupRatio', 30))} ${dim(d.contextualFollowupRatio + '%')} ${dim(d.contextualFollowupRatio > 20 ? '\u2014 builds on previous context well' : '\u2014 could reference prior context more')}`);
383
488
  console.log();
384
489
 
490
+ // Debug cycle details
491
+ const db = metrics.debugCycles.details;
385
492
  console.log(bold(' DEBUG CYCLE DETAILS'));
386
- for (const [k, v] of Object.entries(metrics.debugCycles.details)) {
387
- console.log(` ${dim(pad(k, 30))} ${dim(String(v))}`);
388
- }
493
+ console.log(` ${dim(pad('totalDebugSequences', 30))} ${dim(String(db.totalDebugSequences))}`);
494
+ console.log(` ${dim(pad('avgTurnsToResolve', 30))} ${dim(String(db.avgTurnsToResolve))} ${dim(db.avgTurnsToResolve <= 2 ? '\u2014 surgical precision' : db.avgTurnsToResolve <= 4 ? '\u2014 efficient' : '\u2014 could be tighter')}`);
495
+ console.log(` ${dim(pad('quickFixes', 30))} ${dim(String(db.quickFixes))} ${dim(db.quickFixes > db.totalDebugSequences * 0.5 ? '\u2014 most issues resolved fast' : '')}`);
496
+ console.log(` ${dim(pad('longLoops', 30))} ${dim(String(db.longLoops))} ${dim(db.longLoops === 0 ? '\u2014 zero spirals' : '\u2014 some extended debugging')}`);
497
+ console.log(` ${dim(pad('specificReportRatio', 30))} ${dim(db.specificReportRatio + '%')} ${dim(db.specificReportRatio >= 80 ? '\u2014 precise error reporting' : '\u2014 include more stack traces/line numbers')}`);
498
+ console.log(` ${dim(pad('vagueReports', 30))} ${dim(String(db.vagueReports))} ${dim(db.vagueReports === 0 ? '\u2014 never vague' : `\u2014 ${db.vagueReports} vague reports`)}`);
389
499
  console.log();
390
500
 
501
+ // AI Leverage details
502
+ const ai = metrics.aiLeverage.details;
391
503
  console.log(bold(' AI LEVERAGE DETAILS'));
392
- for (const [k, v] of Object.entries(metrics.aiLeverage.details)) {
393
- if (typeof v === 'object') {
394
- for (const [k2, v2] of Object.entries(v)) {
395
- console.log(` ${dim(pad(` ${k}.${k2}`, 30))} ${dim(String(v2))}`);
396
- }
397
- } else {
398
- console.log(` ${dim(pad(k, 30))} ${dim(String(v))}`);
399
- }
400
- }
504
+ console.log(` ${dim(pad('totalPrompts', 30))} ${dim(numberFormat(ai.totalPrompts))}`);
505
+ console.log(` ${dim(pad('architecturalPrompts', 30))} ${dim(String(ai.architecturalPrompts))} ${dim(`(${Math.round(ai.architecturalPrompts / ai.totalPrompts * 100)}%)`)} ${dim(ai.architecturalPrompts > ai.totalPrompts * 0.1 ? '\u2014 strategic usage' : '\u2014 could use AI for more design work')}`);
506
+ console.log(` ${dim(pad('planningPrompts', 30))} ${dim(String(ai.planningPrompts))} ${dim(`(${Math.round(ai.planningPrompts / ai.totalPrompts * 100)}%)`)}`);
507
+ console.log(` ${dim(pad('exploratoryPrompts', 30))} ${dim(String(ai.exploratoryPrompts))} ${dim(`(${Math.round(ai.exploratoryPrompts / ai.totalPrompts * 100)}%)`)}`);
508
+ console.log(` ${dim(pad('boilerplatePrompts', 30))} ${dim(String(ai.boilerplatePrompts))} ${dim(`(${Math.round(ai.boilerplatePrompts / ai.totalPrompts * 100)}%)`)} ${dim(ai.boilerplatePrompts < ai.totalPrompts * 0.05 ? '\u2014 very low, minimal repetitive work' : '\u2014 some boilerplate delegation')}`);
509
+ console.log(` ${dim(pad('testingPrompts', 30))} ${dim(String(ai.testingPrompts))} ${dim(`(${Math.round(ai.testingPrompts / ai.totalPrompts * 100)}%)`)}`);
510
+ console.log(` ${dim(pad('highLevelRatio', 30))} ${dim(ai.highLevelRatio + '%')} ${dim(ai.highLevelRatio > 30 ? '\u2014 strong strategic AI usage' : '\u2014 room to leverage AI for higher-level work')}`);
511
+ console.log(` ${dim(pad('toolDiversity.research', 30))} ${dim(String(ai.toolDiversity.research))}`);
512
+ console.log(` ${dim(pad('toolDiversity.coding', 30))} ${dim(String(ai.toolDiversity.coding))}`);
401
513
  console.log();
402
514
 
515
+ // Session structure details
516
+ const ss = metrics.sessionStructure.details;
403
517
  console.log(bold(' SESSION STRUCTURE DETAILS'));
404
- for (const [k, v] of Object.entries(metrics.sessionStructure.details)) {
405
- if (typeof v === 'object') {
406
- for (const [k2, v2] of Object.entries(v)) {
407
- console.log(` ${dim(pad(` ${k}.${k2}`, 30))} ${dim(String(v2))}`);
408
- }
409
- } else {
410
- console.log(` ${dim(pad(k, 30))} ${dim(String(v) + (typeof v === 'number' && k.includes('Ratio') ? '%' : ''))}`);
411
- }
518
+ console.log(` ${dim(pad('contextSetRatio', 30))} ${dim(ss.contextSetRatio + '%')} ${dim(ss.contextSetRatio > 50 ? '\u2014 deliberate context-setting' : '\u2014 try explaining goals upfront')}`);
519
+ console.log(` ${dim(pad('planBeforeCodeRatio', 30))} ${dim(ss.planBeforeCodeRatio + '%')} ${dim(ss.planBeforeCodeRatio > 20 ? '\u2014 plans before coding' : '\u2014 consider planning before diving in')}`);
520
+ console.log(` ${dim(pad('reviewEndRatio', 30))} ${dim(ss.reviewEndRatio + '%')} ${dim(ss.reviewEndRatio > 30 ? '\u2014 reviews work before shipping' : '\u2014 add review steps before finishing')}`);
521
+ console.log(` ${dim(pad('refinementRatio', 30))} ${dim(ss.refinementRatio + '%')} ${dim(ss.refinementRatio > 20 ? '\u2014 critically evaluates output' : '\u2014 push back on AI output more')}`);
522
+ console.log(` ${dim(pad('avgFirstPromptLength', 30))} ${dim(ss.avgFirstPromptLength + ' chars')} ${dim(ss.avgFirstPromptLength > 200 ? '\u2014 thorough session openers' : '\u2014 try longer context-setting openers')}`);
523
+ if (ss.durationDistribution) {
524
+ const dur = ss.durationDistribution;
525
+ console.log(` ${dim(pad('duration.focused (10-45m)', 30))} ${dim(String(dur.focused))} ${dim('\u2014 sweet spot sessions')}`);
526
+ console.log(` ${dim(pad('duration.short (<5m)', 30))} ${dim(String(dur.short))}`);
527
+ console.log(` ${dim(pad('duration.medium (5-60m)', 30))} ${dim(String(dur.medium))}`);
528
+ console.log(` ${dim(pad('duration.long (>60m)', 30))} ${dim(String(dur.long))}`);
412
529
  }
413
530
  console.log();
414
531
  }
@@ -417,9 +534,10 @@ export function displayVerbose(metrics, sessions) {
417
534
  // OFFLINE DISPLAY (no API)
418
535
  // ══════════════════════════════════════════════
419
536
 
420
- export function displayOffline(result, metrics) {
421
- displayScore(result, null);
537
+ export function displayOffline(result, metrics, extra = {}) {
538
+ displayScore(result, null, extra);
422
539
  displayDataNarrativesWithTracker(metrics, new Set());
540
+ displayCoaching(result, metrics);
423
541
  displayEasterEggs(result, metrics);
424
542
  console.log(dim(' Run without --offline for personalized AI-generated insights\n'));
425
543
  displayEnding(result);
@@ -429,9 +547,10 @@ export function displayOffline(result, metrics) {
429
547
  // FULL ONLINE DISPLAY
430
548
  // ══════════════════════════════════════════════
431
549
 
432
- export function displayFull(result, metrics, prose) {
433
- displayScore(result, prose);
550
+ export function displayFull(result, metrics, prose, extra = {}) {
551
+ displayScore(result, prose, extra);
434
552
  displayNarratives(metrics, prose);
553
+ displayCoaching(result, metrics);
435
554
  displayEasterEggs(result, metrics);
436
555
  displayEnding(result);
437
556
  }
package/src/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import chalk from 'chalk';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
2
5
  import { detectTools } from './detect.js';
3
6
  import { parseAllProjects } from './parsers/claude-code.js';
4
7
  import { parseAllWorkspaces } from './parsers/cursor.js';
@@ -19,6 +22,52 @@ import {
19
22
  } from './display.js';
20
23
  import { generateProse, askVerbose, askClaim, uploadAndClaim } from './upload.js';
21
24
 
25
+ // ── Score history ──
26
+ const HISTORY_DIR = join(homedir(), '.chekk');
27
+ const HISTORY_FILE = join(HISTORY_DIR, 'history.json');
28
+
29
+ function loadHistory() {
30
+ try {
31
+ if (existsSync(HISTORY_FILE)) {
32
+ return JSON.parse(readFileSync(HISTORY_FILE, 'utf-8'));
33
+ }
34
+ } catch {}
35
+ return { scans: [] };
36
+ }
37
+
38
+ function saveHistory(history) {
39
+ try {
40
+ if (!existsSync(HISTORY_DIR)) mkdirSync(HISTORY_DIR, { recursive: true });
41
+ writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2));
42
+ } catch {}
43
+ }
44
+
45
+ function computePerToolScores(allSessions) {
46
+ const toolSessions = {};
47
+ for (const s of allSessions) {
48
+ const t = s.sourceTool || 'Unknown';
49
+ if (!toolSessions[t]) toolSessions[t] = [];
50
+ toolSessions[t].push(s);
51
+ }
52
+
53
+ const perTool = {};
54
+ for (const [tool, sessions] of Object.entries(toolSessions)) {
55
+ if (sessions.length < 2) {
56
+ perTool[tool] = { sessions: sessions.length, score: null, label: 'limited data' };
57
+ continue;
58
+ }
59
+ const m = {
60
+ decomposition: computeDecomposition(sessions),
61
+ debugCycles: computeDebugCycles(sessions),
62
+ aiLeverage: computeAILeverage(sessions),
63
+ sessionStructure: computeSessionStructure(sessions),
64
+ };
65
+ const r = computeOverallScore(m);
66
+ perTool[tool] = { sessions: sessions.length, score: r.overall, label: null };
67
+ }
68
+ return perTool;
69
+ }
70
+
22
71
  export async function run(options = {}) {
23
72
  // ── Header ──
24
73
  displayHeader();
@@ -34,16 +83,21 @@ export async function run(options = {}) {
34
83
 
35
84
  displayScan(tools);
36
85
 
37
- // ── Step 2: Parse sessions ──
86
+ // ── Step 2: Parse sessions (tag each with sourceTool) ──
38
87
  let allSessions = [];
39
88
  for (const tool of tools) {
89
+ let parsed = [];
40
90
  if (tool.tool === 'Claude Code') {
41
- allSessions.push(...parseAllProjects(tool.basePath));
91
+ parsed = parseAllProjects(tool.basePath);
42
92
  } else if (tool.tool === 'Cursor') {
43
- allSessions.push(...parseAllWorkspaces(tool.basePath));
93
+ parsed = parseAllWorkspaces(tool.basePath);
44
94
  } else if (tool.tool === 'Codex') {
45
- allSessions.push(...parseCodexSessions(tool.basePath));
95
+ parsed = parseCodexSessions(tool.basePath);
96
+ }
97
+ for (const s of parsed) {
98
+ s.sourceTool = tool.tool;
46
99
  }
100
+ allSessions.push(...parsed);
47
101
  }
48
102
 
49
103
  if (allSessions.length === 0) {
@@ -80,6 +134,25 @@ export async function run(options = {}) {
80
134
 
81
135
  const result = computeOverallScore(metrics);
82
136
 
137
+ // ── Cross-platform scores ──
138
+ const perToolScores = tools.length > 1 ? computePerToolScores(allSessions) : null;
139
+
140
+ // ── Score delta (compare to last scan) ──
141
+ const history = loadHistory();
142
+ const lastScan = history.scans.length > 0 ? history.scans[history.scans.length - 1] : null;
143
+ const scoreDelta = lastScan ? result.overall - lastScan.overall : null;
144
+
145
+ // Save current scan
146
+ history.scans.push({
147
+ overall: result.overall,
148
+ tier: result.tier,
149
+ archetype: result.archetype.name,
150
+ date: new Date().toISOString(),
151
+ });
152
+ // Keep last 20 scans
153
+ if (history.scans.length > 20) history.scans = history.scans.slice(-20);
154
+ saveHistory(history);
155
+
83
156
  const sessionStats = {
84
157
  totalSessions: allSessions.length,
85
158
  totalExchanges,
@@ -90,13 +163,11 @@ export async function run(options = {}) {
90
163
 
91
164
  // ── JSON output ──
92
165
  if (options.json) {
93
- console.log(JSON.stringify({ metrics, result, sessionStats }, null, 2));
166
+ console.log(JSON.stringify({ metrics, result, sessionStats, perToolScores, scoreDelta }, null, 2));
94
167
  return;
95
168
  }
96
169
 
97
170
  // ── Step 4: Progress bar + API call in parallel ──
98
- // Run prose generation alongside the progress bar so score appears
99
- // immediately when the bar hits 100% — no lag.
100
171
  let prose = null;
101
172
  if (!options.offline) {
102
173
  const [, proseResult] = await Promise.all([
@@ -110,13 +181,12 @@ export async function run(options = {}) {
110
181
 
111
182
  // ── Step 5: Display results ──
112
183
  if (options.offline) {
113
- displayOffline(result, metrics);
184
+ displayOffline(result, metrics, { scoreDelta, perToolScores });
114
185
  } else {
115
- displayFull(result, metrics, prose);
186
+ displayFull(result, metrics, prose, { scoreDelta, perToolScores });
116
187
  }
117
188
 
118
189
  // ── Step 6: Verbose prompt (interactive) ──
119
- // If --verbose flag was passed, show immediately. Otherwise prompt.
120
190
  if (options.verbose) {
121
191
  displayVerbose(metrics, allSessions);
122
192
  } else {