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
10 changes: 5 additions & 5 deletions src/lib/repo-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -27,23 +27,23 @@ 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;
const normalized = 1 - openIssues / 20;
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;
const normalized = 1 - (days - 7) / (30 - 7);
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";
Expand Down
87 changes: 86 additions & 1 deletion test/repo-health-scoring.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
Loading