Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions internal/analyzer/recruiter_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func BuildRecruiterSummary(
maturityLevel string,
busFactor int,
busRisk string,
openIssues int,
prMergeRate float64,
) RecruiterSummary {

activity := "Low"
Expand All @@ -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,
Expand All @@ -45,6 +61,8 @@ func BuildRecruiterSummary(
MaturityLevel: maturityLevel,
BusFactor: busFactor,
BusRisk: busRisk,
IssueHealth: issueHealth,
PRHealth: prHealth,
ActivityLevel: activity,
}
}
72 changes: 72 additions & 0 deletions internal/analyzer/recruiter_summary_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
4 changes: 2 additions & 2 deletions internal/output/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion internal/output/compare_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,4 @@ func scoreClass(score int) string {
}
}

// removed unused helpers: HasLanguages, joinClasses
// removed unused helpers: HasLanguages, joinClasses
22 changes: 11 additions & 11 deletions internal/output/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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},
},
)

Expand All @@ -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},
},
)

Expand Down Expand Up @@ -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))
}
}
}
2 changes: 2 additions & 0 deletions internal/output/recruiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}