Skip to content

Commit cc0fa04

Browse files
committed
Add percentages to loss
1 parent 2554eda commit cc0fa04

3 files changed

Lines changed: 37 additions & 17 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ The formula models cumulative probability that any given line in the PR becomes
331331
2. **Dependency Propagation**: Changes in one area often necessitate updates elsewhere
332332
3. **API Evolution**: Interface changes require downstream adaptation
333333

334-
**Empirical Calibration**: Nagappan et al. analyzed Windows Vista development and found organizational churndirectly correlated with defect density [10]. Active codebases exhibit 4-8% weekly churn; we use the conservative 4% baseline. The model predicts that a PR open for 30 days has ~16% of its code requiring updates, matching empirical observations of merge conflict rates.
334+
**Empirical Calibration**: Nagappan et al. analyzed Windows Vista development and found organizational churn directly correlated with defect density [10]. Active codebases exhibit 4-8% weekly churn; we use the conservative 4% baseline. The model predicts that a PR open for 30 days has ~16% of its code requiring updates, matching empirical observations of merge conflict rates.
335335

336336
**References**:
337337
[10] Nagappan, N., Murphy, B., & Basili, V. (2008). The Influence of Organizational Structure on Software Quality: An Empirical Case Study. *ICSE '08: Proceedings of the 30th International Conference on Software Engineering*, 521-530. DOI: 10.1145/1368088.1368160
@@ -397,7 +397,7 @@ Where:
397397
- 2 sessions: 1 for reviewer, 1 for author merge
398398
- Total: 2 × 40 min = 80 min (1.33 hours)
399399

400-
**Justification**: Based on Microsoft Research findings that context switches require ~20 minutes to restore working memory [2].
400+
**Justification**: Based on Microsoft Research findings that context switches require ~20 minutes to restore working memory [4].
401401

402402
**Total Future Cost Example** (649 LOC PR):
403403
- Review: 2.4 hours (size-dependent, 649 / 275)

cmd/prcost/repository.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,9 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
381381
// Average Preventable Loss Total (before grand total)
382382
avgPreventableCost := avgCodeChurnCost + avgDeliveryDelayCost + avgCoordinationCost
383383
avgPreventableHours := avgCodeChurnHours + avgDeliveryDelayHours + avgCoordinationHours
384-
fmt.Printf(" Preventable Loss Total $%12s %s\n",
385-
formatWithCommas(avgPreventableCost), formatTimeUnit(avgPreventableHours))
384+
avgPreventablePct := (avgPreventableCost / avgTotalCost) * 100
385+
fmt.Printf(" Preventable Loss Total $%12s %s (%.1f%%)\n",
386+
formatWithCommas(avgPreventableCost), formatTimeUnit(avgPreventableHours), avgPreventablePct)
386387

387388
// Average total
388389
fmt.Println(" ═══════════════════════════════════════════════════════════════")
@@ -490,8 +491,9 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
490491
// Preventable Loss Total (before grand total)
491492
preventableCost := ext.CodeChurnCost + ext.DeliveryDelayCost + ext.CoordinationCost
492493
preventableHours := ext.CodeChurnHours + ext.DeliveryDelayHours + ext.CoordinationHours
493-
fmt.Printf(" Preventable Loss Total $%12s %s\n",
494-
formatWithCommas(preventableCost), formatTimeUnit(preventableHours))
494+
preventablePct := (preventableCost / ext.TotalCost) * 100
495+
fmt.Printf(" Preventable Loss Total $%12s %s (%.1f%%)\n",
496+
formatWithCommas(preventableCost), formatTimeUnit(preventableHours), preventablePct)
495497

496498
// Extrapolated grand total
497499
fmt.Println(" ═══════════════════════════════════════════════════════════════")

internal/server/static/index.html

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ <h1>PR Cost Calculator</h1>
903903

904904
<p class="intro-text">
905905
Calculate the estimated development cost of a pull request - for a single PR, or extrapolate across a sample set within a repository or organization.
906-
Based on <a href="https://github.com/codeGROOVE-dev/prcost#how-it-works">recent software engineering research</a> (COCOMO II, Microsoft, IEEE, Fagan inspection studies).
906+
Based on <a href="https://github.com/codeGROOVE-dev/prcost?tab=readme-ov-file#cost-model-scientific-foundations">recent software engineering research</a> (COCOMO II, Microsoft, IEEE, Fagan inspection studies).
907907
</p>
908908
</header>
909909

@@ -1257,7 +1257,7 @@ <h3>Why calculate PR costs?</h3>
12571257
}
12581258
}
12591259

1260-
function formatEfficiencyHTML(efficiencyPct, grade, message, preventableCost, preventableHours, totalCost, totalHours, isAnnual = false, annualWasteCost = 0, annualWasteHours = 0, userCount = 0) {
1260+
function formatEfficiencyHTML(efficiencyPct, grade, message, preventableCost, preventableHours, totalCost, totalHours, isAnnual = false, annualWasteCost = 0, annualWasteHours = 0, userCount = 0, salary = 250000, benefitsMultiplier = 1.2, analysisType = 'project') {
12611261
let html = '<div class="efficiency-section">';
12621262
html += '<h2>Workflow Efficiency</h2>';
12631263

@@ -1276,7 +1276,9 @@ <h3>Why calculate PR costs?</h3>
12761276
if (isAnnual) {
12771277
html += '<div class="efficiency-annual">';
12781278
html += '<div class="efficiency-annual-label">Annual Wasted Effort</div>';
1279-
html += `<div class="efficiency-annual-value">${formatCurrency(annualWasteCost)}</div>`;
1279+
const annualWasteRounded = Math.round(annualWasteCost);
1280+
const annualWasteFormatted = '$' + annualWasteRounded.toLocaleString('en-US');
1281+
html += `<div class="efficiency-annual-value">${annualWasteFormatted}</div>`;
12801282
html += `<div class="efficiency-annual-time">${formatEngTimeUnit(annualWasteHours)}</div>`;
12811283
html += '</div>';
12821284
}
@@ -1288,9 +1290,9 @@ <h3>Why calculate PR costs?</h3>
12881290
grade === 'C-' || grade === 'D+' || grade === 'D' || grade === 'D-' || grade === 'F';
12891291

12901292
if (showCallout && isAnnual) {
1291-
// Calculate savings potential at 90% efficiency
1293+
// Calculate savings potential at 94% efficiency
12921294
const totalAnnualCost = totalCost * (annualWasteCost / preventableCost);
1293-
const targetWaste = totalAnnualCost * 0.10; // 10% waste at 90% efficiency
1295+
const targetWaste = totalAnnualCost * 0.06; // 6% waste at 94% efficiency
12941296
const currentWaste = annualWasteCost;
12951297
let savings = currentWaste - targetWaste;
12961298

@@ -1299,9 +1301,20 @@ <h3>Why calculate PR costs?</h3>
12991301
savings -= pricingCost;
13001302

13011303
if (savings > 0) {
1304+
// Format savings without decimals
1305+
const savingsRounded = Math.round(savings);
1306+
const savingsFormatted = '$' + savingsRounded.toLocaleString('en-US');
1307+
1308+
// Calculate headcount savings (fully-loaded salary)
1309+
const fullyLoadedSalary = salary * benefitsMultiplier;
1310+
const headcountSavings = Math.round(savings / fullyLoadedSalary);
1311+
1312+
// Strip + from grade if present (e.g., "C+" → "C")
1313+
const gradeForDisplay = grade.replace('+', '');
1314+
13021315
html += '<div class="efficiency-callout">';
1303-
html += `<p><strong>Tired of being a ${grade} student?</strong></p>`;
1304-
html += `<p>Come talk to us at <a href="mailto:go-faster@codeGROOVE.dev">go-faster@codeGROOVE.dev</a> to save at least <strong>${formatCurrency(savings)} a year</strong> in lost development velocity.</p>`;
1316+
html += `<p><strong>Tired of being a ${gradeForDisplay} ${analysisType}?</strong></p>`;
1317+
html += `<p>Come talk to us at <a href="mailto:go-faster@codeGROOVE.dev">go-faster@codeGROOVE.dev</a> to save at least <strong>${savingsFormatted} a year</strong> in lost development velocity. We're serious. That's like <strong>${headcountSavings} free headcount</strong> on your team. It's free for open-source projects.</p>`;
13051318
html += '</div>';
13061319
}
13071320
}
@@ -1534,7 +1547,8 @@ <h3>Why calculate PR costs?</h3>
15341547
// Average Preventable Loss Total (before grand total)
15351548
const avgPreventableCost = avgCodeChurnCost + avgDeliveryDelayCost + avgCoordinationCost;
15361549
const avgPreventableHours = avgCodeChurnHours + avgDeliveryDelayHours + avgCoordinationHours;
1537-
output += ` Preventable Loss Total ${formatCurrency(avgPreventableCost).padStart(12)} ${formatTimeUnit(avgPreventableHours)}\n`;
1550+
const avgPreventablePct = (avgPreventableCost / avgTotalCost) * 100;
1551+
output += ` Preventable Loss Total ${formatCurrency(avgPreventableCost).padStart(12)} ${formatTimeUnit(avgPreventableHours)} (${avgPreventablePct.toFixed(1)}%)\n`;
15381552

15391553
// Average Total
15401554
output += ' ═══════════════════════════════════════════════════════════════\n';
@@ -1606,7 +1620,8 @@ <h3>Why calculate PR costs?</h3>
16061620
// Preventable Loss Total (before grand total)
16071621
const preventableCost = e.code_churn_cost + e.delivery_delay_cost + e.coordination_cost;
16081622
const preventableHours = e.code_churn_hours + e.delivery_delay_hours + e.coordination_hours;
1609-
output += ` Preventable Loss Total ${formatCurrency(preventableCost).padStart(12)} ${formatTimeUnit(preventableHours)}\n`;
1623+
const preventablePct = (preventableCost / e.total_cost) * 100;
1624+
output += ` Preventable Loss Total ${formatCurrency(preventableCost).padStart(12)} ${formatTimeUnit(preventableHours)} (${preventablePct.toFixed(1)}%)\n`;
16101625

16111626
// Total
16121627
output += ' ═══════════════════════════════════════════════════════════════\n';
@@ -1805,7 +1820,7 @@ <h3>Why calculate PR costs?</h3>
18051820
html += `<h2>${sourceName}</h2>`;
18061821
html += '<div class="meta">';
18071822
html += `<strong>Period:</strong> Last ${days} days &nbsp;•&nbsp; `;
1808-
html += `<strong>Total PRs:</strong> ${e.total_prs} &nbsp;•&nbsp; `;
1823+
html += `<strong>Total public PRs:</strong> ${e.total_prs} &nbsp;•&nbsp; `;
18091824
html += `<strong>Sampled:</strong> ${e.successful_samples}`;
18101825
html += '</div>';
18111826
html += '</div>';
@@ -1820,7 +1835,10 @@ <h3>Why calculate PR costs?</h3>
18201835
const annualWasteCost = extPreventableCost * annualMultiplier;
18211836

18221837
// Workflow Efficiency section (at top)
1823-
html += formatEfficiencyHTML(extEfficiencyPct, extEfficiency.grade, extEfficiency.message, extPreventableCost, extPreventableHours, e.total_cost, e.total_hours, true, annualWasteCost, annualWasteHours, e.unique_users || 0);
1838+
const salary = request.config?.annual_salary || 250000;
1839+
const benefitsMultiplier = request.config?.benefits_multiplier || 1.2;
1840+
const analysisType = mode === 'org' ? 'organization' : 'project';
1841+
html += formatEfficiencyHTML(extEfficiencyPct, extEfficiency.grade, extEfficiency.message, extPreventableCost, extPreventableHours, e.total_cost, e.total_hours, true, annualWasteCost, annualWasteHours, e.unique_users || 0, salary, benefitsMultiplier, analysisType);
18241842

18251843
// Calculate average PR efficiency
18261844
const totalPRs = e.total_prs;

0 commit comments

Comments
 (0)