Skip to content

Commit 6d94ed2

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
Add merge rate stats
1 parent c0c2d00 commit 6d94ed2

File tree

10 files changed

+256
-20
lines changed

10 files changed

+256
-20
lines changed

cmd/prcost/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,23 @@ func mergeVelocityGrade(avgOpenDays float64) (grade, message string) {
530530
}
531531
}
532532

533+
// mergeRateGrade returns a grade based on merge success rate percentage.
534+
// A: >90%, B: >80%, C: >70%, D: >60%, F: ≤60%.
535+
func mergeRateGrade(mergeRatePct float64) (grade, message string) {
536+
switch {
537+
case mergeRatePct > 90:
538+
return "A", "Excellent"
539+
case mergeRatePct > 80:
540+
return "B", "Good"
541+
case mergeRatePct > 70:
542+
return "C", "Acceptable"
543+
case mergeRatePct > 60:
544+
return "D", "Low"
545+
default:
546+
return "F", "Poor"
547+
}
548+
}
549+
533550
// printMergeTimeModelingCallout prints a callout showing potential savings from reduced merge time.
534551
func printMergeTimeModelingCallout(breakdown *cost.Breakdown, cfg cost.Config) {
535552
targetHours := cfg.TargetMergeTimeHours

cmd/prcost/repository.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,17 @@ func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days
9999
openPRCount = 0
100100
}
101101

102+
// Convert PRSummary to PRMergeStatus for merge rate calculation
103+
prStatuses := make([]cost.PRMergeStatus, len(prs))
104+
for i, pr := range prs {
105+
prStatuses[i] = cost.PRMergeStatus{
106+
Merged: pr.Merged,
107+
State: pr.State,
108+
}
109+
}
110+
102111
// Extrapolate costs from samples using library function
103-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, openPRCount, actualDays, cfg)
112+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, openPRCount, actualDays, cfg, prStatuses)
104113

105114
// Display results in itemized format
106115
printExtrapolatedResults(fmt.Sprintf("%s/%s", owner, repo), actualDays, &extrapolated, cfg)
@@ -199,8 +208,17 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,
199208
}
200209
slog.Info("Counted total open PRs across organization", "org", org, "open_prs", totalOpenPRs)
201210

211+
// Convert PRSummary to PRMergeStatus for merge rate calculation
212+
prStatuses := make([]cost.PRMergeStatus, len(prs))
213+
for i, pr := range prs {
214+
prStatuses[i] = cost.PRMergeStatus{
215+
Merged: pr.Merged,
216+
State: pr.State,
217+
}
218+
}
219+
202220
// Extrapolate costs from samples using library function
203-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg)
221+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg, prStatuses)
204222

205223
// Display results in itemized format
206224
printExtrapolatedResults(fmt.Sprintf("%s (organization)", org), actualDays, &extrapolated, cfg)
@@ -656,6 +674,18 @@ func printExtrapolatedEfficiency(ext *cost.ExtrapolatedBreakdown, days int, cfg
656674
fmt.Printf(" │ %-60s│\n", velocityHeader)
657675
fmt.Println(" └─────────────────────────────────────────────────────────────┘")
658676

677+
// Merge Rate box (if data available)
678+
if ext.MergedPRs+ext.UnmergedPRs > 0 {
679+
mergeRateGradeStr, mergeRateMessage := mergeRateGrade(ext.MergeRate)
680+
fmt.Println(" ┌─────────────────────────────────────────────────────────────┐")
681+
mergeRateHeader := fmt.Sprintf("MERGE RATE: %s (%.1f%%) - %s", mergeRateGradeStr, ext.MergeRate, mergeRateMessage)
682+
if len(mergeRateHeader) > innerWidth {
683+
mergeRateHeader = mergeRateHeader[:innerWidth]
684+
}
685+
fmt.Printf(" │ %-60s│\n", mergeRateHeader)
686+
fmt.Println(" └─────────────────────────────────────────────────────────────┘")
687+
}
688+
659689
// Weekly waste per PR author
660690
if ext.WasteHoursPerAuthorPerWeek > 0 && ext.TotalAuthors > 0 {
661691
fmt.Printf(" Weekly waste per PR author: $%14s %s (%d authors)\n",

internal/server/server.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,8 +1659,17 @@ func (s *Server) processRepoSample(ctx context.Context, req *RepoSampleRequest,
16591659
openPRCount = 0
16601660
}
16611661

1662+
// Convert PRSummary to PRMergeStatus for merge rate calculation
1663+
prStatuses := make([]cost.PRMergeStatus, len(prs))
1664+
for i, pr := range prs {
1665+
prStatuses[i] = cost.PRMergeStatus{
1666+
Merged: pr.Merged,
1667+
State: pr.State,
1668+
}
1669+
}
1670+
16621671
// Extrapolate costs from samples
1663-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, openPRCount, actualDays, cfg)
1672+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, openPRCount, actualDays, cfg, prStatuses)
16641673

16651674
// Only include seconds_in_state if we have data (turnserver only)
16661675
var secondsInState map[string]int
@@ -1779,8 +1788,17 @@ func (s *Server) processOrgSample(ctx context.Context, req *OrgSampleRequest, to
17791788
}
17801789
s.logger.InfoContext(ctx, "Counted total open PRs across organization", "org", req.Org, "open_prs", totalOpenPRs)
17811790

1791+
// Convert PRSummary to PRMergeStatus for merge rate calculation
1792+
prStatuses := make([]cost.PRMergeStatus, len(prs))
1793+
for i, pr := range prs {
1794+
prStatuses[i] = cost.PRMergeStatus{
1795+
Merged: pr.Merged,
1796+
State: pr.State,
1797+
}
1798+
}
1799+
17821800
// Extrapolate costs from samples
1783-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg)
1801+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg, prStatuses)
17841802

17851803
// Only include seconds_in_state if we have data (turnserver only)
17861804
var secondsInState map[string]int
@@ -2176,8 +2194,17 @@ func (s *Server) processRepoSampleWithProgress(ctx context.Context, req *RepoSam
21762194
openPRCount = 0
21772195
}
21782196

2197+
// Convert PRSummary to PRMergeStatus for merge rate calculation
2198+
prStatuses := make([]cost.PRMergeStatus, len(prs))
2199+
for i, pr := range prs {
2200+
prStatuses[i] = cost.PRMergeStatus{
2201+
Merged: pr.Merged,
2202+
State: pr.State,
2203+
}
2204+
}
2205+
21792206
// Extrapolate costs from samples
2180-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, openPRCount, actualDays, cfg)
2207+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, openPRCount, actualDays, cfg, prStatuses)
21812208

21822209
// Only include seconds_in_state if we have data (turnserver only)
21832210
var secondsInState map[string]int
@@ -2326,8 +2353,17 @@ func (s *Server) processOrgSampleWithProgress(ctx context.Context, req *OrgSampl
23262353
}
23272354
s.logger.InfoContext(ctx, "Counted total open PRs across organization", "open_prs", totalOpenPRs, "org", req.Org)
23282355

2356+
// Convert PRSummary to PRMergeStatus for merge rate calculation
2357+
prStatuses := make([]cost.PRMergeStatus, len(prs))
2358+
for i, pr := range prs {
2359+
prStatuses[i] = cost.PRMergeStatus{
2360+
Merged: pr.Merged,
2361+
State: pr.State,
2362+
}
2363+
}
2364+
23292365
// Extrapolate costs from samples
2330-
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg)
2366+
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg, prStatuses)
23312367

23322368
// Only include seconds_in_state if we have data (turnserver only)
23332369
var secondsInState map[string]int

internal/server/static/formatR2RCallout.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function formatR2RCallout(avgOpenHours, r2rSavings, currentEfficiency, modeledEf
2121
let targetText = targetMergeHours.toFixed(1) + 'h';
2222

2323
let html = '<div style="margin: 24px 0; padding: 12px 20px; background: linear-gradient(135deg, #e6f9f0 0%, #ffffff 100%); border: 1px solid #00c853; border-radius: 8px; font-size: 16px; color: #1d1d1f; line-height: 1.6; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Helvetica, Arial, sans-serif, \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Noto Color Emoji\';">';
24-
html += '💡 <strong>Pro-Tip:</strong> Boost team throughput by <strong>' + efficiencyDelta.toFixed(1) + '%</strong> and save <strong>' + savingsText + '/yr</strong> by reducing merge times to &lt;' + targetText + ' with ';
24+
html += '<span style="font-family: \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Noto Color Emoji\', sans-serif; font-style: normal; font-weight: normal; text-rendering: optimizeLegibility;">\uD83D\uDCA1</span> <strong>Pro-Tip:</strong> Boost team throughput by <strong>' + efficiencyDelta.toFixed(1) + '%</strong> and save <strong>' + savingsText + '/yr</strong> by reducing merge times to &lt;' + targetText + ' with ';
2525
html += '<a href="https://codegroove.dev/products/ready-to-review/" target="_blank" rel="noopener" style="color: #00c853; font-weight: 600; text-decoration: none;">Ready to Review</a>. ';
2626
html += 'Free for open-source repositories, $6/user/org for private repos.';
2727
html += '</div>';

internal/server/static/index.html

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,7 +1410,21 @@ <h3>Why calculate PR costs?</h3>
14101410
}
14111411
}
14121412

1413-
function formatEfficiencyHTML(efficiencyPct, grade, message, preventableCost, preventableHours, totalCost, totalHours, avgOpenHours, isAnnual = false, annualWasteCost = 0, annualWasteHours = 0, wasteHoursPerWeek = 0, wasteCostPerWeek = 0, wasteHoursPerAuthorPerWeek = 0, wasteCostPerAuthorPerWeek = 0, totalAuthors = 0, salary = 250000, benefitsMultiplier = 1.2, analysisType = 'project', sourceName = '') {
1413+
function mergeRateGrade(mergeRatePct) {
1414+
if (mergeRatePct > 90) {
1415+
return { grade: 'A', message: 'Excellent success rate' };
1416+
} else if (mergeRatePct > 80) {
1417+
return { grade: 'B', message: 'Good success rate' };
1418+
} else if (mergeRatePct > 70) {
1419+
return { grade: 'C', message: 'Acceptable success rate' };
1420+
} else if (mergeRatePct > 60) {
1421+
return { grade: 'D', message: 'Low success rate' };
1422+
} else {
1423+
return { grade: 'F', message: 'Poor success rate' };
1424+
}
1425+
}
1426+
1427+
function formatEfficiencyHTML(efficiencyPct, grade, message, preventableCost, preventableHours, totalCost, totalHours, avgOpenHours, isAnnual = false, annualWasteCost = 0, annualWasteHours = 0, wasteHoursPerWeek = 0, wasteCostPerWeek = 0, wasteHoursPerAuthorPerWeek = 0, wasteCostPerAuthorPerWeek = 0, totalAuthors = 0, salary = 250000, benefitsMultiplier = 1.2, analysisType = 'project', sourceName = '', mergeRate = 0, mergedPRs = 0, unmergedPRs = 0) {
14141428
let html = '<div class="efficiency-section">';
14151429

14161430
// Development Efficiency box
@@ -1435,6 +1449,20 @@ <h3>Why calculate PR costs?</h3>
14351449
html += `<div class="efficiency-message">${velocityGradeObj.message}</div>`;
14361450
html += '</div>'; // Close efficiency-box
14371451

1452+
// Merge Rate box (if data available)
1453+
if (mergedPRs + unmergedPRs > 0) {
1454+
const mergeRateGradeObj = mergeRateGrade(mergeRate);
1455+
html += '<div class="efficiency-box">';
1456+
html += '<h3 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600; color: #1d1d1f;">Merge Rate</h3>';
1457+
html += '<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 4px;">';
1458+
html += `<span class="efficiency-grade grade-${mergeRateGradeObj.grade[0]}" style="display: inline-block; min-width: 38px; padding: 3px 7px; border-radius: 6px; font-size: 16px; font-weight: 700; text-align: center; background: rgba(0,0,0,0.06);">${mergeRateGradeObj.grade}</span>`;
1459+
html += `<span style="font-size: 28px; font-weight: 700; color: #1d1d1f;">${mergeRate.toFixed(1)}%</span>`;
1460+
html += '</div>';
1461+
html += `<div class="efficiency-message">${mergeRateGradeObj.message}</div>`;
1462+
html += '<div style="font-size: 11px; color: #86868b; margin-top: 4px;">Recently modified PRs successfully merged</div>';
1463+
html += '</div>'; // Close efficiency-box
1464+
}
1465+
14381466
// Annual Impact box (only if annual)
14391467
if (isAnnual && annualWasteCost > 0) {
14401468
html += '<div class="efficiency-box" style="background: linear-gradient(135deg, #fff9e6 0%, #ffffff 100%); border-left: 3px solid #ffcc00;">';
@@ -1474,7 +1502,7 @@ <h3>Why calculate PR costs?</h3>
14741502
let targetText = targetMergeHours.toFixed(1) + 'h';
14751503

14761504
let html = '<div style="margin: 24px 0; padding: 12px 20px; background: linear-gradient(135deg, #e6f9f0 0%, #ffffff 100%); border: 1px solid #00c853; border-radius: 8px; font-size: 16px; color: #1d1d1f; line-height: 1.6; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Helvetica, Arial, sans-serif, \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Noto Color Emoji\';">';
1477-
html += '💡 <strong>Pro-Tip:</strong> Boost team throughput by <strong>' + efficiencyDelta.toFixed(1) + '%</strong> and save <strong>' + savingsText + '/yr</strong> by reducing merge times to &lt;' + targetText + ' with ';
1505+
html += '<span style="font-family: \'Apple Color Emoji\', \'Segoe UI Emoji\', \'Noto Color Emoji\', sans-serif; font-style: normal; font-weight: normal; text-rendering: optimizeLegibility;">\uD83D\uDCA1</span> <strong>Pro-Tip:</strong> Boost team throughput by <strong>' + efficiencyDelta.toFixed(1) + '%</strong> and save <strong>' + savingsText + '/yr</strong> by reducing merge times to &lt;' + targetText + ' with ';
14781506
html += '<a href="https://codegroove.dev/products/ready-to-review/" target="_blank" rel="noopener" style="color: #00c853; font-weight: 600; text-decoration: none;">Ready to Review</a>. ';
14791507
html += 'Free for open-source repositories, $6/user/org for private repos.';
14801508
html += '</div>';
@@ -2370,7 +2398,10 @@ <h3>Why calculate PR costs?</h3>
23702398
const wasteCostPerAuthorPerWeek = e.waste_cost_per_author_per_week || 0;
23712399
const totalAuthors = e.total_authors || 0;
23722400
const avgPRDurationHours = e.avg_pr_duration_hours || 0;
2373-
html += formatEfficiencyHTML(extEfficiencyPct, extEfficiency.grade, extEfficiency.message, extPreventableCost, extPreventableHours, e.total_cost, e.total_hours, avgPRDurationHours, true, annualWasteCost, annualWasteHours, wasteHoursPerWeek, wasteCostPerWeek, wasteHoursPerAuthorPerWeek, wasteCostPerAuthorPerWeek, totalAuthors, salary, benefitsMultiplier, analysisType, sourceName);
2401+
const mergeRate = e.merge_rate || 0;
2402+
const mergedPRs = e.merged_prs || 0;
2403+
const unmergedPRs = e.unmerged_prs || 0;
2404+
html += formatEfficiencyHTML(extEfficiencyPct, extEfficiency.grade, extEfficiency.message, extPreventableCost, extPreventableHours, e.total_cost, e.total_hours, avgPRDurationHours, true, annualWasteCost, annualWasteHours, wasteHoursPerWeek, wasteCostPerWeek, wasteHoursPerAuthorPerWeek, wasteCostPerAuthorPerWeek, totalAuthors, salary, benefitsMultiplier, analysisType, sourceName, mergeRate, mergedPRs, unmergedPRs);
23742405

23752406
// Add R2R callout if enabled, otherwise generic merge time callout
23762407
// Calculate modeled efficiency (with 1.5h merge time)

pkg/cost/cost.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ type PRData struct {
158158
LinesAdded int
159159
LinesDeleted int
160160
AuthorBot bool
161+
Merged bool // Whether the PR was merged
162+
State string // PR state: "open", "closed"
161163
}
162164

163165
// AuthorCostDetail breaks down the author's costs.

pkg/cost/cost_test.go

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,7 +1261,7 @@ func TestAnalyzePRsContextCancellation(t *testing.T) {
12611261

12621262
func TestExtrapolateFromSamplesEmpty(t *testing.T) {
12631263
cfg := DefaultConfig()
1264-
result := ExtrapolateFromSamples([]Breakdown{}, 100, 10, 5, 30, cfg)
1264+
result := ExtrapolateFromSamples([]Breakdown{}, 100, 10, 5, 30, cfg, []PRMergeStatus{})
12651265

12661266
if result.TotalPRs != 100 {
12671267
t.Errorf("Expected TotalPRs=100, got %d", result.TotalPRs)
@@ -1297,7 +1297,13 @@ func TestExtrapolateFromSamplesSingle(t *testing.T) {
12971297
}, cfg)
12981298

12991299
// Extrapolate from 1 sample to 10 total PRs
1300-
result := ExtrapolateFromSamples([]Breakdown{breakdown}, 10, 2, 0, 7, cfg)
1300+
// Create merge status for 10 PRs: 9 merged, 1 open
1301+
prStatuses := make([]PRMergeStatus, 10)
1302+
for i := 0; i < 9; i++ {
1303+
prStatuses[i] = PRMergeStatus{Merged: true, State: "MERGED"}
1304+
}
1305+
prStatuses[9] = PRMergeStatus{Merged: false, State: "OPEN"}
1306+
result := ExtrapolateFromSamples([]Breakdown{breakdown}, 10, 2, 0, 7, cfg, prStatuses)
13011307

13021308
if result.TotalPRs != 10 {
13031309
t.Errorf("Expected TotalPRs=10, got %d", result.TotalPRs)
@@ -1361,7 +1367,15 @@ func TestExtrapolateFromSamplesMultiple(t *testing.T) {
13611367
}
13621368

13631369
// Extrapolate from 2 samples to 20 total PRs over 14 days
1364-
result := ExtrapolateFromSamples(breakdowns, 20, 5, 3, 14, cfg)
1370+
// Create merge status for 20 PRs: 17 merged, 3 open
1371+
prStatuses := make([]PRMergeStatus, 20)
1372+
for i := 0; i < 17; i++ {
1373+
prStatuses[i] = PRMergeStatus{Merged: true, State: "MERGED"}
1374+
}
1375+
for i := 17; i < 20; i++ {
1376+
prStatuses[i] = PRMergeStatus{Merged: false, State: "OPEN"}
1377+
}
1378+
result := ExtrapolateFromSamples(breakdowns, 20, 5, 3, 14, cfg, prStatuses)
13651379

13661380
if result.TotalPRs != 20 {
13671381
t.Errorf("Expected TotalPRs=20, got %d", result.TotalPRs)
@@ -1431,7 +1445,12 @@ func TestExtrapolateFromSamplesBotVsHuman(t *testing.T) {
14311445
},
14321446
}
14331447

1434-
result := ExtrapolateFromSamples(breakdowns, 10, 5, 0, 7, cfg)
1448+
// Create merge status for 10 PRs: all merged
1449+
prStatuses := make([]PRMergeStatus, 10)
1450+
for i := 0; i < 10; i++ {
1451+
prStatuses[i] = PRMergeStatus{Merged: true, State: "MERGED"}
1452+
}
1453+
result := ExtrapolateFromSamples(breakdowns, 10, 5, 0, 7, cfg, prStatuses)
14351454

14361455
// Should have both human and bot PR counts
14371456
if result.HumanPRs <= 0 {
@@ -1482,7 +1501,12 @@ func TestExtrapolateFromSamplesWasteCalculation(t *testing.T) {
14821501
}, cfg)
14831502

14841503
// Extrapolate over 7 days
1485-
result := ExtrapolateFromSamples([]Breakdown{breakdown}, 10, 3, 0, 7, cfg)
1504+
// Create merge status for 10 PRs: all merged
1505+
prStatuses := make([]PRMergeStatus, 10)
1506+
for i := 0; i < 10; i++ {
1507+
prStatuses[i] = PRMergeStatus{Merged: true, State: "MERGED"}
1508+
}
1509+
result := ExtrapolateFromSamples([]Breakdown{breakdown}, 10, 3, 0, 7, cfg, prStatuses)
14861510

14871511
// Waste per week should be calculated
14881512
if result.WasteHoursPerWeek <= 0 {
@@ -1527,7 +1551,15 @@ func TestExtrapolateFromSamplesR2RSavings(t *testing.T) {
15271551
}, cfg),
15281552
}
15291553

1530-
result := ExtrapolateFromSamples(breakdowns, 100, 10, 5, 30, cfg)
1554+
// Create merge status for 100 PRs: 95 merged, 5 open
1555+
prStatuses := make([]PRMergeStatus, 100)
1556+
for i := 0; i < 95; i++ {
1557+
prStatuses[i] = PRMergeStatus{Merged: true, State: "MERGED"}
1558+
}
1559+
for i := 95; i < 100; i++ {
1560+
prStatuses[i] = PRMergeStatus{Merged: false, State: "OPEN"}
1561+
}
1562+
result := ExtrapolateFromSamples(breakdowns, 100, 10, 5, 30, cfg, prStatuses)
15311563

15321564
// R2R savings should be calculated
15331565
// Savings formula: baseline waste - remodeled waste - subscription cost
@@ -1564,7 +1596,15 @@ func TestExtrapolateFromSamplesOpenPRTracking(t *testing.T) {
15641596

15651597
// Test with actual open PRs
15661598
actualOpenPRs := 15
1567-
result := ExtrapolateFromSamples([]Breakdown{breakdown}, 100, 5, actualOpenPRs, 30, cfg)
1599+
// Create merge status for 100 PRs: 85 merged, 15 open
1600+
prStatuses := make([]PRMergeStatus, 100)
1601+
for i := 0; i < 85; i++ {
1602+
prStatuses[i] = PRMergeStatus{Merged: true, State: "MERGED"}
1603+
}
1604+
for i := 85; i < 100; i++ {
1605+
prStatuses[i] = PRMergeStatus{Merged: false, State: "OPEN"}
1606+
}
1607+
result := ExtrapolateFromSamples([]Breakdown{breakdown}, 100, 5, actualOpenPRs, 30, cfg, prStatuses)
15681608

15691609
// Open PRs should match actual count (not extrapolated)
15701610
if result.OpenPRs != actualOpenPRs {
@@ -1600,7 +1640,12 @@ func TestExtrapolateFromSamplesParticipants(t *testing.T) {
16001640
ClosedAt: now,
16011641
}, cfg)
16021642

1603-
result := ExtrapolateFromSamples([]Breakdown{breakdown}, 10, 5, 0, 7, cfg)
1643+
// Create merge status for 10 PRs: all merged
1644+
prStatuses := make([]PRMergeStatus, 10)
1645+
for i := 0; i < 10; i++ {
1646+
prStatuses[i] = PRMergeStatus{Merged: true, State: "MERGED"}
1647+
}
1648+
result := ExtrapolateFromSamples([]Breakdown{breakdown}, 10, 5, 0, 7, cfg, prStatuses)
16041649

16051650
// Participant costs should be extrapolated
16061651
if result.ParticipantReviewCost <= 0 {

0 commit comments

Comments
 (0)