diff --git a/cmd/analyze.go b/cmd/analyze.go index 3f478a3..17a2eb4 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -380,17 +380,32 @@ var analyzeCmd = &cobra.Command{ // Analyze commit activity per day activity := analyzer.CommitsPerDay(commits) + // Fetch PRs for merge rate calculation + prs, _ := client.GetPullRequestsWithLimit(owner, repo, "all", 100) + mergedCount := 0 + for _, pr := range prs { + if pr.MergedAt != nil { + mergedCount++ + } + } + prMergeRate := 0.0 + if len(prs) > 0 { + prMergeRate = float64(mergedCount) / float64(len(prs)) * 100 + } + // Build recruiter summary recruiterSummary := analyzer.BuildRecruiterSummary( repoInfo.FullName, - repoInfo.Forks, repoInfo.Stars, + repoInfo.Forks, len(commits), len(contributors), maturityScore, maturityLevel, busFactor, busRisk, + repoInfo.OpenIssues, + prMergeRate, ) // Fetch README content and calculate contribution score if contribute flag is enabled diff --git a/internal/analyzer/recruiter_summary.go b/internal/analyzer/recruiter_summary.go index 6e2e3e8..e06efc9 100644 --- a/internal/analyzer/recruiter_summary.go +++ b/internal/analyzer/recruiter_summary.go @@ -26,6 +26,8 @@ func BuildRecruiterSummary( maturityLevel string, busFactor int, busRisk string, + openIssues int, + prMergeRate float64, ) RecruiterSummary { activity := "Low" @@ -35,6 +37,20 @@ func BuildRecruiterSummary( activity = "Moderate" } + issueHealth := "Critical" + if openIssues < 20 { + issueHealth = "Healthy" + } else if openIssues < 50 { + issueHealth = "Backlogged" + } + + prHealth := "Stalled" + if prMergeRate > 60 { + prHealth = "Active" + } else if prMergeRate >= 30 { + prHealth = "Slow" + } + return RecruiterSummary{ RepoName: repoName, Stars: stars, @@ -45,6 +61,8 @@ func BuildRecruiterSummary( MaturityLevel: maturityLevel, BusFactor: busFactor, BusRisk: busRisk, + IssueHealth: issueHealth, + PRHealth: prHealth, ActivityLevel: activity, } } diff --git a/internal/analyzer/recruiter_summary_test.go b/internal/analyzer/recruiter_summary_test.go new file mode 100644 index 0000000..cbf20e3 --- /dev/null +++ b/internal/analyzer/recruiter_summary_test.go @@ -0,0 +1,72 @@ +package analyzer + +import ( + "testing" +) + +func TestBuildRecruiterSummary_Metrics(t *testing.T) { + tests := []struct { + name string + openIssues int + prMergeRate float64 + expectedIssueHealth string + expectedPRHealth string + }{ + { + name: "Healthy issues and active PRs", + openIssues: 15, + prMergeRate: 75.0, + expectedIssueHealth: "Healthy", + expectedPRHealth: "Active", + }, + { + name: "Backlogged issues and slow PRs", + openIssues: 30, + prMergeRate: 45.0, + expectedIssueHealth: "Backlogged", + expectedPRHealth: "Slow", + }, + { + name: "Critical issues and stalled PRs", + openIssues: 55, + prMergeRate: 15.0, + expectedIssueHealth: "Critical", + expectedPRHealth: "Stalled", + }, + { + name: "Boundary test: 20 issues", + openIssues: 20, + prMergeRate: 60.0, + expectedIssueHealth: "Backlogged", + expectedPRHealth: "Slow", + }, + { + name: "Boundary test: 50 issues", + openIssues: 50, + prMergeRate: 30.0, + expectedIssueHealth: "Critical", + expectedPRHealth: "Slow", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + summary := BuildRecruiterSummary( + "test/repo", + 100, 50, // stars, forks + 200, 10, // commits, contributors + 80, "High", // maturity + 5, "Low", // bus factor + tt.openIssues, + tt.prMergeRate, + ) + + if summary.IssueHealth != tt.expectedIssueHealth { + t.Errorf("IssueHealth = %v, want %v", summary.IssueHealth, tt.expectedIssueHealth) + } + if summary.PRHealth != tt.expectedPRHealth { + t.Errorf("PRHealth = %v, want %v", summary.PRHealth, tt.expectedPRHealth) + } + }) + } +} diff --git a/internal/output/compare.go b/internal/output/compare.go index a119c3e..0c400cb 100644 --- a/internal/output/compare.go +++ b/internal/output/compare.go @@ -41,10 +41,10 @@ type CompareRepository struct { // CompareReport is the complete comparison payload used by every output format. type CompareReport struct { - GeneratedAt time.Time `json:"generated_at"` + GeneratedAt time.Time `json:"generated_at"` Repo1 CompareRepository `json:"repo1"` Repo2 CompareRepository `json:"repo2"` - Verdict string `json:"verdict"` + Verdict string `json:"verdict"` } // IsIdentical reports whether the key terminal metrics are equivalent. diff --git a/internal/output/compare_html.go b/internal/output/compare_html.go index 4c7fd9f..64bc339 100644 --- a/internal/output/compare_html.go +++ b/internal/output/compare_html.go @@ -207,4 +207,4 @@ func scoreClass(score int) string { } } -// removed unused helpers: HasLanguages, joinClasses \ No newline at end of file +// removed unused helpers: HasLanguages, joinClasses diff --git a/internal/output/compare_test.go b/internal/output/compare_test.go index 58bb075..50b4cd6 100644 --- a/internal/output/compare_test.go +++ b/internal/output/compare_test.go @@ -21,9 +21,9 @@ func TestBuildCompareReport(t *testing.T) { DefaultBranch: "main", PushedAt: time.Now().Add(-24 * time.Hour), }, - Commits: []github.Commit{{}, {}, {}}, + Commits: []github.Commit{{}, {}, {}}, Contributors: []github.Contributor{{}, {}}, - Languages: map[string]int{"Go": 1000, "Shell": 100}, + Languages: map[string]int{"Go": 1000, "Shell": 100}, }, CompareInput{ Repo: &github.Repo{ @@ -36,9 +36,9 @@ func TestBuildCompareReport(t *testing.T) { DefaultBranch: "main", PushedAt: time.Now().Add(-48 * time.Hour), }, - Commits: []github.Commit{{}, {}}, + Commits: []github.Commit{{}, {}}, Contributors: []github.Contributor{{}}, - Languages: map[string]int{"TypeScript": 900, "CSS": 50}, + Languages: map[string]int{"TypeScript": 900, "CSS": 50}, }, ) @@ -58,16 +58,16 @@ func TestBuildCompareReport(t *testing.T) { func TestRenderCompareOutputs(t *testing.T) { report := BuildCompareReport( CompareInput{ - Repo: &github.Repo{FullName: "owner/alpha", Stars: 10, Forks: 4, OpenIssues: 1, Language: "Go", DefaultBranch: "main"}, - Commits: []github.Commit{{}}, + Repo: &github.Repo{FullName: "owner/alpha", Stars: 10, Forks: 4, OpenIssues: 1, Language: "Go", DefaultBranch: "main"}, + Commits: []github.Commit{{}}, Contributors: []github.Contributor{{}}, - Languages: map[string]int{"Go": 1}, + Languages: map[string]int{"Go": 1}, }, CompareInput{ - Repo: &github.Repo{FullName: "owner/beta", Stars: 20, Forks: 6, OpenIssues: 2, Language: "Rust", DefaultBranch: "main"}, - Commits: []github.Commit{{}, {}}, + Repo: &github.Repo{FullName: "owner/beta", Stars: 20, Forks: 6, OpenIssues: 2, Language: "Rust", DefaultBranch: "main"}, + Commits: []github.Commit{{}, {}}, Contributors: []github.Contributor{{}, {}}, - Languages: map[string]int{"Rust": 2}, + Languages: map[string]int{"Rust": 2}, }, ) @@ -128,4 +128,4 @@ func TestRenderCompareOutputs(t *testing.T) { if !strings.Contains(normHTML, strings.ToLower(report.Verdict)) { t.Fatalf("html output missing verdict: %s", string(htmlData)) } -} \ No newline at end of file +} diff --git a/internal/output/recruiter.go b/internal/output/recruiter.go index e01092b..e4b3df2 100644 --- a/internal/output/recruiter.go +++ b/internal/output/recruiter.go @@ -20,5 +20,7 @@ func PrintRecruiterSummary(s analyzer.RecruiterSummary) { fmt.Println("👥 Contributors:", s.Contributors) fmt.Println("🏗️ Maturity:", s.MaturityLevel, "(", s.MaturityScore, ")") fmt.Println("⚠️ Bus Factor:", s.BusFactor, "-", s.BusRisk) + fmt.Println("🐛 Issue Health:", s.IssueHealth) + fmt.Println("📥 PR Health:", s.PRHealth) fmt.Println("🔥 Activity:", s.ActivityLevel) }