Skip to content

Commit 955608c

Browse files
committed
refactor(github): extract developer score into pure helper
1 parent 4538996 commit 955608c

2 files changed

Lines changed: 134 additions & 10 deletions

File tree

lib/github.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
fetchOrgMembers,
1717
getOrgDashboardData,
1818
getWrappedData,
19+
computeDeveloperScore,
1920
} from './github';
2021
import type { ContributionCalendar } from '../types';
2122

@@ -1305,3 +1306,96 @@ describe('getWrappedData', () => {
13051306
expect(body.variables.to).toBe('2024-12-31T23:59:59Z');
13061307
});
13071308
});
1309+
1310+
describe('computeDeveloperScore', () => {
1311+
it('handles all-zero inputs', () => {
1312+
const score = computeDeveloperScore({
1313+
repos: 0,
1314+
followers: 0,
1315+
stars: 0,
1316+
contributions: 0,
1317+
longestStreak: 0,
1318+
});
1319+
expect(score).toBe(0);
1320+
});
1321+
1322+
it('calculates perfect 100 for exact saturation threshold values', () => {
1323+
const score = computeDeveloperScore({
1324+
repos: 50,
1325+
followers: 50,
1326+
stars: 100,
1327+
contributions: 400,
1328+
longestStreak: 50,
1329+
});
1330+
expect(score).toBe(100);
1331+
});
1332+
1333+
it('clamps the developer score to a maximum of 100 under excess inputs', () => {
1334+
const score = computeDeveloperScore({
1335+
repos: 1000,
1336+
followers: 500,
1337+
stars: 9999,
1338+
contributions: 10000,
1339+
longestStreak: 365,
1340+
});
1341+
expect(score).toBe(100);
1342+
});
1343+
1344+
it('correctly calculates developer score under realistic normal values with rounding', () => {
1345+
const score = computeDeveloperScore({
1346+
repos: 10, // 10 * 0.5 = 5 pts
1347+
followers: 8, // 8 * 0.5 = 4 pts
1348+
stars: 15, // 15 * 0.2 = 3 pts
1349+
contributions: 120, // 120 / 20 = 6 pts
1350+
longestStreak: 12, // 12 * 0.2 = 2.4 pts
1351+
}); // Total = 20.4 -> rounded to 20
1352+
expect(score).toBe(20);
1353+
});
1354+
1355+
it('handles individual factor saturation caps correctly', () => {
1356+
// repos saturates at 50 (25 pts), followers saturates at 50 (25 pts)
1357+
const score = computeDeveloperScore({
1358+
repos: 100, // Caps at 25 pts
1359+
followers: 60, // Caps at 25 pts
1360+
stars: 0,
1361+
contributions: 0,
1362+
longestStreak: 0,
1363+
});
1364+
expect(score).toBe(50);
1365+
});
1366+
1367+
it('handles small fractional values and rounds to nearest integer correctly', () => {
1368+
// longestStreak of 3 yields 3 * 0.2 = 0.6 -> rounds to 1
1369+
expect(
1370+
computeDeveloperScore({
1371+
repos: 0,
1372+
followers: 0,
1373+
stars: 0,
1374+
contributions: 0,
1375+
longestStreak: 3,
1376+
})
1377+
).toBe(1);
1378+
1379+
// sum of small fractional parts: 1 repos (0.5) + 1 followers (0.5) + 1 stars (0.2) + 1 contributions (0.05) + 1 longestStreak (0.2) = 1.45 -> rounds to 1
1380+
expect(
1381+
computeDeveloperScore({
1382+
repos: 1,
1383+
followers: 1,
1384+
stars: 1,
1385+
contributions: 1,
1386+
longestStreak: 1,
1387+
})
1388+
).toBe(1);
1389+
1390+
// sum: 1 repos (0.5) + 1 followers (0.5) + 1 stars (0.2) + 1 contributions (0.05) + 2 longestStreak (0.4) = 1.65 -> rounds to 2
1391+
expect(
1392+
computeDeveloperScore({
1393+
repos: 1,
1394+
followers: 1,
1395+
stars: 1,
1396+
contributions: 1,
1397+
longestStreak: 2,
1398+
})
1399+
).toBe(2);
1400+
});
1401+
});

lib/github.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,33 @@ export function buildCommitClock(allDays: ContributionDay[]) {
728728
return dayNames.map((name, i) => ({ day: name, commits: dayTotals[i] }));
729729
}
730730

731+
export interface DeveloperScoreInput {
732+
repos: number;
733+
followers: number;
734+
stars: number;
735+
contributions: number;
736+
longestStreak: number;
737+
}
738+
739+
export function computeDeveloperScore({
740+
repos,
741+
followers,
742+
stars,
743+
contributions,
744+
longestStreak,
745+
}: DeveloperScoreInput): number {
746+
return Math.min(
747+
Math.round(
748+
Math.min(repos * 0.5, 25) +
749+
Math.min(followers * 0.5, 25) +
750+
Math.min(stars * 0.2, 20) +
751+
Math.min(contributions / 20, 20) +
752+
Math.min(longestStreak * 0.2, 10)
753+
),
754+
100
755+
);
756+
}
757+
731758
export async function getFullDashboardData(username: string, options: FetchOptions = {}) {
732759
const [profileResult, reposResult, calendarResult] = await Promise.allSettled([
733760
fetchUserProfile(username, options),
@@ -751,16 +778,19 @@ export async function getFullDashboardData(username: string, options: FetchOptio
751778
const streakStats = calculateStreak(calendarData);
752779
const totalStars = reposData.reduce((acc, repo) => acc + repo.stargazers_count, 0);
753780

754-
const developerScore = Math.min(
755-
Math.round(
756-
Math.min(profileData.public_repos * 0.5, 25) +
757-
Math.min(profileData.followers * 0.5, 25) +
758-
Math.min(totalStars * 0.2, 20) +
759-
Math.min(streakStats.totalContributions / 20, 20) +
760-
Math.min(streakStats.longestStreak * 0.2, 10)
761-
),
762-
100
763-
);
781+
// Developer Score — 5-factor weighted formula (max 100 pts)
782+
// Repos: up to 25 pts (saturates at 50 public repos)
783+
// Followers: up to 25 pts (saturates at 50 followers)
784+
// Stars: up to 20 pts (saturates at 100 total stars)
785+
// Contributions: up to 20 pts (saturates at 400 yearly contributions)
786+
// Streak: up to 10 pts (saturates at a 50-day longest streak)
787+
const developerScore = computeDeveloperScore({
788+
repos: profileData.public_repos,
789+
followers: profileData.followers,
790+
stars: totalStars,
791+
contributions: streakStats.totalContributions,
792+
longestStreak: streakStats.longestStreak,
793+
});
764794

765795
const profile = {
766796
username: profileData.login,

0 commit comments

Comments
 (0)