diff --git a/src/lib/repo-health.ts b/src/lib/repo-health.ts index f01494bc..d35691ad 100644 --- a/src/lib/repo-health.ts +++ b/src/lib/repo-health.ts @@ -8,13 +8,13 @@ function clamp(n: number, min: number, max: number): number { return Math.min(max, Math.max(min, n)); } -function scoreCommitFrequency(commits30d: number): number { +export function scoreCommitFrequency(commits30d: number): number { // 10+ commits => full 25 points; linear below const normalized = clamp(commits30d / 10, 0, 1); return normalized * 25; } -function scorePrMergeRate(rate: number): number { +export function scorePrMergeRate(rate: number): number { // rate is already 0-1 return clamp(rate, 0, 1) * 25; } @@ -27,7 +27,7 @@ export function scoreAvgPrOpenTimeHours(avgHours: number): number { return clamp(normalized, 0, 1) * 20; } -function scoreOpenIssuesCount(openIssues: number): number { +export function scoreOpenIssuesCount(openIssues: number): number { // 0 issues => full 15; 20+ => 0; linear in between if (openIssues <= 0) return 15; if (openIssues >= 20) return 0; @@ -35,7 +35,7 @@ function scoreOpenIssuesCount(openIssues: number): number { return clamp(normalized, 0, 1) * 15; } -function scoreDaysSinceLastCommit(days: number): number { +export function scoreDaysSinceLastCommit(days: number): number { // <7 days => full 15; 7-30 => scale down linearly; >30 => 0 if (days <= 7) return 15; if (days >= 30) return 0; @@ -43,7 +43,7 @@ function scoreDaysSinceLastCommit(days: number): number { return clamp(normalized, 0, 1) * 15; } -function gradeForScore(score: number): RepoHealthScore["grade"] { +export function gradeForScore(score: number): RepoHealthScore["grade"] { if (score >= 70) return "green"; if (score >= 40) return "yellow"; return "red"; diff --git a/test/repo-health-scoring.test.ts b/test/repo-health-scoring.test.ts index c2c57ad8..8f4eb746 100644 --- a/test/repo-health-scoring.test.ts +++ b/test/repo-health-scoring.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { scoreAvgPrOpenTimeHours, computeHealthScore } from '../src/lib/repo-health'; +import { scoreAvgPrOpenTimeHours, computeHealthScore, scoreCommitFrequency, scorePrMergeRate, scoreOpenIssuesCount, scoreDaysSinceLastCommit } from '../src/lib/repo-health'; import type { RepoHealthSignals } from '../src/types/repo-health'; describe('gradeForScore', () => { @@ -98,3 +98,88 @@ describe('scoreAvgPrOpenTimeHours', () => { expect(scoreAvgPrOpenTimeHours(-10)).toBe(20); }); }); + +describe('scoreCommitFrequency', () => { + it('returns 0 for zero commits', () => { + expect(scoreCommitFrequency(0)).toBe(0); + }); + + it('returns 25 for 10+ commits', () => { + expect(scoreCommitFrequency(10)).toBe(25); + expect(scoreCommitFrequency(15)).toBe(25); + }); + + it('scales linearly between 0 and 10 commits', () => { + expect(scoreCommitFrequency(5)).toBe(12.5); + expect(scoreCommitFrequency(2.5)).toBe(6.25); + }); + + it('handles negative values', () => { + expect(scoreCommitFrequency(-5)).toBe(0); + }); + + it('handles non-finite values', () => { + expect(scoreCommitFrequency(NaN)).toBe(0); + }); +}); + +describe('scorePrMergeRate', () => { + it('returns 0 for 0% merge rate', () => { + expect(scorePrMergeRate(0)).toBe(0); + }); + + it('returns 25 for 100% merge rate', () => { + expect(scorePrMergeRate(1)).toBe(25); + }); + + it('scales linearly between 0 and 1', () => { + expect(scorePrMergeRate(0.5)).toBe(12.5); + }); + + it('handles values outside 0-1 range', () => { + expect(scorePrMergeRate(-0.5)).toBe(0); + expect(scorePrMergeRate(1.5)).toBe(25); + }); +}); + +describe('scoreOpenIssuesCount', () => { + it('returns 15 for 0 issues', () => { + expect(scoreOpenIssuesCount(0)).toBe(15); + }); + + it('returns 0 for 20+ issues', () => { + expect(scoreOpenIssuesCount(20)).toBe(0); + expect(scoreOpenIssuesCount(50)).toBe(0); + }); + + it('scales linearly between 0 and 20', () => { + expect(scoreOpenIssuesCount(10)).toBe(7.5); + }); + + it('handles negative values', () => { + expect(scoreOpenIssuesCount(-5)).toBe(15); + }); +}); + +describe('scoreDaysSinceLastCommit', () => { + it('returns 15 for 0 days', () => { + expect(scoreDaysSinceLastCommit(0)).toBe(15); + }); + + it('returns 15 for 7 days', () => { + expect(scoreDaysSinceLastCommit(7)).toBe(15); + }); + + it('returns 0 for 30+ days', () => { + expect(scoreDaysSinceLastCommit(30)).toBe(0); + expect(scoreDaysSinceLastCommit(100)).toBe(0); + }); + + it('scales linearly between 7 and 30 days', () => { + expect(scoreDaysSinceLastCommit(18.5)).toBe(7.5); + }); + + it('handles negative values', () => { + expect(scoreDaysSinceLastCommit(-5)).toBe(15); + }); +});