Skip to content

Commit e780ff1

Browse files
committed
Tune CLI output, improve time/wk/author
1 parent bbc580b commit e780ff1

7 files changed

Lines changed: 246 additions & 139 deletions

File tree

cmd/prcost/main.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -447,23 +447,23 @@ func efficiencyGrade(efficiencyPct float64) (string, string) {
447447
case efficiencyPct >= 87:
448448
return "B+", "Pretty good!"
449449
case efficiencyPct >= 83:
450-
return "B", "Good work!"
450+
return "B", "Minor inefficiency detected."
451451
case efficiencyPct >= 80:
452-
return "B-", "Room for improvement."
452+
return "B-", "Minor inefficiency detected."
453453
case efficiencyPct >= 77:
454-
return "C+", "Some inefficiency."
454+
return "C+", "Some inefficiency detected."
455455
case efficiencyPct >= 73:
456-
return "C", "Needs work."
456+
return "C", "Moderate inefficiency detected."
457457
case efficiencyPct >= 70:
458-
return "C-", "Optimize your workflow."
458+
return "C-", "Moderate inefficiency detected."
459459
case efficiencyPct >= 67:
460-
return "D+", "Major inefficiency."
460+
return "D+", "Noticeable inefficiency detected."
461461
case efficiencyPct >= 63:
462-
return "D", "Critical issues."
462+
return "D", "Significant inefficiency detected."
463463
case efficiencyPct >= 60:
464-
return "D-", "Severe inefficiency."
464+
return "D-", "Severe inefficiency detected."
465465
default:
466-
return "F", "Rethink your workflow."
466+
return "F", "Critical inefficiency detected."
467467
}
468468
}
469469

@@ -494,14 +494,12 @@ func printEfficiency(breakdown *cost.Breakdown, formatCurrency func(float64) str
494494
grade, message := efficiencyGrade(efficiencyPct)
495495

496496
fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
497-
fmt.Printf(" │ WORKFLOW EFFICIENCY: %s (%.1f%%) - %s", grade, efficiencyPct, message)
498-
// Calculate padding to reach 63 chars (box width)
499-
headerLen := 25 + len(grade) + len(fmt.Sprintf("%.1f", efficiencyPct)) + len(message)
500-
padding := 63 - headerLen
497+
headerText := fmt.Sprintf("WORKFLOW EFFICIENCY: %s (%.1f%%) - %s", grade, efficiencyPct, message)
498+
padding := 60 - len(headerText)
501499
if padding < 0 {
502500
padding = 0
503501
}
504-
fmt.Printf("%*s│\n", padding, "")
502+
fmt.Printf(" │ %s%*s│\n", headerText, padding, "")
505503
fmt.Println(" └─────────────────────────────────────────────────────────────┘")
506504
fmt.Printf(" Preventable Waste: $%12s %s\n",
507505
formatWithCommas(preventableCost), formatTimeUnit(preventableHours))

cmd/prcost/repository.go

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
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
}

internal/server/server.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,7 +1251,7 @@ func (s *Server) processRepoSample(ctx context.Context, req *RepoSampleRequest,
12511251
}
12521252

12531253
// Extrapolate costs from samples
1254-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs))
1254+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), req.Days, cfg)
12551255

12561256
return &SampleResponse{
12571257
Extrapolated: extrapolated,
@@ -1337,7 +1337,7 @@ func (s *Server) processOrgSample(ctx context.Context, req *OrgSampleRequest, to
13371337
}
13381338

13391339
// Extrapolate costs from samples
1340-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs))
1340+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), req.Days, cfg)
13411341

13421342
return &SampleResponse{
13431343
Extrapolated: extrapolated,
@@ -1693,7 +1693,7 @@ func (s *Server) processRepoSampleWithProgress(ctx context.Context, req *RepoSam
16931693
}
16941694

16951695
// Extrapolate costs from samples
1696-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs))
1696+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), req.Days, cfg)
16971697

16981698
// Send final result
16991699
logSSEError(ctx, s.logger, sendSSE(writer, ProgressUpdate{
@@ -1803,7 +1803,7 @@ func (s *Server) processOrgSampleWithProgress(ctx context.Context, req *OrgSampl
18031803
}
18041804

18051805
// Extrapolate costs from samples
1806-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs))
1806+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), req.Days, cfg)
18071807

18081808
// Send final result
18091809
logSSEError(ctx, s.logger, sendSSE(writer, ProgressUpdate{

0 commit comments

Comments
 (0)