claude-roi 0.8.0 → 0.8.2

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.2",
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;
@@ -787,6 +810,19 @@
787
810
  transform: scale(1.1);
788
811
  }
789
812
  tr.orphaned { background: rgba(245, 158, 11, 0.06); }
813
+ .expand-chevron {
814
+ display: inline-block;
815
+ width: 16px;
816
+ height: 16px;
817
+ margin-right: 6px;
818
+ vertical-align: middle;
819
+ transition: transform 0.2s ease;
820
+ color: var(--text-muted);
821
+ font-size: 0.7rem;
822
+ flex-shrink: 0;
823
+ }
824
+ tr:hover .expand-chevron { color: var(--accent-blue); }
825
+ tr.expanded .expand-chevron { transform: rotate(90deg); }
790
826
  .expand-row {
791
827
  display: none;
792
828
  background: var(--overlay-subtle);
@@ -1874,6 +1910,13 @@
1874
1910
  <script>
1875
1911
  const GRADE_VAR = { A: 'var(--grade-a)', B: 'var(--grade-b)', C: 'var(--grade-c)', D: 'var(--grade-d)', F: 'var(--grade-f)' };
1876
1912
  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)' };
1913
+ function scoreColor(score) {
1914
+ if (score >= 80) return 'var(--grade-a)';
1915
+ if (score >= 60) return 'var(--grade-b)';
1916
+ if (score >= 40) return 'var(--grade-c)';
1917
+ if (score >= 20) return 'var(--grade-d)';
1918
+ return 'var(--text-muted)';
1919
+ }
1877
1920
  const INSIGHT_ICONS = { warning: '!', success: '+', info: 'i', tip: '*' };
1878
1921
  const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1879
1922
  let DATA = null;
@@ -2081,11 +2124,11 @@ function render() {
2081
2124
  }
2082
2125
 
2083
2126
  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;
2127
+ const es = s.efficiencyScore || { score: 0, tier: 'Getting Started', letter: 'F', explanation: '', tip: '' };
2128
+ const sColor = scoreColor(es.score);
2129
+ const scoreDeg = Math.round(es.score * 3.6);
2087
2130
  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>
2131
+ <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
2132
  <div class="hero-stats">
2090
2133
  <div class="hero-stats-left">
2091
2134
  <div class="stat-card glow cost-card">
@@ -2119,11 +2162,14 @@ function renderHeroStats(s) {
2119
2162
  </div>
2120
2163
  </div>
2121
2164
  </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>
2165
+ <div class="stat-card grade glow grade-card" style="--grade-color: ${sColor}; --grade-deg: ${scoreDeg}deg;">
2166
+ <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
2167
  <div class="grade-circle">
2125
- <div class="value" style="color: ${gradeColor};">${s.overallGrade}</div>
2168
+ <div class="value" style="color: ${sColor};">${es.score}</div>
2126
2169
  </div>
2170
+ <div class="score-tier" style="color: ${sColor};">${es.tier}</div>
2171
+ <div class="score-context">${es.explanation}</div>
2172
+ <div class="score-tip">${es.tip}</div>
2127
2173
  <div class="sub">${s.orphanedSessionRate}% sessions orphaned</div>
2128
2174
  </div>
2129
2175
  </div>
@@ -2131,7 +2177,7 @@ function renderHeroStats(s) {
2131
2177
  <span><span class="dot" style="background: var(--accent-orange);"></span>Cost</span>
2132
2178
  <span><span class="dot" style="background: var(--accent-green);"></span>Output</span>
2133
2179
  <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>
2180
+ <span><span class="dot" style="background: ${sColor};"></span>Score: ${es.score} · ${es.tier}</span>
2135
2181
  </div>
2136
2182
  </div>`;
2137
2183
  }
@@ -2497,7 +2543,7 @@ function renderSessionsTable(sessions) {
2497
2543
  const thArrow = col => col === sortCol ? (sortOrder === 1 ? ' ^' : ' v') : '';
2498
2544
 
2499
2545
  return `<div class="sessions-section">
2500
- <h2>Sessions (${sessions.length})</h2>
2546
+ <h2>Sessions (${sessions.length}) <span style="font-size:0.75rem;font-weight:400;color:var(--accent-blue);opacity:0.7;margin-left:6px;">&#9654; click a row to view git commits</span></h2>
2501
2547
  <div class="sessions-table-wrap">
2502
2548
  <table>
2503
2549
  <colgroup><col><col><col><col><col><col><col><col><col></colgroup>
@@ -2529,8 +2575,8 @@ function renderSessionsTable(sessions) {
2529
2575
  : primaryName;
2530
2576
  const rowClass = s.isOrphaned ? 'orphaned' : '';
2531
2577
  return `
2532
- <tr class="${rowClass}" style="cursor:pointer;" onclick="toggleExpand(${idx})">
2533
- <td>${formatDate(s.startTime)}</td>
2578
+ <tr class="${rowClass}" style="cursor:pointer;" onclick="toggleExpand(${idx}, this)">
2579
+ <td><span class="expand-chevron">&#9654;</span>${formatDate(s.startTime)}</td>
2534
2580
  <td>${s.projectName || '—'}</td>
2535
2581
  <td>${modelDisplay}</td>
2536
2582
  <td>${s.userMessageCount + s.assistantMessageCount}</td>
@@ -2947,9 +2993,10 @@ function bindEvents() {
2947
2993
  }
2948
2994
  }
2949
2995
 
2950
- window.toggleExpand = function(idx) {
2996
+ window.toggleExpand = function(idx, clickedRow) {
2951
2997
  const row = document.getElementById(`expand-${idx}`);
2952
2998
  if (row) row.classList.toggle('open');
2999
+ if (clickedRow) clickedRow.classList.toggle('expanded');
2953
3000
  };
2954
3001
 
2955
3002
  window.sortTable = function(col) {
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,