@@ -16,7 +16,7 @@ import (
1616// and extrapolation - all functionality is available to external clients.
1717//
1818//nolint:revive // argument-limit: acceptable for entry point function
19- func analyzeRepository (ctx context.Context , owner , repo string , sampleSize , days int , cfg cost.Config , token , dataSource string , modelMergeTime * time. Duration ) error {
19+ func analyzeRepository (ctx context.Context , owner , repo string , sampleSize , days int , cfg cost.Config , token , dataSource string ) error {
2020 // Calculate since date
2121 since := time .Now ().AddDate (0 , 0 , - days )
2222
@@ -105,7 +105,7 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
105105 extrapolated := cost .ExtrapolateFromSamples (breakdowns , len (prs ), totalAuthors , openPRCount , actualDays , cfg )
106106
107107 // Display results in itemized format
108- printExtrapolatedResults (fmt .Sprintf ("%s/%s" , owner , repo ), actualDays , & extrapolated , cfg , * modelMergeTime )
108+ printExtrapolatedResults (fmt .Sprintf ("%s/%s" , owner , repo ), actualDays , & extrapolated , cfg )
109109
110110 return nil
111111}
@@ -115,7 +115,7 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
115115// and extrapolation - all functionality is available to external clients.
116116//
117117//nolint:revive // argument-limit: acceptable for entry point function
118- func analyzeOrganization (ctx context.Context , org string , sampleSize , days int , cfg cost.Config , token , dataSource string , modelMergeTime * time. Duration ) error {
118+ func analyzeOrganization (ctx context.Context , org string , sampleSize , days int , cfg cost.Config , token , dataSource string ) error {
119119 slog .Info ("Fetching PR list from organization" )
120120
121121 // Calculate since date
@@ -207,7 +207,7 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,
207207 extrapolated := cost .ExtrapolateFromSamples (breakdowns , len (prs ), totalAuthors , totalOpenPRs , actualDays , cfg )
208208
209209 // Display results in itemized format
210- printExtrapolatedResults (fmt .Sprintf ("%s (organization)" , org ), actualDays , & extrapolated , cfg , * modelMergeTime )
210+ printExtrapolatedResults (fmt .Sprintf ("%s (organization)" , org ), actualDays , & extrapolated , cfg )
211211
212212 return nil
213213}
@@ -278,7 +278,7 @@ func formatTimeUnit(hours float64) string {
278278// printExtrapolatedResults displays extrapolated cost breakdown in itemized format.
279279//
280280//nolint:maintidx,revive // acceptable complexity/length for comprehensive display function
281- func printExtrapolatedResults (title string , days int , ext * cost.ExtrapolatedBreakdown , cfg cost.Config , modelMergeTime time. Duration ) {
281+ func printExtrapolatedResults (title string , days int , ext * cost.ExtrapolatedBreakdown , cfg cost.Config ) {
282282 fmt .Println ()
283283 fmt .Printf (" %s\n " , title )
284284 avgOpenTime := formatTimeUnit (ext .AvgPRDurationHours )
@@ -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 )))
@@ -598,11 +614,11 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
598614 fmt .Println ()
599615
600616 // Print extrapolated efficiency score + annual waste
601- printExtrapolatedEfficiency (ext , days , cfg , modelMergeTime )
617+ printExtrapolatedEfficiency (ext , days , cfg )
602618}
603619
604620// printExtrapolatedEfficiency prints the workflow efficiency + annual waste section for extrapolated totals.
605- func printExtrapolatedEfficiency (ext * cost.ExtrapolatedBreakdown , days int , cfg cost.Config , modelMergeTime time. Duration ) {
621+ func printExtrapolatedEfficiency (ext * cost.ExtrapolatedBreakdown , days int , cfg cost.Config ) {
606622 // Calculate preventable waste: Code Churn + All Delay Costs + Automated Updates + PR Tracking
607623 preventableHours := ext .CodeChurnHours + ext .DeliveryDelayHours + ext .AutomatedUpdatesHours + ext .PRTrackingHours
608624 preventableCost := ext .CodeChurnCost + ext .DeliveryDelayCost + ext .AutomatedUpdatesCost + ext .PRTrackingCost
@@ -660,14 +676,14 @@ func printExtrapolatedEfficiency(ext *cost.ExtrapolatedBreakdown, days int, cfg
660676 fmt .Println ()
661677
662678 // Print merge time modeling callout if average PR duration exceeds model merge time
663- if ext .AvgPRDurationHours > modelMergeTime . Hours () {
664- printExtrapolatedMergeTimeModelingCallout (ext , days , modelMergeTime , cfg )
679+ if ext .AvgPRDurationHours > cfg . TargetMergeTimeHours {
680+ printExtrapolatedMergeTimeModelingCallout (ext , days , cfg )
665681 }
666682}
667683
668684// printExtrapolatedMergeTimeModelingCallout prints a callout showing potential savings from reduced merge time.
669- func printExtrapolatedMergeTimeModelingCallout (ext * cost.ExtrapolatedBreakdown , days int , targetMergeTime time. Duration , cfg cost.Config ) {
670- targetHours := targetMergeTime . Hours ()
685+ func printExtrapolatedMergeTimeModelingCallout (ext * cost.ExtrapolatedBreakdown , days int , cfg cost.Config ) {
686+ targetHours := cfg . TargetMergeTimeHours
671687
672688 // Calculate hourly rate
673689 hourlyRate := (cfg .AnnualSalary * cfg .BenefitsMultiplier ) / cfg .HoursPerYear
@@ -686,7 +702,7 @@ func printExtrapolatedMergeTimeModelingCallout(ext *cost.ExtrapolatedBreakdown,
686702
687703 // PR tracking: scales with open time
688704 remodelPRTrackingPerPR := 0.0
689- if targetHours >= 1.0 { // Only track PRs open >= 1 hour
705+ if targetHours >= 1.0 { // Minimal tracking for PRs open >= 1 hour
690706 daysOpen := targetHours / 24.0
691707 remodelPRTrackingHours := (cfg .PRTrackingMinutesPerDay / 60.0 ) * daysOpen
692708 remodelPRTrackingPerPR = remodelPRTrackingHours * hourlyRate
0 commit comments