claude-roi 0.8.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-roi",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Correlate Claude Code token usage with git output to measure AI coding agent ROI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -297,7 +297,7 @@
297
297
  /* Hero Stats — 3+1 layout */
298
298
  .hero-stats {
299
299
  display: grid;
300
- grid-template-columns: 1fr 220px;
300
+ grid-template-columns: 1fr 240px;
301
301
  gap: 20px;
302
302
  align-items: stretch;
303
303
  }
@@ -394,24 +394,47 @@
394
394
  align-items: center;
395
395
  justify-content: center;
396
396
  text-align: center;
397
- padding: 24px;
397
+ padding: 16px 16px 14px;
398
+ gap: 4px;
398
399
  }
399
400
  .stat-card.grade .value {
400
401
  font-family: var(--font-display);
401
- font-size: 3.5rem;
402
+ font-size: 2rem;
402
403
  line-height: 1;
403
404
  font-weight: 800;
404
405
  }
406
+ .stat-card.grade .label {
407
+ margin-bottom: 2px;
408
+ }
409
+ .stat-card.grade .sub {
410
+ margin: 0;
411
+ }
405
412
  .grade-circle {
406
- width: 120px;
407
- height: 120px;
413
+ width: 80px;
414
+ height: 80px;
408
415
  border-radius: 50%;
409
416
  display: flex;
410
417
  align-items: center;
411
418
  justify-content: center;
412
- margin-bottom: 12px;
419
+ margin-bottom: 2px;
413
420
  position: relative;
414
421
  }
422
+ .score-tier {
423
+ font-size: 0.75rem;
424
+ text-transform: uppercase;
425
+ letter-spacing: 0.1em;
426
+ font-weight: 700;
427
+ }
428
+ .score-context {
429
+ font-size: 0.68rem;
430
+ color: var(--text-secondary);
431
+ }
432
+ .score-tip {
433
+ font-size: 0.64rem;
434
+ color: var(--text-muted);
435
+ font-style: italic;
436
+ line-height: 1.3;
437
+ }
415
438
  .grade-circle::before {
416
439
  content: '';
417
440
  position: absolute;
@@ -1874,6 +1897,13 @@
1874
1897
  <script>
1875
1898
  const GRADE_VAR = { A: 'var(--grade-a)', B: 'var(--grade-b)', C: 'var(--grade-c)', D: 'var(--grade-d)', F: 'var(--grade-f)' };
1876
1899
  const GRADE_BG_VAR = { A: 'var(--grade-a-bg)', B: 'var(--grade-b-bg)', C: 'var(--grade-c-bg)', D: 'var(--grade-d-bg)', F: 'var(--grade-f-bg)' };
1900
+ function scoreColor(score) {
1901
+ if (score >= 80) return 'var(--grade-a)';
1902
+ if (score >= 60) return 'var(--grade-b)';
1903
+ if (score >= 40) return 'var(--grade-c)';
1904
+ if (score >= 20) return 'var(--grade-d)';
1905
+ return 'var(--text-muted)';
1906
+ }
1877
1907
  const INSIGHT_ICONS = { warning: '!', success: '+', info: 'i', tip: '*' };
1878
1908
  const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1879
1909
  let DATA = null;
@@ -2081,11 +2111,11 @@ function render() {
2081
2111
  }
2082
2112
 
2083
2113
  function renderHeroStats(s) {
2084
- const gradeColor = GRADE_VAR[s.overallGrade] || GRADE_VAR.F;
2085
- const GRADE_DEG = { A: 324, B: 270, C: 216, D: 144, F: 72 };
2086
- const gradeDeg = GRADE_DEG[s.overallGrade] || 72;
2114
+ const es = s.efficiencyScore || { score: 0, tier: 'Getting Started', letter: 'F', explanation: '', tip: '' };
2115
+ const sColor = scoreColor(es.score);
2116
+ const scoreDeg = Math.round(es.score * 3.6);
2087
2117
  return `<div class="stats-section">
2088
- <h2>Performance Overview <i class="info-tip" data-tip="High-level metrics for your AI coding sessions — cost, output, efficiency, and overall grade.">i</i></h2>
2118
+ <h2>Performance Overview <i class="info-tip" data-tip="High-level metrics for your AI coding sessions — cost, output, efficiency, and overall score.">i</i></h2>
2089
2119
  <div class="hero-stats">
2090
2120
  <div class="hero-stats-left">
2091
2121
  <div class="stat-card glow cost-card">
@@ -2119,11 +2149,14 @@ function renderHeroStats(s) {
2119
2149
  </div>
2120
2150
  </div>
2121
2151
  </div>
2122
- <div class="stat-card grade glow grade-card" style="--grade-color: ${gradeColor}; --grade-deg: ${gradeDeg}deg;">
2123
- <div class="label">ROI Grade <i class="info-tip" data-tip="Overall efficiency grade from A (great) to F (poor). Based on cost-per-commit and code survival.">i</i></div>
2152
+ <div class="stat-card grade glow grade-card" style="--grade-color: ${sColor}; --grade-deg: ${scoreDeg}deg;">
2153
+ <div class="label">Efficiency Score <i class="info-tip" data-tip="Score from 0–100 based on cost-per-commit and code survival rate. Higher is better.">i</i></div>
2124
2154
  <div class="grade-circle">
2125
- <div class="value" style="color: ${gradeColor};">${s.overallGrade}</div>
2155
+ <div class="value" style="color: ${sColor};">${es.score}</div>
2126
2156
  </div>
2157
+ <div class="score-tier" style="color: ${sColor};">${es.tier}</div>
2158
+ <div class="score-context">${es.explanation}</div>
2159
+ <div class="score-tip">${es.tip}</div>
2127
2160
  <div class="sub">${s.orphanedSessionRate}% sessions orphaned</div>
2128
2161
  </div>
2129
2162
  </div>
@@ -2131,7 +2164,7 @@ function renderHeroStats(s) {
2131
2164
  <span><span class="dot" style="background: var(--accent-orange);"></span>Cost</span>
2132
2165
  <span><span class="dot" style="background: var(--accent-green);"></span>Output</span>
2133
2166
  <span><span class="dot" style="background: var(--accent-blue);"></span>Efficiency</span>
2134
- <span><span class="dot" style="background: ${gradeColor};"></span>Grade: ${s.overallGrade}</span>
2167
+ <span><span class="dot" style="background: ${sColor};"></span>Score: ${es.score} · ${es.tier}</span>
2135
2168
  </div>
2136
2169
  </div>`;
2137
2170
  }
package/src/metrics.js CHANGED
@@ -262,6 +262,45 @@ function computeEfficiencyGrade(costPerCommit, survivalRate) {
262
262
  return 'F';
263
263
  }
264
264
 
265
+ function computeEfficiencyScore(costPerCommit, survivalRate, orphanedRate, totalCommits) {
266
+ if (totalCommits === 0) {
267
+ return {
268
+ score: 0, tier: 'Getting Started', letter: 'F',
269
+ explanation: 'No commits matched to sessions yet — this is normal for exploratory work.',
270
+ tip: 'Commits are matched by file overlap with Claude-edited files.',
271
+ };
272
+ }
273
+
274
+ // Score: 50 pts from cost efficiency (log scale) + 50 pts from survival rate
275
+ let costScore;
276
+ if (costPerCommit <= 2) costScore = 50;
277
+ else if (costPerCommit >= 50) costScore = 0;
278
+ else costScore = Math.max(0, 50 * (1 - Math.log(costPerCommit / 2) / Math.log(25)));
279
+ const survivalScore = Math.min(survivalRate, 100) / 100 * 50;
280
+ const score = Math.round(costScore + survivalScore);
281
+
282
+ const tier = score >= 80 ? 'Excellent' : score >= 60 ? 'Solid' : score >= 40 ? 'Developing' : score >= 20 ? 'Early' : 'Getting Started';
283
+ const letter = score >= 80 ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : score >= 20 ? 'D' : 'F';
284
+
285
+ // Build explanation from actual metrics
286
+ const costAdj = costPerCommit <= 2 ? 'excellent' : costPerCommit <= 5 ? 'good' : costPerCommit <= 15 ? 'moderate' : 'high';
287
+ const explanation = `$${costPerCommit.toFixed(2)}/commit (${costAdj}) · ${Math.round(survivalRate)}% code survival`;
288
+
289
+ // Actionable tip based on weakest metric
290
+ let tip;
291
+ if (costScore < survivalScore) {
292
+ tip = 'Try shorter, focused sessions to reduce cost per commit.';
293
+ } else if (survivalRate < 50) {
294
+ tip = 'Review AI-generated code before committing to improve survival rate.';
295
+ } else if (orphanedRate > 40) {
296
+ tip = `${orphanedRate}% of sessions had no commits — some may be exploratory, which is fine.`;
297
+ } else {
298
+ tip = 'Keep it up — your efficiency is on track.';
299
+ }
300
+
301
+ return { score, tier, letter, explanation, tip };
302
+ }
303
+
265
304
  function computeSessionGrade(session) {
266
305
  if (session.commitCount === 0) return 'F';
267
306
  const costPerCommit = session.cost.totalCost / session.commitCount;
@@ -508,9 +547,11 @@ export function computeMetrics(correlatedSessions, organicCommits, commitsByRepo
508
547
  const lineSurvival = computeLineSurvival(commitsByRepo);
509
548
 
510
549
  const avgCost = totalCommits > 0 ? totalCost / totalCommits : 0;
550
+ const orphanedSessionRate = totalSessions > 0 ? Math.round((orphanedCount / totalSessions) * 100) : 0;
511
551
  const overallGrade = totalCommits > 0
512
552
  ? computeEfficiencyGrade(avgCost, lineSurvival.survivalRate)
513
553
  : 'F';
554
+ const efficiencyScore = computeEfficiencyScore(avgCost, lineSurvival.survivalRate, orphanedSessionRate, totalCommits);
514
555
 
515
556
  // ---- Daily timeline ----
516
557
  const dailyMap = new Map();
@@ -679,9 +720,10 @@ export function computeMetrics(correlatedSessions, organicCommits, commitsByRepo
679
720
  avgCostPerLine: totalLinesAdded > 0 ? totalCost / totalLinesAdded : null,
680
721
  totalInputTokens,
681
722
  totalOutputTokens,
682
- orphanedSessionRate: totalSessions > 0 ? Math.round((orphanedCount / totalSessions) * 100) : 0,
723
+ orphanedSessionRate,
683
724
  lineSurvivalRate: lineSurvival.survivalRate,
684
725
  overallGrade,
726
+ efficiencyScore,
685
727
  totalCommitsOnMain,
686
728
  mainBranchPct: totalCommits > 0 ? Math.round((totalCommitsOnMain / totalCommits) * 100) : 0,
687
729
  organicCommitCount: organicCommits.length,