@@ -14,7 +14,9 @@ import (
1414// analyzeRepository performs repository-wide cost analysis by sampling PRs.
1515// Uses library functions from pkg/github and pkg/cost for fetching, sampling,
1616// and extrapolation - all functionality is available to external clients.
17- func analyzeRepository (ctx context.Context , owner , repo string , sampleSize , days int , cfg cost.Config , token string , dataSource string ) error {
17+ //
18+ //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 {
1820 // Calculate since date
1921 since := time .Now ().AddDate (0 , 0 , - days )
2022
@@ -103,15 +105,17 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
103105 extrapolated := cost .ExtrapolateFromSamples (breakdowns , len (prs ), totalAuthors , openPRCount , actualDays , cfg )
104106
105107 // Display results in itemized format
106- printExtrapolatedResults (fmt .Sprintf ("%s/%s" , owner , repo ), actualDays , & extrapolated , cfg )
108+ printExtrapolatedResults (fmt .Sprintf ("%s/%s" , owner , repo ), actualDays , & extrapolated , cfg , * modelMergeTime )
107109
108110 return nil
109111}
110112
111113// analyzeOrganization performs organization-wide cost analysis by sampling PRs across all repos.
112114// Uses library functions from pkg/github and pkg/cost for fetching, sampling,
113115// and extrapolation - all functionality is available to external clients.
114- func analyzeOrganization (ctx context.Context , org string , sampleSize , days int , cfg cost.Config , token string , dataSource string ) error {
116+ //
117+ //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 {
115119 slog .Info ("Fetching PR list from organization" )
116120
117121 // Calculate since date
@@ -203,7 +207,7 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,
203207 extrapolated := cost .ExtrapolateFromSamples (breakdowns , len (prs ), totalAuthors , totalOpenPRs , actualDays , cfg )
204208
205209 // Display results in itemized format
206- printExtrapolatedResults (fmt .Sprintf ("%s (organization)" , org ), actualDays , & extrapolated , cfg )
210+ printExtrapolatedResults (fmt .Sprintf ("%s (organization)" , org ), actualDays , & extrapolated , cfg , * modelMergeTime )
207211
208212 return nil
209213}
@@ -274,7 +278,7 @@ func formatTimeUnit(hours float64) string {
274278// printExtrapolatedResults displays extrapolated cost breakdown in itemized format.
275279//
276280//nolint:maintidx,revive // acceptable complexity/length for comprehensive display function
277- func printExtrapolatedResults (title string , days int , ext * cost.ExtrapolatedBreakdown , cfg cost.Config ) {
281+ func printExtrapolatedResults (title string , days int , ext * cost.ExtrapolatedBreakdown , cfg cost.Config , modelMergeTime time. Duration ) {
278282 fmt .Println ()
279283 fmt .Printf (" %s\n " , title )
280284 avgOpenTime := formatTimeUnit (ext .AvgPRDurationHours )
@@ -594,11 +598,11 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
594598 fmt .Println ()
595599
596600 // Print extrapolated efficiency score + annual waste
597- printExtrapolatedEfficiency (ext , days , cfg )
601+ printExtrapolatedEfficiency (ext , days , cfg , modelMergeTime )
598602}
599603
600604// printExtrapolatedEfficiency prints the workflow efficiency + annual waste section for extrapolated totals.
601- func printExtrapolatedEfficiency (ext * cost.ExtrapolatedBreakdown , days int , cfg cost.Config ) {
605+ func printExtrapolatedEfficiency (ext * cost.ExtrapolatedBreakdown , days int , cfg cost.Config , modelMergeTime time. Duration ) {
602606 // Calculate preventable waste: Code Churn + All Delay Costs + Automated Updates + PR Tracking
603607 preventableHours := ext .CodeChurnHours + ext .DeliveryDelayHours + ext .AutomatedUpdatesHours + ext .PRTrackingHours
604608 preventableCost := ext .CodeChurnCost + ext .DeliveryDelayCost + ext .AutomatedUpdatesCost + ext .PRTrackingCost
@@ -654,4 +658,81 @@ func printExtrapolatedEfficiency(ext *cost.ExtrapolatedBreakdown, days int, cfg
654658 fmt .Printf (" If Sustained for 1 Year: $%14s %.1f headcount\n " ,
655659 formatWithCommas (annualWasteCost ), headcount )
656660 fmt .Println ()
661+
662+ // 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 )
665+ }
666+ }
667+
668+ // 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 ()
671+
672+ // Calculate hourly rate
673+ hourlyRate := (cfg .AnnualSalary * cfg .BenefitsMultiplier ) / cfg .HoursPerYear
674+
675+ // Recalculate average preventable costs with target merge time
676+ // This mirrors the logic from ExtrapolateFromSamples but with target merge time
677+
678+ // Average delivery delay per PR at target merge time
679+ remodelDeliveryDelayPerPR := hourlyRate * cfg .DeliveryDelayFactor * targetHours
680+
681+ // Code churn: minimal for short PRs (< 1 day = ~0%)
682+ remodelCodeChurnPerPR := 0.0
683+
684+ // Automated updates: only for PRs open > 1 day
685+ remodelAutomatedUpdatesPerPR := 0.0
686+
687+ // PR tracking: scales with open time
688+ remodelPRTrackingPerPR := 0.0
689+ if targetHours >= 1.0 { // Only track PRs open >= 1 hour
690+ daysOpen := targetHours / 24.0
691+ remodelPRTrackingHours := (cfg .PRTrackingMinutesPerDay / 60.0 ) * daysOpen
692+ remodelPRTrackingPerPR = remodelPRTrackingHours * hourlyRate
693+ }
694+
695+ // Calculate total remodeled preventable cost for the period
696+ totalPRs := float64 (ext .TotalPRs )
697+ remodelPreventablePerPeriod := (remodelDeliveryDelayPerPR + remodelCodeChurnPerPR +
698+ remodelAutomatedUpdatesPerPR + remodelPRTrackingPerPR ) * totalPRs
699+
700+ // Current preventable cost for the period
701+ currentPreventablePerPeriod := ext .CodeChurnCost + ext .DeliveryDelayCost +
702+ ext .AutomatedUpdatesCost + ext .PRTrackingCost
703+
704+ // Calculate savings for the period
705+ savingsPerPeriod := currentPreventablePerPeriod - remodelPreventablePerPeriod
706+
707+ // Calculate efficiency improvement
708+ // Current efficiency: (total hours - preventable hours) / total hours
709+ // Modeled efficiency: (total hours - remodeled preventable hours) / total hours
710+ currentPreventableHours := ext .CodeChurnHours + ext .DeliveryDelayHours +
711+ ext .AutomatedUpdatesHours + ext .PRTrackingHours
712+ remodelPreventableHours := remodelPreventablePerPeriod / hourlyRate
713+
714+ var currentEfficiency , modeledEfficiency , efficiencyDelta float64
715+ if ext .TotalHours > 0 {
716+ currentEfficiency = 100.0 * (ext .TotalHours - currentPreventableHours ) / ext .TotalHours
717+ modeledEfficiency = 100.0 * (ext .TotalHours - remodelPreventableHours ) / ext .TotalHours
718+ efficiencyDelta = modeledEfficiency - currentEfficiency
719+ }
720+
721+ if savingsPerPeriod > 0 {
722+ // Annualize the savings
723+ weeksInPeriod := float64 (days ) / 7.0
724+ annualSavings := savingsPerPeriod * (52.0 / weeksInPeriod )
725+
726+ fmt .Println (" ┌─────────────────────────────────────────────────────────────┐" )
727+ fmt .Printf (" │ %-60s│\n " , "MERGE TIME MODELING" )
728+ fmt .Println (" └─────────────────────────────────────────────────────────────┘" )
729+ fmt .Printf (" If you lowered your average merge time to %s, you would save\n " , formatTimeUnit (targetHours ))
730+ fmt .Printf (" ~$%s/yr in engineering overhead" , formatWithCommas (annualSavings ))
731+ if efficiencyDelta > 0 {
732+ fmt .Printf (" (+%.1f%% throughput).\n " , efficiencyDelta )
733+ } else {
734+ fmt .Println ("." )
735+ }
736+ fmt .Println ()
737+ }
657738}
0 commit comments