diff --git a/cmd/prcost/main.go b/cmd/prcost/main.go
index 6f08d93..3a2c7b9 100644
--- a/cmd/prcost/main.go
+++ b/cmd/prcost/main.go
@@ -428,6 +428,13 @@ func formatWithCommas(amount float64) string {
// formatLOC formats lines of code in kilo format with appropriate precision and commas for large values.
func formatLOC(kloc float64) string {
+ loc := kloc * 1000 // Convert to actual lines
+
+ // For values < 1k LOC, just show LOC count without 'k' suffix
+ if loc < 1000 {
+ return fmt.Sprintf("%d LOC", int(loc))
+ }
+
// For values >= 100k, add commas (e.g., "1,517k" instead of "1517k")
if kloc >= 100.0 {
intPart := int(kloc)
diff --git a/cmd/prcost/repository.go b/cmd/prcost/repository.go
index 59c98aa..b8f076a 100644
--- a/cmd/prcost/repository.go
+++ b/cmd/prcost/repository.go
@@ -60,7 +60,6 @@ func isBotAuthor(author string) bool {
// Uses library functions from pkg/github and pkg/cost for fetching, sampling,
// and extrapolation - all functionality is available to external clients.
func analyzeRepository(ctx context.Context, owner, repo string, sampleSize, days int, cfg cost.Config, token string, dataSource string) error {
-
// Calculate since date
since := time.Now().AddDate(0, 0, -days)
@@ -283,28 +282,13 @@ func analyzeOrganization(ctx context.Context, org string, sampleSize, days int,
// Count unique authors across all PRs (not just samples)
totalAuthors := github.CountUniqueAuthors(prs)
- // Count open PRs across all unique repos in the organization
- uniqueRepos := make(map[string]bool)
- for _, pr := range prs {
- repoKey := pr.Owner + "/" + pr.Repo
- uniqueRepos[repoKey] = true
- }
-
- totalOpenPRs := 0
- for repoKey := range uniqueRepos {
- parts := strings.SplitN(repoKey, "/", 2)
- if len(parts) != 2 {
- continue
- }
- owner, repo := parts[0], parts[1]
- openCount, err := github.CountOpenPRsInRepo(ctx, owner, repo, token)
- if err != nil {
- slog.Warn("Failed to count open PRs for repo", "repo", repoKey, "error", err)
- continue
- }
- totalOpenPRs += openCount
+ // Count open PRs across the entire organization with a single query
+ totalOpenPRs, err := github.CountOpenPRsInOrg(ctx, org, token)
+ if err != nil {
+ slog.Warn("Failed to count open PRs in organization, using 0", "error", err)
+ totalOpenPRs = 0
}
- slog.Info("Counted total open PRs across organization", "open_prs", totalOpenPRs, "repos", len(uniqueRepos))
+ slog.Info("Counted total open PRs across organization", "org", org, "open_prs", totalOpenPRs)
// Extrapolate costs from samples using library function
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg)
@@ -433,14 +417,18 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
fmt.Printf(" Development Costs (%d PRs, %s total LOC)\n", ext.HumanPRs, totalLOCStr)
fmt.Println(" ────────────────────────────────────────")
+ // Calculate average events and sessions
+ avgAuthorEvents := float64(ext.AuthorEvents) / float64(ext.TotalPRs)
+ avgAuthorSessions := float64(ext.AuthorSessions) / float64(ext.TotalPRs)
+
fmt.Printf(" New Development $%10s %s (%s LOC)\n",
formatWithCommas(avgAuthorNewCodeCost), formatTimeUnit(avgAuthorNewCodeHours), newLOCStr)
fmt.Printf(" Adaptation $%10s %s (%s LOC)\n",
formatWithCommas(avgAuthorAdaptationCost), formatTimeUnit(avgAuthorAdaptationHours), modifiedLOCStr)
- fmt.Printf(" GitHub Activity $%10s %s\n",
- formatWithCommas(avgAuthorGitHubCost), formatTimeUnit(avgAuthorGitHubHours))
- fmt.Printf(" Context Switching $%10s %s\n",
- formatWithCommas(avgAuthorGitHubContextCost), formatTimeUnit(avgAuthorGitHubContextHours))
+ fmt.Printf(" GitHub Activity $%10s %s (%.1f events)\n",
+ formatWithCommas(avgAuthorGitHubCost), formatTimeUnit(avgAuthorGitHubHours), avgAuthorEvents)
+ fmt.Printf(" Context Switching $%10s %s (%.1f sessions)\n",
+ formatWithCommas(avgAuthorGitHubContextCost), formatTimeUnit(avgAuthorGitHubContextHours), avgAuthorSessions)
// Show bot PR LOC even though cost is $0
if ext.BotPRs > 0 {
@@ -457,18 +445,23 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
// Participants section (if any participants)
if ext.ParticipantTotalCost > 0 {
+ avgParticipantEvents := float64(ext.ParticipantEvents) / float64(ext.TotalPRs)
+ avgParticipantSessions := float64(ext.ParticipantSessions) / float64(ext.TotalPRs)
+
+ avgParticipantReviews := float64(ext.ParticipantReviews) / float64(ext.TotalPRs)
+
fmt.Println(" Participant Costs")
fmt.Println(" ─────────────────")
if avgParticipantReviewCost > 0 {
- fmt.Printf(" Review Activity $%10s %s\n",
- formatWithCommas(avgParticipantReviewCost), formatTimeUnit(avgParticipantReviewHours))
+ fmt.Printf(" Review Activity $%10s %s (%.1f reviews)\n",
+ formatWithCommas(avgParticipantReviewCost), formatTimeUnit(avgParticipantReviewHours), avgParticipantReviews)
}
if avgParticipantGitHubCost > 0 {
- fmt.Printf(" GitHub Activity $%10s %s\n",
- formatWithCommas(avgParticipantGitHubCost), formatTimeUnit(avgParticipantGitHubHours))
+ fmt.Printf(" GitHub Activity $%10s %s (%.1f events)\n",
+ formatWithCommas(avgParticipantGitHubCost), formatTimeUnit(avgParticipantGitHubHours), avgParticipantEvents)
}
- fmt.Printf(" Context Switching $%10s %s\n",
- formatWithCommas(avgParticipantContextCost), formatTimeUnit(avgParticipantContextHours))
+ fmt.Printf(" Context Switching $%10s %s (%.1f sessions)\n",
+ formatWithCommas(avgParticipantContextCost), formatTimeUnit(avgParticipantContextHours), avgParticipantSessions)
fmt.Println(" ──────────")
participantPct := (avgParticipantTotalCost / avgTotalCost) * 100
fmt.Printf(" Subtotal $%10s %s (%.1f%%)\n",
@@ -533,8 +526,9 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
formatWithCommas(avgFutureMergeCost), formatTimeUnit(avgFutureMergeHours), ext.FutureMergePRCount)
}
if ext.FutureContextCost > 0.01 {
- fmt.Printf(" Context Switching $%10s %s\n",
- formatWithCommas(avgFutureContextCost), formatTimeUnit(avgFutureContextHours))
+ avgFutureContextSessions := float64(ext.FutureContextSessions) / float64(ext.TotalPRs)
+ fmt.Printf(" Context Switching $%10s %s (%.1f sessions)\n",
+ formatWithCommas(avgFutureContextCost), formatTimeUnit(avgFutureContextHours), avgFutureContextSessions)
}
avgFutureCost := avgCodeChurnCost + avgFutureReviewCost + avgFutureMergeCost + avgFutureContextCost
avgFutureHours := avgCodeChurnHours + avgFutureReviewHours + avgFutureMergeHours + avgFutureContextHours
@@ -589,10 +583,10 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
formatWithCommas(ext.AuthorNewCodeCost), formatTimeUnit(ext.AuthorNewCodeHours), totalNewLOCStr)
fmt.Printf(" Adaptation $%10s %s (%s LOC)\n",
formatWithCommas(ext.AuthorAdaptationCost), formatTimeUnit(ext.AuthorAdaptationHours), totalModifiedLOCStr)
- fmt.Printf(" GitHub Activity $%10s %s\n",
- formatWithCommas(ext.AuthorGitHubCost), formatTimeUnit(ext.AuthorGitHubHours))
- fmt.Printf(" Context Switching $%10s %s\n",
- formatWithCommas(ext.AuthorGitHubContextCost), formatTimeUnit(ext.AuthorGitHubContextHours))
+ fmt.Printf(" GitHub Activity $%10s %s (%d events)\n",
+ formatWithCommas(ext.AuthorGitHubCost), formatTimeUnit(ext.AuthorGitHubHours), ext.AuthorEvents)
+ fmt.Printf(" Context Switching $%10s %s (%d sessions)\n",
+ formatWithCommas(ext.AuthorGitHubContextCost), formatTimeUnit(ext.AuthorGitHubContextHours), ext.AuthorSessions)
// Show bot PR LOC even though cost is $0
if ext.BotPRs > 0 {
@@ -612,15 +606,15 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
fmt.Println(" Participant Costs")
fmt.Println(" ─────────────────")
if ext.ParticipantReviewCost > 0 {
- fmt.Printf(" Review Activity $%10s %s\n",
- formatWithCommas(ext.ParticipantReviewCost), formatTimeUnit(ext.ParticipantReviewHours))
+ fmt.Printf(" Review Activity $%10s %s (%d reviews)\n",
+ formatWithCommas(ext.ParticipantReviewCost), formatTimeUnit(ext.ParticipantReviewHours), ext.ParticipantReviews)
}
if ext.ParticipantGitHubCost > 0 {
- fmt.Printf(" GitHub Activity $%10s %s\n",
- formatWithCommas(ext.ParticipantGitHubCost), formatTimeUnit(ext.ParticipantGitHubHours))
+ fmt.Printf(" GitHub Activity $%10s %s (%d events)\n",
+ formatWithCommas(ext.ParticipantGitHubCost), formatTimeUnit(ext.ParticipantGitHubHours), ext.ParticipantEvents)
}
- fmt.Printf(" Context Switching $%10s %s\n",
- formatWithCommas(ext.ParticipantContextCost), formatTimeUnit(ext.ParticipantContextHours))
+ fmt.Printf(" Context Switching $%10s %s (%d sessions)\n",
+ formatWithCommas(ext.ParticipantContextCost), formatTimeUnit(ext.ParticipantContextHours), ext.ParticipantSessions)
fmt.Println(" ──────────")
pct = (ext.ParticipantTotalCost / ext.TotalCost) * 100
fmt.Printf(" Subtotal $%10s %s (%.1f%%)\n",
@@ -681,8 +675,8 @@ func printExtrapolatedResults(title string, days int, ext *cost.ExtrapolatedBrea
formatWithCommas(ext.FutureMergeCost), formatTimeUnit(ext.FutureMergeHours), ext.FutureMergePRCount)
}
if ext.FutureContextCost > 0.01 {
- fmt.Printf(" Context Switching $%10s %s\n",
- formatWithCommas(ext.FutureContextCost), formatTimeUnit(ext.FutureContextHours))
+ fmt.Printf(" Context Switching $%10s %s (%d sessions)\n",
+ formatWithCommas(ext.FutureContextCost), formatTimeUnit(ext.FutureContextHours), ext.FutureContextSessions)
}
extFutureCost := ext.CodeChurnCost + ext.FutureReviewCost + ext.FutureMergeCost + ext.FutureContextCost
extFutureHours := ext.CodeChurnHours + ext.FutureReviewHours + ext.FutureMergeHours + ext.FutureContextHours
diff --git a/internal/server/server.go b/internal/server/server.go
index 736fb80..b8095e5 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -1412,28 +1412,13 @@ func (s *Server) processOrgSample(ctx context.Context, req *OrgSampleRequest, to
// Count unique authors across all PRs (not just samples)
totalAuthors := github.CountUniqueAuthors(prs)
- // Count open PRs across all unique repos in the organization
- uniqueRepos := make(map[string]bool)
- for _, pr := range prs {
- repoKey := pr.Owner + "/" + pr.Repo
- uniqueRepos[repoKey] = true
- }
-
- totalOpenPRs := 0
- for repoKey := range uniqueRepos {
- parts := strings.SplitN(repoKey, "/", 2)
- if len(parts) != 2 {
- continue
- }
- owner, repo := parts[0], parts[1]
- openCount, err := github.CountOpenPRsInRepo(ctx, owner, repo, token)
- if err != nil {
- s.logger.WarnContext(ctx, "Failed to count open PRs for repo", "repo", repoKey, errorKey, err)
- continue
- }
- totalOpenPRs += openCount
+ // Count open PRs across the entire organization with a single query
+ totalOpenPRs, err := github.CountOpenPRsInOrg(ctx, req.Org, token)
+ if err != nil {
+ s.logger.WarnContext(ctx, "Failed to count open PRs in organization, using 0", errorKey, err)
+ totalOpenPRs = 0
}
- s.logger.InfoContext(ctx, "Counted total open PRs across organization", "open_prs", totalOpenPRs, "repos", len(uniqueRepos))
+ s.logger.InfoContext(ctx, "Counted total open PRs across organization", "org", req.Org, "open_prs", totalOpenPRs)
// Extrapolate costs from samples
extrapolated := cost.ExtrapolateFromSamples(breakdowns, len(prs), totalAuthors, totalOpenPRs, actualDays, cfg)
diff --git a/internal/server/static/index.html b/internal/server/static/index.html
index 6d47638..edff0c5 100644
--- a/internal/server/static/index.html
+++ b/internal/server/static/index.html
@@ -1245,6 +1245,13 @@
Why calculate PR costs?
}
function formatLOC(kloc) {
+ const loc = kloc * 1000; // Convert to actual lines
+
+ // For values < 1k LOC, just show LOC count without 'k' suffix
+ if (loc < 1000) {
+ return Math.floor(loc) + ' LOC';
+ }
+
// For values >= 100k, add commas (e.g., "1,517k" instead of "1517k")
if (kloc >= 100.0) {
const intPart = Math.floor(kloc);
@@ -1404,7 +1411,7 @@ Why calculate PR costs?
html += `${annualWasteFormatted}
`;
const annualCostPerHead = salary * benefitsMultiplier;
const headcount = annualWasteCost / annualCostPerHead;
- html += `${headcount.toFixed(1)} headcount • ${formatTimeUnit(annualWasteHours)}
`;
+ html += `${headcount.toFixed(1)} headcount
`;
html += ''; // Close efficiency-box
}
@@ -1601,23 +1608,37 @@ Why calculate PR costs?
let output = '';
+ // Calculate LOC averages
+ const avgNewLines = e.total_new_lines / totalPRs;
+ const avgModifiedLines = e.total_modified_lines / totalPRs;
+ const avgTotalLOC = (avgNewLines + avgModifiedLines) / 1000;
+ const newLOC = avgNewLines / 1000;
+ const modifiedLOC = avgModifiedLines / 1000;
+
// Development Costs
- output += ' Development Costs\n';
- output += ' ─────────────────\n';
- output += ` New Development ${formatCurrency(avgAuthorNewCodeCost).padStart(10)} ${formatTimeUnit(avgAuthorNewCodeHours)}\n`;
- output += ` Adaptation ${formatCurrency(avgAuthorAdaptationCost).padStart(10)} ${formatTimeUnit(avgAuthorAdaptationHours)}\n`;
- output += ` GitHub Activity ${formatCurrency(avgAuthorGitHubCost).padStart(10)} ${formatTimeUnit(avgAuthorGitHubHours)}\n`;
- output += ` Context Switching ${formatCurrency(avgAuthorGitHubContextCost).padStart(10)} ${formatTimeUnit(avgAuthorGitHubContextHours)}\n`;
+ const avgAuthorEvents = e.author_events / totalPRs;
+ const avgAuthorSessions = e.author_sessions / totalPRs;
+ output += ` Development Costs (${formatLOC(avgTotalLOC)} total)\n`;
+ output += ' ──────────────────────────────────\n';
+ output += ` New Development ${formatCurrency(avgAuthorNewCodeCost).padStart(10)} ${formatTimeUnit(avgAuthorNewCodeHours)} (${formatLOC(newLOC)})\n`;
+ output += ` Adaptation ${formatCurrency(avgAuthorAdaptationCost).padStart(10)} ${formatTimeUnit(avgAuthorAdaptationHours)} (${formatLOC(modifiedLOC)})\n`;
+ output += ` GitHub Activity ${formatCurrency(avgAuthorGitHubCost).padStart(10)} ${formatTimeUnit(avgAuthorGitHubHours)} (${avgAuthorEvents.toFixed(1)} events)\n`;
+ output += ` Context Switching ${formatCurrency(avgAuthorGitHubContextCost).padStart(10)} ${formatTimeUnit(avgAuthorGitHubContextHours)} (${avgAuthorSessions.toFixed(1)} sessions)\n`;
output += ' ──────────\n';
let pct = (avgAuthorTotalCost / avgTotalCost) * 100;
output += ` Subtotal ${formatCurrency(avgAuthorTotalCost).padStart(10)} ${formatTimeUnit(avgAuthorTotalHours)} (${pct.toFixed(1)}%)\n\n`;
// Participants
if (e.participant_total_cost > 0) {
- output += ' Participant Costs\n';
+ const avgParticipantEvents = e.participant_events / totalPRs;
+ const avgParticipantSessions = e.participant_sessions / totalPRs;
+ const avgParticipantReviews = e.participant_reviews / totalPRs;
+
+ output += ` Participant Costs\n`;
output += ' ─────────────────\n';
- output += ` Review Activity ${formatCurrency(avgParticipantReviewCost).padStart(10)} ${formatTimeUnit(avgParticipantReviewHours)}\n`;
- output += ` GitHub Activity ${formatCurrency(avgParticipantGitHubCost).padStart(10)} ${formatTimeUnit(avgParticipantGitHubHours)}\n`;
+ output += ` Review Activity ${formatCurrency(avgParticipantReviewCost).padStart(10)} ${formatTimeUnit(avgParticipantReviewHours)} (${avgParticipantReviews.toFixed(1)} reviews)\n`;
+ output += ` GitHub Activity ${formatCurrency(avgParticipantGitHubCost).padStart(10)} ${formatTimeUnit(avgParticipantGitHubHours)} (${avgParticipantEvents.toFixed(1)} events)\n`;
+ output += ` Context Switching ${formatCurrency(avgParticipantContextCost).padStart(10)} ${formatTimeUnit(avgParticipantContextHours)} (${avgParticipantSessions.toFixed(1)} sessions)\n`;
output += ' ──────────\n';
pct = (avgParticipantTotalCost / avgTotalCost) * 100;
output += ` Subtotal ${formatCurrency(avgParticipantTotalCost).padStart(10)} ${formatTimeUnit(avgParticipantTotalHours)} (${pct.toFixed(1)}%)\n\n`;
@@ -1649,16 +1670,19 @@ Why calculate PR costs?
output += ' Future Costs\n';
output += ' ────────────\n';
if (e.code_churn_cost > 0.01) {
- output += ` Code Churn ${formatCurrency(avgCodeChurnCost).padStart(10)} ${formatTimeUnit(avgCodeChurnHours)}\n`;
+ const avgReworkPct = e.avg_rework_percentage || 0;
+ const label = avgReworkPct > 0 ? `Code Churn (${avgReworkPct.toFixed(0)}% drift)` : 'Code Churn';
+ output += ` ${label.padEnd(26)} ${formatCurrency(avgCodeChurnCost).padStart(10)} ${formatTimeUnit(avgCodeChurnHours)} (${e.code_churn_pr_count} PRs)\n`;
}
if (e.future_review_cost > 0.01) {
- output += ` Review ${formatCurrency(avgFutureReviewCost).padStart(10)} ${formatTimeUnit(avgFutureReviewHours)}\n`;
+ output += ` Review ${formatCurrency(avgFutureReviewCost).padStart(10)} ${formatTimeUnit(avgFutureReviewHours)} (${e.future_review_pr_count} PRs)\n`;
}
if (e.future_merge_cost > 0.01) {
- output += ` Merge ${formatCurrency(avgFutureMergeCost).padStart(10)} ${formatTimeUnit(avgFutureMergeHours)}\n`;
+ output += ` Merge ${formatCurrency(avgFutureMergeCost).padStart(10)} ${formatTimeUnit(avgFutureMergeHours)} (${e.future_merge_pr_count} PRs)\n`;
}
if (e.future_context_cost > 0.01) {
- output += ` Context Switching ${formatCurrency(avgFutureContextCost).padStart(10)} ${formatTimeUnit(avgFutureContextHours)}\n`;
+ const avgFutureContextSessions = e.future_context_sessions / totalPRs;
+ output += ` Context Switching ${formatCurrency(avgFutureContextCost).padStart(10)} ${formatTimeUnit(avgFutureContextHours)} (${avgFutureContextSessions.toFixed(1)} sessions)\n`;
}
const avgFutureCost = avgCodeChurnCost + avgFutureReviewCost + avgFutureMergeCost + avgFutureContextCost;
const avgFutureHours = avgCodeChurnHours + avgFutureReviewHours + avgFutureMergeHours + avgFutureContextHours;
@@ -1684,22 +1708,22 @@ Why calculate PR costs?
let output = '';
// Calculate LOC for header and lines
- const totalLOC = ((e.new_lines || 0) + (e.modified_lines || 0) + (e.bot_new_lines || 0) + (e.bot_modified_lines || 0)) / 1000;
- const newLOC = (e.new_lines || 0) / 1000;
- const modifiedLOC = (e.modified_lines || 0) / 1000;
+ const totalLOC = ((e.total_new_lines || 0) + (e.total_modified_lines || 0) + (e.bot_new_lines || 0) + (e.bot_modified_lines || 0)) / 1000;
+ const newLOC = (e.total_new_lines || 0) / 1000;
+ const modifiedLOC = (e.total_modified_lines || 0) / 1000;
const botLOC = ((e.bot_new_lines || 0) + (e.bot_modified_lines || 0)) / 1000;
// Development Costs
- output += ` Development Costs (${e.human_prs || e.total_prs || 0} PRs, ${totalLOC.toFixed(1)}k total LOC)\n`;
- output += ' ────────────────────────────────────────\n';
- output += ` New Development ${formatCurrency(e.author_new_code_cost).padStart(10)} ${formatTimeUnit(e.author_new_code_hours)} (${newLOC.toFixed(1)}k LOC)\n`;
- output += ` Adaptation ${formatCurrency(e.author_adaptation_cost).padStart(10)} ${formatTimeUnit(e.author_adaptation_hours)} (${modifiedLOC.toFixed(1)}k LOC)\n`;
- output += ` GitHub Activity ${formatCurrency(e.author_github_cost).padStart(10)} ${formatTimeUnit(e.author_github_hours)}\n`;
- output += ` Context Switching ${formatCurrency(e.author_github_context_cost).padStart(10)} ${formatTimeUnit(e.author_github_context_hours)}\n`;
+ output += ` Development Costs (${e.human_prs || e.total_prs || 0} PRs, ${formatLOC(totalLOC)} total)\n`;
+ output += ' ─────────────────────────────────────────────────────────────────────\n';
+ output += ` New Development ${formatCurrency(e.author_new_code_cost).padStart(10)} ${formatTimeUnit(e.author_new_code_hours)} (${formatLOC(newLOC)})\n`;
+ output += ` Adaptation ${formatCurrency(e.author_adaptation_cost).padStart(10)} ${formatTimeUnit(e.author_adaptation_hours)} (${formatLOC(modifiedLOC)})\n`;
+ output += ` GitHub Activity ${formatCurrency(e.author_github_cost).padStart(10)} ${formatTimeUnit(e.author_github_hours)} (${e.author_events || 0} events)\n`;
+ output += ` Context Switching ${formatCurrency(e.author_github_context_cost).padStart(10)} ${formatTimeUnit(e.author_github_context_hours)} (${e.author_sessions || 0} sessions)\n`;
// Show bot PR LOC even though cost is $0
if ((e.bot_prs || 0) > 0) {
- output += ` Automated Updates — ${formatTimeUnit(0)} (${e.bot_prs} PRs, ${botLOC.toFixed(1)}k LOC)\n`;
+ output += ` Automated Updates — ${formatTimeUnit(0)} (${e.bot_prs} PRs, ${formatLOC(botLOC)})\n`;
}
output += ' ──────────\n';
@@ -1708,15 +1732,15 @@ Why calculate PR costs?
// Participants
if (e.participant_total_cost > 0) {
- output += ' Participant Costs\n';
+ output += ` Participant Costs\n`;
output += ' ─────────────────\n';
if ((e.participant_review_cost || 0) > 0) {
- output += ` Review Activity ${formatCurrency(e.participant_review_cost).padStart(10)} ${formatTimeUnit(e.participant_review_hours)}\n`;
+ output += ` Review Activity ${formatCurrency(e.participant_review_cost).padStart(10)} ${formatTimeUnit(e.participant_review_hours)} (${e.participant_reviews || 0} reviews)\n`;
}
if ((e.participant_github_cost || 0) > 0) {
- output += ` GitHub Activity ${formatCurrency(e.participant_github_cost).padStart(10)} ${formatTimeUnit(e.participant_github_hours)}\n`;
+ output += ` GitHub Activity ${formatCurrency(e.participant_github_cost).padStart(10)} ${formatTimeUnit(e.participant_github_hours)} (${e.participant_events || 0} events)\n`;
}
- output += ` Context Switching ${formatCurrency(e.participant_context_cost).padStart(10)} ${formatTimeUnit(e.participant_context_hours)}\n`;
+ output += ` Context Switching ${formatCurrency(e.participant_context_cost).padStart(10)} ${formatTimeUnit(e.participant_context_hours)} (${e.participant_sessions || 0} sessions)\n`;
output += ' ──────────\n';
pct = (e.participant_total_cost / e.total_cost) * 100;
output += ` Subtotal ${formatCurrency(e.participant_total_cost).padStart(10)} ${formatTimeUnit(e.participant_total_hours)} (${pct.toFixed(1)}%)\n\n`;
@@ -1764,7 +1788,7 @@ Why calculate PR costs?
output += ` Merge ${formatCurrency(e.future_merge_cost).padStart(10)} ${formatTimeUnit(e.future_merge_hours)} (${openPRs} PRs)\n`;
}
if ((e.future_context_cost || 0) > 0.01) {
- output += ` Context Switching ${formatCurrency(e.future_context_cost).padStart(10)} ${formatTimeUnit(e.future_context_hours)}\n`;
+ output += ` Context Switching ${formatCurrency(e.future_context_cost).padStart(10)} ${formatTimeUnit(e.future_context_hours)} (${e.future_context_sessions || 0} sessions)\n`;
}
const futureCost = (e.code_churn_cost || 0) + (e.future_review_cost || 0) + (e.future_merge_cost || 0) + (e.future_context_cost || 0);
const futureHours = (e.code_churn_hours || 0) + (e.future_review_hours || 0) + (e.future_merge_hours || 0) + (e.future_context_hours || 0);
diff --git a/pkg/cost/cost.go b/pkg/cost/cost.go
index 4e6c56a..ef733bc 100644
--- a/pkg/cost/cost.go
+++ b/pkg/cost/cost.go
@@ -93,22 +93,22 @@ type Config struct {
// DefaultConfig returns reasonable defaults for cost calculation.
func DefaultConfig() Config {
return Config{
- AnnualSalary: 249000.0, // Average Staff Software Engineer salary (2025, Glassdoor)
- BenefitsMultiplier: 1.3, // 30% benefits overhead
- HoursPerYear: 2080.0, // Standard full-time hours
- EventDuration: 10 * time.Minute, // 10 minutes per GitHub event
- ContextSwitchInDuration: 3 * time.Minute, // 3 min to context switch in (Microsoft Research)
- ContextSwitchOutDuration: 16*time.Minute + 33*time.Second, // 16m33s to context switch out (Microsoft Research)
- SessionGapThreshold: 20 * time.Minute, // Events within 20 min are same session
- DeliveryDelayFactor: 0.15, // 15% opportunity cost
- AutomatedUpdatesFactor: 0.01, // 1% overhead for bot PRs
- PRTrackingMinutesPerDay: 1.0, // 1 minute per day for PRs open >24 hours
- MaxDelayAfterLastEvent: 14 * 24 * time.Hour, // 14 days (2 weeks) after last event
- MaxProjectDelay: 90 * 24 * time.Hour, // 90 days absolute max
- MaxCodeDrift: 90 * 24 * time.Hour, // 90 days
- ReviewInspectionRate: 275.0, // 275 LOC/hour (average of optimal 150-400 range)
- ModificationCostFactor: 0.4, // Modified code costs 40% of new code
- COCOMO: cocomo.DefaultConfig(),
+ AnnualSalary: 249000.0, // Average Staff Software Engineer salary (2025, Glassdoor)
+ BenefitsMultiplier: 1.3, // 30% benefits overhead
+ HoursPerYear: 2080.0, // Standard full-time hours
+ EventDuration: 10 * time.Minute, // 10 minutes per GitHub event
+ ContextSwitchInDuration: 3 * time.Minute, // 3 min to context switch in (Microsoft Research)
+ ContextSwitchOutDuration: 16*time.Minute + 33*time.Second, // 16m33s to context switch out (Microsoft Research)
+ SessionGapThreshold: 20 * time.Minute, // Events within 20 min are same session
+ DeliveryDelayFactor: 0.15, // 15% opportunity cost
+ AutomatedUpdatesFactor: 0.01, // 1% overhead for bot PRs
+ PRTrackingMinutesPerDay: 1.0, // 1 minute per day for PRs open >24 hours
+ MaxDelayAfterLastEvent: 14 * 24 * time.Hour, // 14 days (2 weeks) after last event
+ MaxProjectDelay: 90 * 24 * time.Hour, // 90 days absolute max
+ MaxCodeDrift: 90 * 24 * time.Hour, // 90 days
+ ReviewInspectionRate: 275.0, // 275 LOC/hour (average of optimal 150-400 range)
+ ModificationCostFactor: 0.4, // Modified code costs 40% of new code
+ COCOMO: cocomo.DefaultConfig(),
}
}
diff --git a/pkg/cost/cost_test.go b/pkg/cost/cost_test.go
index c629876..7be45fb 100644
--- a/pkg/cost/cost_test.go
+++ b/pkg/cost/cost_test.go
@@ -31,10 +31,6 @@ func TestDefaultConfig(t *testing.T) {
t.Errorf("Expected delivery delay factor 0.15, got %.2f", cfg.DeliveryDelayFactor)
}
- if cfg.CoordinationFactor != 0.05 {
- t.Errorf("Expected coordination factor 0.05, got %.2f", cfg.CoordinationFactor)
- }
-
if cfg.MaxDelayAfterLastEvent != 14*24*time.Hour {
t.Errorf("Expected 14 days max delay after last event, got %v", cfg.MaxDelayAfterLastEvent)
}
@@ -354,11 +350,6 @@ func TestCalculateDelayComponents(t *testing.T) {
t.Error("Delivery delay cost should be positive for 7-day old PR")
}
- // Should have coordination cost
- if breakdown.DelayCostDetail.CoordinationCost <= 0 {
- t.Error("Coordination cost should be positive for 7-day old PR")
- }
-
// Should have code churn cost (7 days = ~7% drift)
if breakdown.DelayCostDetail.CodeChurnCost <= 0 {
t.Error("Code churn cost should be positive for 7-day old PR")
@@ -383,7 +374,6 @@ func TestCalculateDelayComponents(t *testing.T) {
// Total delay should equal sum of components
expectedDelay := breakdown.DelayCostDetail.DeliveryDelayCost +
- breakdown.DelayCostDetail.CoordinationCost +
breakdown.DelayCostDetail.CodeChurnCost +
breakdown.DelayCostDetail.AutomatedUpdatesCost +
breakdown.DelayCostDetail.PRTrackingCost +
@@ -418,15 +408,11 @@ func TestCalculateShortPRNoRework(t *testing.T) {
t.Error("Rework percentage should be zero for 2-day old PR")
}
- // Should still have delivery delay and coordination costs
+ // Should still have delivery delay cost
if breakdown.DelayCostDetail.DeliveryDelayCost <= 0 {
t.Error("Delivery delay cost should be positive even for short PR")
}
- if breakdown.DelayCostDetail.CoordinationCost <= 0 {
- t.Error("Coordination cost should be positive even for short PR")
- }
-
futureTotalCost := breakdown.DelayCostDetail.FutureReviewCost +
breakdown.DelayCostDetail.FutureMergeCost +
breakdown.DelayCostDetail.FutureContextCost
@@ -511,20 +497,13 @@ func TestCalculateWithRealPR13(t *testing.T) {
// 90 days absolute cap = 2160 hours
// Delivery: 2160 * 0.15 = 324 hours
- // Coordination: 2160 * 0.05 = 108 hours
- expectedDeliveryHours := 90.0 * 24.0 * 0.15 // 324 hours
- expectedCoordinationHours := 90.0 * 24.0 * 0.05 // 108 hours
+ expectedDeliveryHours := 90.0 * 24.0 * 0.15 // 324 hours
if breakdown.DelayCostDetail.DeliveryDelayHours != expectedDeliveryHours {
t.Errorf("Expected %.0f delivery delay hours (15%% of 90 day cap), got %.2f",
expectedDeliveryHours, breakdown.DelayCostDetail.DeliveryDelayHours)
}
- if breakdown.DelayCostDetail.CoordinationHours != expectedCoordinationHours {
- t.Errorf("Expected %.0f coordination hours (5%% of 90 day cap), got %.2f",
- expectedCoordinationHours, breakdown.DelayCostDetail.CoordinationHours)
- }
-
// Code drift should be capped at 90 days (not unlimited)
// At 90 days, drift is ~35%, so we should never see >100% rework
if breakdown.DelayCostDetail.ReworkPercentage > 100.0 {
@@ -543,8 +522,6 @@ func TestCalculateWithRealPR13(t *testing.T) {
t.Logf(" Author cost: $%.2f", breakdown.Author.TotalCost)
t.Logf(" Delivery Delay (15%%): $%.2f (%.0f hrs, capped at 90 days)",
breakdown.DelayCostDetail.DeliveryDelayCost, breakdown.DelayCostDetail.DeliveryDelayHours)
- t.Logf(" Coordination (5%%): $%.2f (%.0f hrs, capped at 90 days)",
- breakdown.DelayCostDetail.CoordinationCost, breakdown.DelayCostDetail.CoordinationHours)
t.Logf(" Code Churn: $%.2f (%.1f%% rework, capped at 90 days drift)",
breakdown.DelayCostDetail.CodeChurnCost, breakdown.DelayCostDetail.ReworkPercentage)
futureTotalCost := breakdown.DelayCostDetail.FutureReviewCost +
@@ -579,30 +556,23 @@ func TestCalculateLongPRCapped(t *testing.T) {
// Last event was 120 days ago, so we only count 14 days after it
// Capped hours: 14 days = 336 hours
// Delivery delay: 336 * 0.15 = 50.4 hours
- // Coordination: 336 * 0.05 = 16.8 hours
expectedDeliveryHours := 14.0 * 24.0 * 0.15
- expectedCoordinationHours := 14.0 * 24.0 * 0.05
if breakdown.DelayCostDetail.DeliveryDelayHours != expectedDeliveryHours {
t.Errorf("Expected %.1f delivery delay hours (15%% of 14 days), got %.2f",
expectedDeliveryHours, breakdown.DelayCostDetail.DeliveryDelayHours)
}
-
- if breakdown.DelayCostDetail.CoordinationHours != expectedCoordinationHours {
- t.Errorf("Expected %.1f coordination hours (5%% of 14 days), got %.2f",
- expectedCoordinationHours, breakdown.DelayCostDetail.CoordinationHours)
- }
}
func TestDelayHoursNeverExceedPRAge(t *testing.T) {
// Test that delay hours (as productivity-equivalent time) are reasonable
- // Delivery (15%) + Coordination (5%) should equal 20% of PR age
+ // Delivery (15%) should be proportional to PR age
now := time.Now()
testCases := []struct {
name string
ageHrs float64
prData PRData
- wantMax float64 // Maximum acceptable delay hours (delivery + coordination)
+ wantMax float64 // Maximum acceptable delay hours (delivery)
}{
{
name: "1 day old PR",
@@ -615,7 +585,7 @@ func TestDelayHoursNeverExceedPRAge(t *testing.T) {
},
CreatedAt: now.Add(-24 * time.Hour),
},
- wantMax: 24.0 * 0.20, // 20% of 24 hours = 4.8 hours
+ wantMax: 24.0 * 0.15, // 15% of 24 hours = 3.6 hours
},
{
name: "7 day old PR",
@@ -628,7 +598,7 @@ func TestDelayHoursNeverExceedPRAge(t *testing.T) {
},
CreatedAt: now.Add(-7 * 24 * time.Hour),
},
- wantMax: 168.0 * 0.20, // 20% of 168 hours = 33.6 hours
+ wantMax: 168.0 * 0.15, // 15% of 168 hours = 25.2 hours
},
}
@@ -638,28 +608,19 @@ func TestDelayHoursNeverExceedPRAge(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
breakdown := Calculate(tc.prData, cfg)
- totalDelayHours := breakdown.DelayCostDetail.DeliveryDelayHours +
- breakdown.DelayCostDetail.CoordinationHours
-
- // Total delay hours should not exceed 20% of PR age
- if totalDelayHours > tc.wantMax+0.1 { // Allow 0.1 hr tolerance for floating point
- t.Errorf("Delay hours exceed 20%% of PR age: got %.2f hours, want <= %.2f hours (PR age: %.1f hrs)",
- totalDelayHours, tc.wantMax, tc.ageHrs)
+ // Delivery delay hours should not exceed 15% of PR age
+ if breakdown.DelayCostDetail.DeliveryDelayHours > tc.wantMax+0.1 { // Allow 0.1 hr tolerance for floating point
+ t.Errorf("Delay hours exceed 15%% of PR age: got %.2f hours, want <= %.2f hours (PR age: %.1f hrs)",
+ breakdown.DelayCostDetail.DeliveryDelayHours, tc.wantMax, tc.ageHrs)
}
- // Verify the split: delivery should be 15%, coordination should be 5%
+ // Verify delivery should be 15%
expectedDelivery := tc.ageHrs * 0.15
- expectedCoordination := tc.ageHrs * 0.05
if breakdown.DelayCostDetail.DeliveryDelayHours > expectedDelivery+0.1 {
t.Errorf("Delivery delay hours too high: got %.2f, want %.2f (15%% of %.1f)",
breakdown.DelayCostDetail.DeliveryDelayHours, expectedDelivery, tc.ageHrs)
}
-
- if breakdown.DelayCostDetail.CoordinationHours > expectedCoordination+0.1 {
- t.Errorf("Coordination hours too high: got %.2f, want %.2f (5%% of %.1f)",
- breakdown.DelayCostDetail.CoordinationHours, expectedCoordination, tc.ageHrs)
- }
})
}
}
@@ -727,10 +688,6 @@ func TestCalculateFastTurnaroundNoDelay(t *testing.T) {
t.Errorf("Expected 0 delivery delay cost for %v minute PR, got $%.2f",
tc.openMinutes, breakdown.DelayCostDetail.DeliveryDelayCost)
}
- if breakdown.DelayCostDetail.CoordinationCost != 0 {
- t.Errorf("Expected 0 coordination cost for %v minute PR, got $%.2f",
- tc.openMinutes, breakdown.DelayCostDetail.CoordinationCost)
- }
} else if breakdown.DelayCost == 0 {
// For PRs >= 30 minutes, delay cost should be > 0
t.Errorf("Expected non-zero delay cost for %v minute PR, got $0",
diff --git a/pkg/cost/extrapolate.go b/pkg/cost/extrapolate.go
index 0ddc1e9..4d4f70a 100644
--- a/pkg/cost/extrapolate.go
+++ b/pkg/cost/extrapolate.go
@@ -35,6 +35,10 @@ type ExtrapolatedBreakdown struct {
AuthorGitHubContextHours float64 `json:"author_github_context_hours"`
AuthorTotalHours float64 `json:"author_total_hours"`
+ // Author activity metrics (extrapolated)
+ AuthorEvents int `json:"author_events"` // Total GitHub events by authors
+ AuthorSessions int `json:"author_sessions"` // Total GitHub work sessions by authors
+
// LOC metrics (extrapolated totals)
TotalNewLines int `json:"total_new_lines"` // Total net new lines across all PRs
TotalModifiedLines int `json:"total_modified_lines"` // Total modified lines across all PRs
@@ -54,6 +58,11 @@ type ExtrapolatedBreakdown struct {
ParticipantContextHours float64 `json:"participant_context_hours"`
ParticipantTotalHours float64 `json:"participant_total_hours"`
+ // Participant activity metrics (extrapolated)
+ ParticipantEvents int `json:"participant_events"` // Total GitHub events by participants
+ ParticipantSessions int `json:"participant_sessions"` // Total GitHub work sessions by participants
+ ParticipantReviews int `json:"participant_reviews"` // Total number of reviews performed
+
// Delay costs (extrapolated)
DeliveryDelayCost float64 `json:"delivery_delay_cost"`
CodeChurnCost float64 `json:"code_churn_cost"`
@@ -75,9 +84,11 @@ type ExtrapolatedBreakdown struct {
DelayTotalHours float64 `json:"delay_total_hours"`
// Counts for future costs (extrapolated)
- CodeChurnPRCount int `json:"code_churn_pr_count"` // Number of PRs with code churn
- FutureReviewPRCount int `json:"future_review_pr_count"` // Number of PRs with future review costs
- FutureMergePRCount int `json:"future_merge_pr_count"` // Number of PRs with future merge costs
+ CodeChurnPRCount int `json:"code_churn_pr_count"` // Number of PRs with code churn
+ FutureReviewPRCount int `json:"future_review_pr_count"` // Number of PRs with future review costs
+ FutureMergePRCount int `json:"future_merge_pr_count"` // Number of PRs with future merge costs
+ FutureContextSessions int `json:"future_context_sessions"` // Estimated future context switching sessions
+ AvgReworkPercentage float64 `json:"avg_rework_percentage"` // Average code drift/rework percentage
// Grand totals
TotalCost float64 `json:"total_cost"`
@@ -138,6 +149,10 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
var sumPRDuration float64
var sumNewLines, sumModifiedLines int
var sumBotNewLines, sumBotModifiedLines int
+ var sumAuthorEvents, sumAuthorSessions int
+ var sumParticipantEvents, sumParticipantSessions, sumParticipantReviews int
+ var sumFutureContextSessions int
+ var sumReworkPercentage float64
var countCodeChurn, countFutureReview, countFutureMerge int
for i := range breakdowns {
@@ -180,6 +195,8 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
sumAuthorGitHubHours += breakdown.Author.GitHubHours
sumAuthorGitHubContextHours += breakdown.Author.GitHubContextHours
sumAuthorHours += breakdown.Author.TotalHours
+ sumAuthorEvents += breakdown.Author.Events
+ sumAuthorSessions += breakdown.Author.Sessions
// Accumulate participant costs (combined across all participants)
for _, p := range breakdown.Participants {
@@ -191,6 +208,11 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
sumParticipantGitHubHours += p.GitHubHours
sumParticipantContextHours += p.GitHubContextHours
sumParticipantHours += p.TotalHours
+ sumParticipantEvents += p.Events
+ sumParticipantSessions += p.Sessions
+ if p.ReviewCost > 0 {
+ sumParticipantReviews++ // Count reviewers (participants who performed reviews)
+ }
}
// Accumulate delay costs
@@ -202,9 +224,10 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
sumFutureMergeCost += breakdown.DelayCostDetail.FutureMergeCost
sumFutureContextCost += breakdown.DelayCostDetail.FutureContextCost
- // Count PRs with each future cost type
+ // Count PRs with each future cost type and accumulate rework percentage
if breakdown.DelayCostDetail.CodeChurnCost > 0.01 {
countCodeChurn++
+ sumReworkPercentage += breakdown.DelayCostDetail.ReworkPercentage
}
if breakdown.DelayCostDetail.FutureReviewCost > 0.01 {
countFutureReview++
@@ -212,6 +235,10 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
if breakdown.DelayCostDetail.FutureMergeCost > 0.01 {
countFutureMerge++
}
+ if breakdown.DelayCostDetail.FutureContextCost > 0.01 {
+ // Future context cost assumes 3 sessions per open PR (review request, review, merge)
+ sumFutureContextSessions += 3
+ }
sumDeliveryDelayHours += breakdown.DelayCostDetail.DeliveryDelayHours
sumCodeChurnHours += breakdown.DelayCostDetail.CodeChurnHours
sumAutomatedUpdatesHours += breakdown.DelayCostDetail.AutomatedUpdatesHours
@@ -244,6 +271,8 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
extAuthorGitHubContextHours := sumAuthorGitHubContextHours / samples * multiplier
extAuthorTotal := extAuthorNewCodeCost + extAuthorAdaptationCost + extAuthorGitHubCost + extAuthorGitHubContextCost
extAuthorHours := sumAuthorHours / samples * multiplier
+ extAuthorEvents := int(float64(sumAuthorEvents) / samples * multiplier)
+ extAuthorSessions := int(float64(sumAuthorSessions) / samples * multiplier)
extParticipantReviewCost := sumParticipantReviewCost / samples * multiplier
extParticipantGitHubCost := sumParticipantGitHubCost / samples * multiplier
@@ -253,6 +282,9 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
extParticipantGitHubHours := sumParticipantGitHubHours / samples * multiplier
extParticipantContextHours := sumParticipantContextHours / samples * multiplier
extParticipantHours := sumParticipantHours / samples * multiplier
+ extParticipantEvents := int(float64(sumParticipantEvents) / samples * multiplier)
+ extParticipantSessions := int(float64(sumParticipantSessions) / samples * multiplier)
+ extParticipantReviews := int(float64(sumParticipantReviews) / samples * multiplier)
extDeliveryDelayCost := sumDeliveryDelayCost / samples * multiplier
extCodeChurnCost := sumCodeChurnCost / samples * multiplier
@@ -278,9 +310,16 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
extCodeChurnPRCount := int(float64(countCodeChurn) / samples * multiplier)
extFutureReviewPRCount := int(float64(countFutureReview) / samples * multiplier)
extFutureMergePRCount := int(float64(countFutureMerge) / samples * multiplier)
+ extFutureContextSessions := int(float64(sumFutureContextSessions) / samples * multiplier)
// Use actual open PR count from repository query, not extrapolated from sample
extOpenPRs := actualOpenPRs
+ // Calculate average rework percentage (only for PRs with code churn)
+ var avgReworkPercentage float64
+ if countCodeChurn > 0 {
+ avgReworkPercentage = sumReworkPercentage / float64(countCodeChurn)
+ }
+
extTotalCost := sumTotalCost / samples * multiplier
extTotalHours := extAuthorHours + extParticipantHours + extDelayHours
@@ -419,6 +458,9 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
AuthorGitHubContextHours: extAuthorGitHubContextHours,
AuthorTotalHours: extAuthorHours,
+ AuthorEvents: extAuthorEvents,
+ AuthorSessions: extAuthorSessions,
+
TotalNewLines: extTotalNewLines,
TotalModifiedLines: extTotalModifiedLines,
BotNewLines: extBotNewLines,
@@ -435,10 +477,14 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
ParticipantContextHours: extParticipantContextHours,
ParticipantTotalHours: extParticipantHours,
+ ParticipantEvents: extParticipantEvents,
+ ParticipantSessions: extParticipantSessions,
+ ParticipantReviews: extParticipantReviews,
+
DeliveryDelayCost: extDeliveryDelayCost,
CodeChurnCost: extCodeChurnCost,
AutomatedUpdatesCost: extAutomatedUpdatesCost,
- PRTrackingCost: extPRTrackingCost,
+ PRTrackingCost: extPRTrackingCost,
FutureReviewCost: extFutureReviewCost,
FutureMergeCost: extFutureMergeCost,
FutureContextCost: extFutureContextCost,
@@ -447,15 +493,17 @@ func ExtrapolateFromSamples(breakdowns []Breakdown, totalPRs, totalAuthors, actu
DeliveryDelayHours: extDeliveryDelayHours,
CodeChurnHours: extCodeChurnHours,
AutomatedUpdatesHours: extAutomatedUpdatesHours,
- PRTrackingHours: extPRTrackingHours,
+ PRTrackingHours: extPRTrackingHours,
FutureReviewHours: extFutureReviewHours,
FutureMergeHours: extFutureMergeHours,
FutureContextHours: extFutureContextHours,
DelayTotalHours: extDelayHours,
- CodeChurnPRCount: extCodeChurnPRCount,
- FutureReviewPRCount: extFutureReviewPRCount,
- FutureMergePRCount: extFutureMergePRCount,
+ CodeChurnPRCount: extCodeChurnPRCount,
+ FutureReviewPRCount: extFutureReviewPRCount,
+ FutureMergePRCount: extFutureMergePRCount,
+ FutureContextSessions: extFutureContextSessions,
+ AvgReworkPercentage: avgReworkPercentage,
TotalCost: extTotalCost,
TotalHours: extTotalHours,
diff --git a/pkg/github/query.go b/pkg/github/query.go
index 43cbc8c..24eb366 100644
--- a/pkg/github/query.go
+++ b/pkg/github/query.go
@@ -48,7 +48,6 @@ func FetchPRsFromRepo(ctx context.Context, owner, repo string, since time.Time,
return nil, err
}
-
// If we didn't hit the limit, we got all PRs within the period - done!
if !hitLimit {
return recent, nil