claude-roi 0.7.3 → 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.7.3",
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;
@@ -717,9 +740,20 @@
717
740
  width: 100%;
718
741
  border-collapse: collapse;
719
742
  font-size: 0.85rem;
720
- }
743
+ table-layout: fixed;
744
+ }
745
+ /* Column widths: Date | Project | Model | Msgs | Autopilot | Cost | Commits | Lines | Grade */
746
+ table col:nth-child(1) { width: 13%; }
747
+ table col:nth-child(2) { width: 14%; }
748
+ table col:nth-child(3) { width: 18%; }
749
+ table col:nth-child(4) { width: 7%; }
750
+ table col:nth-child(5) { width: 10%; }
751
+ table col:nth-child(6) { width: 8%; }
752
+ table col:nth-child(7) { width: 9%; }
753
+ table col:nth-child(8) { width: 13%; }
754
+ table col:nth-child(9) { width: 8%; }
721
755
  thead th {
722
- padding: 14px 14px;
756
+ padding: 14px 12px;
723
757
  text-align: left;
724
758
  color: var(--text-muted);
725
759
  font-family: var(--font-display);
@@ -734,6 +768,8 @@
734
768
  transition: color 0.2s;
735
769
  position: relative;
736
770
  }
771
+ /* Center-align numeric columns (4-9): Msgs, Autopilot, Cost, Commits, Lines, Grade */
772
+ thead th:nth-child(n+4), tbody td:nth-child(n+4) { text-align: center; }
737
773
  thead th:hover { color: var(--text-primary); }
738
774
  thead th.sorted { color: var(--accent-blue); }
739
775
  thead th.sorted::after {
@@ -752,9 +788,11 @@
752
788
  tbody tr:hover { background: var(--overlay-soft); }
753
789
  tbody tr:last-child { border-bottom: none; }
754
790
  tbody td {
755
- padding: 12px 14px;
791
+ padding: 12px 12px;
756
792
  white-space: nowrap;
757
793
  font-size: 0.85rem;
794
+ overflow: hidden;
795
+ text-overflow: ellipsis;
758
796
  }
759
797
  .grade-badge {
760
798
  display: inline-flex;
@@ -1859,6 +1897,13 @@
1859
1897
  <script>
1860
1898
  const GRADE_VAR = { A: 'var(--grade-a)', B: 'var(--grade-b)', C: 'var(--grade-c)', D: 'var(--grade-d)', F: 'var(--grade-f)' };
1861
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
+ }
1862
1907
  const INSIGHT_ICONS = { warning: '!', success: '+', info: 'i', tip: '*' };
1863
1908
  const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1864
1909
  let DATA = null;
@@ -2066,11 +2111,11 @@ function render() {
2066
2111
  }
2067
2112
 
2068
2113
  function renderHeroStats(s) {
2069
- const gradeColor = GRADE_VAR[s.overallGrade] || GRADE_VAR.F;
2070
- const GRADE_DEG = { A: 324, B: 270, C: 216, D: 144, F: 72 };
2071
- 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);
2072
2117
  return `<div class="stats-section">
2073
- <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>
2074
2119
  <div class="hero-stats">
2075
2120
  <div class="hero-stats-left">
2076
2121
  <div class="stat-card glow cost-card">
@@ -2104,11 +2149,14 @@ function renderHeroStats(s) {
2104
2149
  </div>
2105
2150
  </div>
2106
2151
  </div>
2107
- <div class="stat-card grade glow grade-card" style="--grade-color: ${gradeColor}; --grade-deg: ${gradeDeg}deg;">
2108
- <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>
2109
2154
  <div class="grade-circle">
2110
- <div class="value" style="color: ${gradeColor};">${s.overallGrade}</div>
2155
+ <div class="value" style="color: ${sColor};">${es.score}</div>
2111
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>
2112
2160
  <div class="sub">${s.orphanedSessionRate}% sessions orphaned</div>
2113
2161
  </div>
2114
2162
  </div>
@@ -2116,7 +2164,7 @@ function renderHeroStats(s) {
2116
2164
  <span><span class="dot" style="background: var(--accent-orange);"></span>Cost</span>
2117
2165
  <span><span class="dot" style="background: var(--accent-green);"></span>Output</span>
2118
2166
  <span><span class="dot" style="background: var(--accent-blue);"></span>Efficiency</span>
2119
- <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>
2120
2168
  </div>
2121
2169
  </div>`;
2122
2170
  }
@@ -2485,6 +2533,7 @@ function renderSessionsTable(sessions) {
2485
2533
  <h2>Sessions (${sessions.length})</h2>
2486
2534
  <div class="sessions-table-wrap">
2487
2535
  <table>
2536
+ <colgroup><col><col><col><col><col><col><col><col><col></colgroup>
2488
2537
  <thead>
2489
2538
  <tr>
2490
2539
  <th onclick="sortTable('startTime')" class="${sortCol === 'startTime' ? 'sorted' : ''}">Date${thArrow('startTime')}</th>
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,