Skip to content

Commit 9838cd3

Browse files
committed
Improve future cost calculations
1 parent 5e98e2d commit 9838cd3

3 files changed

Lines changed: 89 additions & 46 deletions

File tree

cmd/prcost/repository.go

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
396396
fmt.Println()
397397
}
398398

399-
// Merge Delay section
399+
// Delay Costs section
400400
avgHumanOpenTime := formatTimeUnit(ext.AvgHumanPRDurationHours)
401401
avgBotOpenTime := formatTimeUnit(ext.AvgBotPRDurationHours)
402402
delayCostsHeader := fmt.Sprintf(" Delay Costs (human PRs avg %s open", avgHumanOpenTime)
@@ -422,6 +422,17 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
422422
fmt.Print(formatSubtotalLine(avgMergeDelayCost, formatTimeUnit(avgMergeDelayHours), fmt.Sprintf("(%.1f%%)", pct)))
423423
fmt.Println()
424424

425+
// Preventable Future Costs section
426+
if avgCodeChurnCost > 0 {
427+
fmt.Println(" Preventable Future Costs")
428+
fmt.Println(" ────────────────────────")
429+
fmt.Print(formatItemLine("Rework due to churn", avgCodeChurnCost, formatTimeUnit(avgCodeChurnHours), fmt.Sprintf("(%d PRs)", ext.CodeChurnPRCount)))
430+
fmt.Print(formatSectionDivider())
431+
pct = (avgCodeChurnCost / avgTotalCost) * 100
432+
fmt.Print(formatSubtotalLine(avgCodeChurnCost, formatTimeUnit(avgCodeChurnHours), fmt.Sprintf("(%.1f%%)", pct)))
433+
fmt.Println()
434+
}
435+
425436
// Future Costs section
426437
avgFutureReviewCost := ext.FutureReviewCost / float64(ext.TotalPRs)
427438
avgFutureMergeCost := ext.FutureMergeCost / float64(ext.TotalPRs)
@@ -430,15 +441,12 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
430441
avgFutureMergeHours := ext.FutureMergeHours / float64(ext.TotalPRs)
431442
avgFutureContextHours := ext.FutureContextHours / float64(ext.TotalPRs)
432443

433-
hasFutureCosts := ext.CodeChurnCost > 0.01 || ext.FutureReviewCost > 0.01 ||
444+
hasFutureCosts := ext.FutureReviewCost > 0.01 ||
434445
ext.FutureMergeCost > 0.01 || ext.FutureContextCost > 0.01
435446

436447
if hasFutureCosts {
437448
fmt.Println(" Future Costs")
438449
fmt.Println(" ────────────")
439-
if ext.CodeChurnCost > 0.01 {
440-
fmt.Print(formatItemLine("Code Churn", avgCodeChurnCost, formatTimeUnit(avgCodeChurnHours), fmt.Sprintf("(%d PRs)", ext.CodeChurnPRCount)))
441-
}
442450
if ext.FutureReviewCost > 0.01 {
443451
fmt.Print(formatItemLine("Review", avgFutureReviewCost, formatTimeUnit(avgFutureReviewHours), fmt.Sprintf("(%d PRs)", ext.FutureReviewPRCount)))
444452
}
@@ -449,8 +457,8 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
449457
avgFutureContextSessions := float64(ext.FutureContextSessions) / float64(ext.TotalPRs)
450458
fmt.Print(formatItemLine("Context Switching", avgFutureContextCost, formatTimeUnit(avgFutureContextHours), fmt.Sprintf("(%.1f sessions)", avgFutureContextSessions)))
451459
}
452-
avgFutureCost := avgCodeChurnCost + avgFutureReviewCost + avgFutureMergeCost + avgFutureContextCost
453-
avgFutureHours := avgCodeChurnHours + avgFutureReviewHours + avgFutureMergeHours + avgFutureContextHours
460+
avgFutureCost := avgFutureReviewCost + avgFutureMergeCost + avgFutureContextCost
461+
avgFutureHours := avgFutureReviewHours + avgFutureMergeHours + avgFutureContextHours
454462
fmt.Print(formatSectionDivider())
455463
pct = (avgFutureCost / avgTotalCost) * 100
456464
fmt.Print(formatSubtotalLine(avgFutureCost, formatTimeUnit(avgFutureHours), fmt.Sprintf("(%.1f%%)", pct)))
@@ -529,7 +537,7 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
529537
fmt.Println()
530538
}
531539

532-
// Merge Delay section (extrapolated)
540+
// Delay Costs section (extrapolated)
533541
extAvgHumanOpenTime := formatTimeUnit(ext.AvgHumanPRDurationHours)
534542
extAvgBotOpenTime := formatTimeUnit(ext.AvgBotPRDurationHours)
535543
extDelayCostsHeader := fmt.Sprintf(" Delay Costs (human PRs avg %s open", extAvgHumanOpenTime)
@@ -549,25 +557,33 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
549557
if ext.PRTrackingCost > 0 {
550558
fmt.Print(formatItemLine("PR Tracking", ext.PRTrackingCost, formatTimeUnit(ext.PRTrackingHours), fmt.Sprintf("(%d open PRs)", ext.OpenPRs)))
551559
}
552-
extMergeDelayCost := ext.DeliveryDelayCost + ext.CodeChurnCost + ext.AutomatedUpdatesCost + ext.PRTrackingCost
553-
extMergeDelayHours := ext.DeliveryDelayHours + ext.CodeChurnHours + ext.AutomatedUpdatesHours + ext.PRTrackingHours
560+
extMergeDelayCost := ext.DeliveryDelayCost + ext.AutomatedUpdatesCost + ext.PRTrackingCost
561+
extMergeDelayHours := ext.DeliveryDelayHours + ext.AutomatedUpdatesHours + ext.PRTrackingHours
554562
fmt.Print(formatSectionDivider())
555563
pct = (extMergeDelayCost / ext.TotalCost) * 100
556564
fmt.Print(formatSubtotalLine(extMergeDelayCost, formatTimeUnit(extMergeDelayHours), fmt.Sprintf("(%.1f%%)", pct)))
557565
fmt.Println()
558566

567+
// Preventable Future Costs section (extrapolated)
568+
if ext.CodeChurnCost > 0 {
569+
fmt.Println(" Preventable Future Costs")
570+
fmt.Println(" ────────────────────────")
571+
totalKLOC := float64(ext.TotalNewLines+ext.TotalModifiedLines) / 1000.0
572+
churnLOCStr := formatLOC(totalKLOC)
573+
fmt.Print(formatItemLine("Rework due to churn", ext.CodeChurnCost, formatTimeUnit(ext.CodeChurnHours), fmt.Sprintf("(%d PRs, ~%s)", ext.CodeChurnPRCount, churnLOCStr)))
574+
fmt.Print(formatSectionDivider())
575+
pct = (ext.CodeChurnCost / ext.TotalCost) * 100
576+
fmt.Print(formatSubtotalLine(ext.CodeChurnCost, formatTimeUnit(ext.CodeChurnHours), fmt.Sprintf("(%.1f%%)", pct)))
577+
fmt.Println()
578+
}
579+
559580
// Future Costs section (extrapolated)
560-
extHasFutureCosts := ext.CodeChurnCost > 0.01 || ext.FutureReviewCost > 0.01 ||
581+
extHasFutureCosts := ext.FutureReviewCost > 0.01 ||
561582
ext.FutureMergeCost > 0.01 || ext.FutureContextCost > 0.01
562583

563584
if extHasFutureCosts {
564585
fmt.Println(" Future Costs")
565586
fmt.Println(" ────────────")
566-
if ext.CodeChurnCost > 0.01 {
567-
totalKLOC := float64(ext.TotalNewLines+ext.TotalModifiedLines) / 1000.0
568-
churnLOCStr := formatLOC(totalKLOC)
569-
fmt.Print(formatItemLine("Code Churn", ext.CodeChurnCost, formatTimeUnit(ext.CodeChurnHours), fmt.Sprintf("(%d PRs, ~%s)", ext.CodeChurnPRCount, churnLOCStr)))
570-
}
571587
if ext.FutureReviewCost > 0.01 {
572588
fmt.Print(formatItemLine("Review", ext.FutureReviewCost, formatTimeUnit(ext.FutureReviewHours), fmt.Sprintf("(%d PRs)", ext.FutureReviewPRCount)))
573589
}
@@ -577,8 +593,8 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
577593
if ext.FutureContextCost > 0.01 {
578594
fmt.Print(formatItemLine("Context Switching", ext.FutureContextCost, formatTimeUnit(ext.FutureContextHours), fmt.Sprintf("(%d sessions)", ext.FutureContextSessions)))
579595
}
580-
extFutureCost := ext.CodeChurnCost + ext.FutureReviewCost + ext.FutureMergeCost + ext.FutureContextCost
581-
extFutureHours := ext.CodeChurnHours + ext.FutureReviewHours + ext.FutureMergeHours + ext.FutureContextHours
596+
extFutureCost := ext.FutureReviewCost + ext.FutureMergeCost + ext.FutureContextCost
597+
extFutureHours := ext.FutureReviewHours + ext.FutureMergeHours + ext.FutureContextHours
582598
fmt.Print(formatSectionDivider())
583599
pct = (extFutureCost / ext.TotalCost) * 100
584600
fmt.Print(formatSubtotalLine(extFutureCost, formatTimeUnit(extFutureHours), fmt.Sprintf("(%.1f%%)", pct)))

internal/server/static/index.html

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,26 +1592,33 @@ <h3>Why calculate PR costs?</h3>
15921592
output += ' ───────────\n';
15931593
const cappedLabel = b.delay_capped ? ' (capped)' : '';
15941594
output += ` Workstream blockage ${formatCurrency(b.delay_cost_detail.delivery_delay_cost).padStart(12)} ${formatTimeUnit(b.delay_cost_detail.delivery_delay_hours)}${cappedLabel}\n`;
1595-
const mergeDelayCost = b.delay_cost_detail.delivery_delay_cost + b.delay_cost_detail.code_churn_cost + b.delay_cost_detail.automated_updates_cost + b.delay_cost_detail.pr_tracking_cost;
1596-
const mergeDelayHours = b.delay_cost_detail.delivery_delay_hours + b.delay_cost_detail.code_churn_hours + b.delay_cost_detail.automated_updates_hours + b.delay_cost_detail.pr_tracking_hours;
1595+
const mergeDelayCost = b.delay_cost_detail.delivery_delay_cost + b.delay_cost_detail.automated_updates_cost + b.delay_cost_detail.pr_tracking_cost;
1596+
const mergeDelayHours = b.delay_cost_detail.delivery_delay_hours + b.delay_cost_detail.automated_updates_hours + b.delay_cost_detail.pr_tracking_hours;
15971597
output += ' ────────────\n';
15981598
pct = (mergeDelayCost / b.total_cost) * 100;
15991599
output += formatSubtotalLine("Subtotal", mergeDelayCost, formatTimeUnit(mergeDelayHours), `(${pct.toFixed(1)}%)`);
16001600
output += '\n';
16011601

1602+
// Preventable Future Costs
1603+
if (b.delay_cost_detail.rework_percentage > 0) {
1604+
output += ' Preventable Future Costs\n';
1605+
output += ' ────────────────────────\n';
1606+
output += ` Rework due to churn (${Math.round(b.delay_cost_detail.rework_percentage)}% drift) ${formatCurrency(b.delay_cost_detail.code_churn_cost).padStart(12)} ${formatTimeUnit(b.delay_cost_detail.code_churn_hours)}\n`;
1607+
output += ' ────────────\n';
1608+
pct = (b.delay_cost_detail.code_churn_cost / b.total_cost) * 100;
1609+
output += formatSubtotalLine("Subtotal", b.delay_cost_detail.code_churn_cost, formatTimeUnit(b.delay_cost_detail.code_churn_hours), `(${pct.toFixed(1)}%)`);
1610+
output += '\n';
1611+
}
1612+
16021613
// Future Costs
1603-
const hasFuture = b.delay_cost_detail.rework_percentage > 0 ||
1604-
b.delay_cost_detail.future_review_cost > 0 ||
1614+
const hasFuture = b.delay_cost_detail.future_review_cost > 0 ||
16051615
b.delay_cost_detail.future_merge_cost > 0 ||
16061616
b.delay_cost_detail.future_context_cost > 0;
16071617

16081618
if (hasFuture) {
16091619
output += ' Future Costs\n';
16101620
output += ' ────────────\n';
16111621

1612-
if (b.delay_cost_detail.rework_percentage > 0) {
1613-
output += ` Code Churn (${Math.round(b.delay_cost_detail.rework_percentage)}% drift) ${formatCurrency(b.delay_cost_detail.code_churn_cost).padStart(12)} ${formatTimeUnit(b.delay_cost_detail.code_churn_hours)}\n`;
1614-
}
16151622
if (b.delay_cost_detail.future_review_cost > 0) {
16161623
output += ` Review ${formatCurrency(b.delay_cost_detail.future_review_cost).padStart(12)} ${formatTimeUnit(b.delay_cost_detail.future_review_hours)}\n`;
16171624
}
@@ -1622,9 +1629,9 @@ <h3>Why calculate PR costs?</h3>
16221629
output += ` Context Switching ${formatCurrency(b.delay_cost_detail.future_context_cost).padStart(12)} ${formatTimeUnit(b.delay_cost_detail.future_context_hours)}\n`;
16231630
}
16241631

1625-
const futureCost = b.delay_cost_detail.code_churn_cost + b.delay_cost_detail.future_review_cost +
1632+
const futureCost = b.delay_cost_detail.future_review_cost +
16261633
b.delay_cost_detail.future_merge_cost + b.delay_cost_detail.future_context_cost;
1627-
const futureHours = b.delay_cost_detail.code_churn_hours + b.delay_cost_detail.future_review_hours +
1634+
const futureHours = b.delay_cost_detail.future_review_hours +
16281635
b.delay_cost_detail.future_merge_hours + b.delay_cost_detail.future_context_hours;
16291636
output += ' ────────────\n';
16301637
pct = (futureCost / b.total_cost) * 100;
@@ -1840,23 +1847,31 @@ <h3>Why calculate PR costs?</h3>
18401847
if (avgPRTrackingCost > 0.01) {
18411848
output += formatItemLine("PR Tracking", avgPRTrackingCost, formatTimeUnit(avgPRTrackingHours), `(${e.open_prs} open PRs)`);
18421849
}
1843-
const avgMergeDelayCost = avgDeliveryDelayCost + avgCodeChurnCost + avgAutomatedUpdatesCost + avgPRTrackingCost;
1844-
const avgMergeDelayHours = avgDeliveryDelayHours + avgCodeChurnHours + avgAutomatedUpdatesHours + avgPRTrackingHours;
1850+
const avgMergeDelayCost = avgDeliveryDelayCost + avgAutomatedUpdatesCost + avgPRTrackingCost;
1851+
const avgMergeDelayHours = avgDeliveryDelayHours + avgAutomatedUpdatesHours + avgPRTrackingHours;
18451852
output += ' ──────────\n';
18461853
pct = (avgMergeDelayCost / avgTotalCost) * 100;
18471854
output += formatSubtotalLine("Subtotal", avgMergeDelayCost, formatTimeUnit(avgMergeDelayHours), `(${pct.toFixed(1)}%)`);
18481855
output += '\n';
18491856

1857+
// Preventable Future Costs
1858+
if (e.code_churn_cost > 0.01) {
1859+
output += ' Preventable Future Costs\n';
1860+
output += ' ────────────────────────\n';
1861+
const avgReworkPct = e.avg_rework_percentage || 0;
1862+
const label = avgReworkPct > 0 ? `Rework due to churn (${avgReworkPct.toFixed(0)}% drift)` : 'Rework due to churn';
1863+
output += formatItemLine(label, avgCodeChurnCost, formatTimeUnit(avgCodeChurnHours), `(${e.code_churn_pr_count} PRs)`);
1864+
output += ' ──────────\n';
1865+
pct = (avgCodeChurnCost / avgTotalCost) * 100;
1866+
output += formatSubtotalLine("Subtotal", avgCodeChurnCost, formatTimeUnit(avgCodeChurnHours), `(${pct.toFixed(1)}%)`);
1867+
output += '\n';
1868+
}
1869+
18501870
// Future Costs
1851-
const hasFuture = e.code_churn_cost > 0.01 || e.future_review_cost > 0.01 || e.future_merge_cost > 0.01 || e.future_context_cost > 0.01;
1871+
const hasFuture = e.future_review_cost > 0.01 || e.future_merge_cost > 0.01 || e.future_context_cost > 0.01;
18521872
if (hasFuture) {
18531873
output += ' Future Costs\n';
18541874
output += ' ────────────\n';
1855-
if (e.code_churn_cost > 0.01) {
1856-
const avgReworkPct = e.avg_rework_percentage || 0;
1857-
const label = avgReworkPct > 0 ? `Code Churn (${avgReworkPct.toFixed(0)}% drift)` : 'Code Churn';
1858-
output += formatItemLine(label, avgCodeChurnCost, formatTimeUnit(avgCodeChurnHours), `(${e.code_churn_pr_count} PRs)`);
1859-
}
18601875
if (e.future_review_cost > 0.01) {
18611876
output += formatItemLine("Review", avgFutureReviewCost, formatTimeUnit(avgFutureReviewHours), `(${e.future_review_pr_count} PRs)`);
18621877
}
@@ -1867,8 +1882,8 @@ <h3>Why calculate PR costs?</h3>
18671882
const avgFutureContextSessions = e.future_context_sessions / totalPRs;
18681883
output += formatItemLine("Context Switching", avgFutureContextCost, formatTimeUnit(avgFutureContextHours), `(${avgFutureContextSessions.toFixed(1)} sessions)`);
18691884
}
1870-
const avgFutureCost = avgCodeChurnCost + avgFutureReviewCost + avgFutureMergeCost + avgFutureContextCost;
1871-
const avgFutureHours = avgCodeChurnHours + avgFutureReviewHours + avgFutureMergeHours + avgFutureContextHours;
1885+
const avgFutureCost = avgFutureReviewCost + avgFutureMergeCost + avgFutureContextCost;
1886+
const avgFutureHours = avgFutureReviewHours + avgFutureMergeHours + avgFutureContextHours;
18721887
output += ' ──────────\n';
18731888
pct = (avgFutureCost / avgTotalCost) * 100;
18741889
output += formatSubtotalLine("Subtotal", avgFutureCost, formatTimeUnit(avgFutureHours), `(${pct.toFixed(1)}%)`);
@@ -1951,21 +1966,29 @@ <h3>Why calculate PR costs?</h3>
19511966
output += formatItemLine("PR Tracking", e.pr_tracking_cost, formatTimeUnit(e.pr_tracking_hours), `(${e.open_prs || 0} open PRs)`);
19521967
}
19531968

1954-
const mergeDelayCost = (e.delivery_delay_cost || 0) + (e.code_churn_cost || 0) + (e.automated_updates_cost || 0) + (e.pr_tracking_cost || 0);
1955-
const mergeDelayHours = (e.delivery_delay_hours || 0) + (e.code_churn_hours || 0) + (e.automated_updates_hours || 0) + (e.pr_tracking_hours || 0);
1969+
const mergeDelayCost = (e.delivery_delay_cost || 0) + (e.automated_updates_cost || 0) + (e.pr_tracking_cost || 0);
1970+
const mergeDelayHours = (e.delivery_delay_hours || 0) + (e.automated_updates_hours || 0) + (e.pr_tracking_hours || 0);
19561971
output += ' ──────────\n';
19571972
pct = (mergeDelayCost / e.total_cost) * 100;
19581973
output += formatSubtotalLine("Subtotal", mergeDelayCost, formatTimeUnit(mergeDelayHours), `(${pct.toFixed(1)}%)`);
19591974
output += '\n';
19601975

1976+
// Preventable Future Costs
1977+
if ((e.code_churn_cost || 0) > 0.01) {
1978+
output += ' Preventable Future Costs\n';
1979+
output += ' ────────────────────────\n';
1980+
output += formatItemLine("Rework due to churn", e.code_churn_cost, formatTimeUnit(e.code_churn_hours), `(${e.code_churn_pr_count || 0} PRs)`);
1981+
output += ' ──────────\n';
1982+
pct = (e.code_churn_cost / e.total_cost) * 100;
1983+
output += formatSubtotalLine("Subtotal", e.code_churn_cost, formatTimeUnit(e.code_churn_hours), `(${pct.toFixed(1)}%)`);
1984+
output += '\n';
1985+
}
1986+
19611987
// Future Costs
1962-
const hasFuture = (e.code_churn_cost || 0) > 0.01 || (e.future_review_cost || 0) > 0.01 || (e.future_merge_cost || 0) > 0.01 || (e.future_context_cost || 0) > 0.01;
1988+
const hasFuture = (e.future_review_cost || 0) > 0.01 || (e.future_merge_cost || 0) > 0.01 || (e.future_context_cost || 0) > 0.01;
19631989
if (hasFuture) {
19641990
output += ' Future Costs\n';
19651991
output += ' ────────────\n';
1966-
if ((e.code_churn_cost || 0) > 0.01) {
1967-
output += formatItemLine("Code Churn", e.code_churn_cost, formatTimeUnit(e.code_churn_hours), "");
1968-
}
19691992
if ((e.future_review_cost || 0) > 0.01) {
19701993
const openPRs = e.open_prs || 0;
19711994
output += formatItemLine("Review", e.future_review_cost, formatTimeUnit(e.future_review_hours), `(${openPRs} PRs)`);
@@ -1977,8 +2000,8 @@ <h3>Why calculate PR costs?</h3>
19772000
if ((e.future_context_cost || 0) > 0.01) {
19782001
output += formatItemLine("Context Switching", e.future_context_cost, formatTimeUnit(e.future_context_hours), `(${e.future_context_sessions || 0} sessions)`);
19792002
}
1980-
const futureCost = (e.code_churn_cost || 0) + (e.future_review_cost || 0) + (e.future_merge_cost || 0) + (e.future_context_cost || 0);
1981-
const futureHours = (e.code_churn_hours || 0) + (e.future_review_hours || 0) + (e.future_merge_hours || 0) + (e.future_context_hours || 0);
2003+
const futureCost = (e.future_review_cost || 0) + (e.future_merge_cost || 0) + (e.future_context_cost || 0);
2004+
const futureHours = (e.future_review_hours || 0) + (e.future_merge_hours || 0) + (e.future_context_hours || 0);
19822005
output += ' ──────────\n';
19832006
pct = (futureCost / e.total_cost) * 100;
19842007
output += formatSubtotalLine("Subtotal", futureCost, formatTimeUnit(futureHours), `(${pct.toFixed(1)}%)`);

0 commit comments

Comments
 (0)