55 "errors"
66 "fmt"
77 "log/slog"
8- "strconv"
98 "time"
109
1110 "github.com/codeGROOVE-dev/prcost/pkg/cost"
@@ -81,10 +80,10 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
8180 }
8281
8382 // Extrapolate costs from samples using library function
84- extrapolated := cost .ExtrapolateFromSamples (breakdowns , len (prs ))
83+ extrapolated := cost .ExtrapolateFromSamples (breakdowns , len (prs ), days , cfg )
8584
8685 // Display results in itemized format
87- printExtrapolatedResults (fmt .Sprintf ("%s/%s" , owner , repo ), days , & extrapolated )
86+ printExtrapolatedResults (fmt .Sprintf ("%s/%s" , owner , repo ), days , & extrapolated , cfg )
8887
8988 return nil
9089}
@@ -158,10 +157,10 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,
158157 }
159158
160159 // Extrapolate costs from samples using library function
161- extrapolated := cost .ExtrapolateFromSamples (breakdowns , len (prs ))
160+ extrapolated := cost .ExtrapolateFromSamples (breakdowns , len (prs ), days , cfg )
162161
163162 // Display results in itemized format
164- printExtrapolatedResults (fmt .Sprintf ("%s (organization)" , org ), days , & extrapolated )
163+ printExtrapolatedResults (fmt .Sprintf ("%s (organization)" , org ), days , & extrapolated , cfg )
165164
166165 return nil
167166}
@@ -205,45 +204,14 @@ func formatTimeUnit(hours float64) string {
205204 return fmt .Sprintf ("%.1f years" , years )
206205}
207206
208- // formatEngTimeUnit formats time for annual metrics.
209- // Uses standard time units (months, years) for large durations.
210- func formatEngTimeUnit (hours float64 ) string {
211- // Show minutes for values less than 1 hour
212- if hours < 1.0 {
213- minutes := hours * 60.0
214- return fmt .Sprintf ("%.1f min" , minutes )
215- }
216-
217- if hours < 48 {
218- return fmt .Sprintf ("%.1f hrs" , hours )
219- }
220-
221- days := hours / 24.0
222- if days < 14 {
223- return fmt .Sprintf ("%.1f days" , days )
224- }
225-
226- weeks := days / 7.0
227- if weeks < 8 {
228- return fmt .Sprintf ("%.1f weeks" , weeks )
229- }
230-
231- months := days / 30.0
232- if months < 24 {
233- return fmt .Sprintf ("%.1f months" , months )
234- }
235-
236- years := days / 365.0
237- return fmt .Sprintf ("%.1f years" , years )
238- }
239-
240207// printExtrapolatedResults displays extrapolated cost breakdown in itemized format.
241208//
242209//nolint:maintidx,revive // acceptable complexity/length for comprehensive display function
243- func printExtrapolatedResults (title string , days int , ext * cost.ExtrapolatedBreakdown ) {
210+ func printExtrapolatedResults (title string , days int , ext * cost.ExtrapolatedBreakdown , cfg cost. Config ) {
244211 fmt .Println ()
245212 fmt .Printf (" %s\n " , title )
246- fmt .Printf (" Period: Last %d days • Total PRs: %d • Sampled: %d\n " , days , ext .TotalPRs , ext .SuccessfulSamples )
213+ avgOpenTime := formatTimeUnit (ext .AvgPRDurationHours )
214+ fmt .Printf (" Period: Last %d days • Total PRs: %d • Sampled: %d • Avg Open Time: %s\n " , days , ext .TotalPRs , ext .SuccessfulSamples , avgOpenTime )
247215 fmt .Println ()
248216
249217 // Calculate average per PR
@@ -279,7 +247,9 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
279247
280248 // Show average PR breakdown with improved visual hierarchy
281249 fmt .Println (" ┌─────────────────────────────────────────────────────────────┐" )
282- fmt .Printf (" │ Average PR (sampled over %d day period)%*s│\n " , days , 51 - len (strconv .Itoa (days )), "" )
250+ headerText := fmt .Sprintf ("Average PR (sampled over %d day period)" , days )
251+ padding := 60 - len (headerText )
252+ fmt .Printf (" │ %s%*s│\n " , headerText , padding , "" )
283253 fmt .Println (" └─────────────────────────────────────────────────────────────┘" )
284254 fmt .Println ()
285255
@@ -378,6 +348,13 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
378348 fmt .Println ()
379349 }
380350
351+ // Per-author preventable loss for average PR (if we have authors)
352+ if ext .UniqueAuthors > 0 && ext .AvgWasteHoursPerAuthorPerWeek > 0 {
353+ avgWeeklyCostPerAuthor := ext .AvgWasteCostPerAuthorPerYear / 52.0
354+ fmt .Printf (" Per Author per Week $%12s %s\n " ,
355+ formatWithCommas (avgWeeklyCostPerAuthor ), formatTimeUnit (ext .AvgWasteHoursPerAuthorPerWeek ))
356+ }
357+
381358 // Average Preventable Loss Total (before grand total)
382359 avgPreventableCost := avgCodeChurnCost + avgDeliveryDelayCost + avgCoordinationCost
383360 avgPreventableHours := avgCodeChurnHours + avgDeliveryDelayHours + avgCoordinationHours
@@ -394,8 +371,8 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
394371
395372 // Extrapolated total section with improved visual hierarchy
396373 fmt .Println (" ┌─────────────────────────────────────────────────────────────┐" )
397- headerText : = fmt .Sprintf ("Estimated costs within a %d day period (extrapolated)" , days )
398- padding := 61 - len (headerText )
374+ headerText = fmt .Sprintf ("Estimated costs within a %d day period (extrapolated)" , days )
375+ padding = 60 - len (headerText )
399376 fmt .Printf (" │ %s%*s│\n " , headerText , padding , "" )
400377 fmt .Println (" └─────────────────────────────────────────────────────────────┘" )
401378 fmt .Println ()
@@ -488,6 +465,13 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
488465 fmt .Println ()
489466 }
490467
468+ // Per-author preventable loss (if we have authors)
469+ if ext .UniqueAuthors > 0 && ext .AvgWasteHoursPerAuthorPerWeek > 0 {
470+ weeklyCostPerAuthor := ext .AvgWasteCostPerAuthorPerYear / 52.0
471+ fmt .Printf (" Per Author per Week $%12s %s\n " ,
472+ formatWithCommas (weeklyCostPerAuthor ), formatTimeUnit (ext .AvgWasteHoursPerAuthorPerWeek ))
473+ }
474+
491475 // Preventable Loss Total (before grand total)
492476 preventableCost := ext .CodeChurnCost + ext .DeliveryDelayCost + ext .CoordinationCost
493477 preventableHours := ext .CodeChurnHours + ext .DeliveryDelayHours + ext .CoordinationHours
@@ -502,11 +486,11 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
502486 fmt .Println ()
503487
504488 // Print extrapolated efficiency score + annual waste
505- printExtrapolatedEfficiency (ext , days )
489+ printExtrapolatedEfficiency (ext , days , cfg )
506490}
507491
508492// printExtrapolatedEfficiency prints the workflow efficiency + annual waste section for extrapolated totals.
509- func printExtrapolatedEfficiency (ext * cost.ExtrapolatedBreakdown , days int ) {
493+ func printExtrapolatedEfficiency (ext * cost.ExtrapolatedBreakdown , days int , cfg cost. Config ) {
510494 // Calculate preventable waste: Code Churn + All Delay Costs
511495 preventableHours := ext .CodeChurnHours + ext .DeliveryDelayHours + ext .CoordinationHours
512496 preventableCost := ext .CodeChurnCost + ext .DeliveryDelayCost + ext .CoordinationCost
@@ -523,22 +507,28 @@ func printExtrapolatedEfficiency(ext *cost.ExtrapolatedBreakdown, days int) {
523507
524508 // Calculate annual waste
525509 annualMultiplier := 365.0 / float64 (days )
526- annualWasteHours := preventableHours * annualMultiplier
527510 annualWasteCost := preventableCost * annualMultiplier
528511
529512 fmt .Println (" ┌─────────────────────────────────────────────────────────────┐" )
530- fmt .Printf (" │ WORKFLOW EFFICIENCY: %s (%.1f%%) - %s" , grade , efficiencyPct , message )
531- // Calculate padding to reach 63 chars (box width)
532- headerLen := 25 + len (grade ) + len (fmt .Sprintf ("%.1f" , efficiencyPct )) + len (message )
533- padding := 63 - headerLen
513+ headerText := fmt .Sprintf ("WORKFLOW EFFICIENCY: %s (%.1f%%) - %s" , grade , efficiencyPct , message )
514+ padding := 60 - len (headerText )
534515 if padding < 0 {
535516 padding = 0
536517 }
537- fmt .Printf ("% *s│\n " , padding , "" )
518+ fmt .Printf (" │ %s% *s│\n " , headerText , padding , "" )
538519 fmt .Println (" └─────────────────────────────────────────────────────────────┘" )
539- fmt .Printf (" Preventable Waste: $%12s %s\n " ,
540- formatWithCommas (preventableCost ), formatTimeUnit (preventableHours ))
541- fmt .Printf (" Annual Wasted Effort: $%12s %s\n " ,
542- formatWithCommas (annualWasteCost ), formatEngTimeUnit (annualWasteHours ))
520+
521+ // Per-author preventable waste (if we have authors)
522+ if ext .UniqueAuthors > 0 && ext .AvgWasteHoursPerAuthorPerWeek > 0 {
523+ weeklyCostPerAuthor := ext .AvgWasteCostPerAuthorPerYear / 52.0
524+ fmt .Printf (" Lost Time per Author per Week: $%12s %s\n " ,
525+ formatWithCommas (weeklyCostPerAuthor ), formatTimeUnit (ext .AvgWasteHoursPerAuthorPerWeek ))
526+ }
527+
528+ // Calculate headcount from annual waste
529+ annualCostPerHead := cfg .AnnualSalary * cfg .BenefitsMultiplier
530+ headcount := annualWasteCost / annualCostPerHead
531+ fmt .Printf (" If Sustained for 1 Year: $%12s %.1f headcount\n " ,
532+ formatWithCommas (annualWasteCost ), headcount )
543533 fmt .Println ()
544534}
0 commit comments