From f5fc6f61553d6a519bef10c659b8b0bc090a9317 Mon Sep 17 00:00:00 2001 From: Krishna Raj B Date: Sun, 16 Aug 2026 18:13:18 +0530 Subject: [PATCH 1/5] feat(scoring): add Evergreen playstyle for active years --- lib/scoring/playstyles.ts | 1 + tests/playstyles.test.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/scoring/playstyles.ts b/lib/scoring/playstyles.ts index c4771ff..9cfde97 100644 --- a/lib/scoring/playstyles.ts +++ b/lib/scoring/playstyles.ts @@ -26,6 +26,7 @@ const CATALOG: PlaystyleDef[] = [ { name: "Polyglot", icon: "languages", noun: "languages", value: (s) => s.languages, base: 5, plus: 9 }, { name: "Prolific", icon: "folder-git", noun: "public repos", value: (s) => s.public_repos, base: 30, plus: 150 }, { name: "Veteran", icon: "clock", noun: "years on GitHub", value: (s) => s.account_age_years, base: 5, plus: 12 }, + { name: "Evergreen", icon: "leaf", noun: "active years", value: (s) => s.active_years, base: 5, plus: 10 }, ]; const MAX_SHOWN = 8; diff --git a/tests/playstyles.test.ts b/tests/playstyles.test.ts index 95baf6f..aa3b223 100644 --- a/tests/playstyles.test.ts +++ b/tests/playstyles.test.ts @@ -58,6 +58,7 @@ describe("derivePlaystyles — qualifying", () => { expect(names(signals({ languages: 5 }))).toContain("Polyglot"); expect(names(signals({ public_repos: 30 }))).toContain("Prolific"); expect(names(signals({ account_age_years: 5 }))).toContain("Veteran"); + expect(names(signals({ active_years: 5 }))).toContain("Evergreen"); }); }); @@ -85,7 +86,7 @@ describe("derivePlaystyles — the elite (PlayStyle+) tier", () => { }); describe("derivePlaystyles — the shown list", () => { - // Clears all 11 base thresholds; only 8 may be shown. + // Clears all 12 base thresholds; only 8 may be shown. const everything = signals({ total_stars_owned: 600, max_repo_stars: 1_100, @@ -99,15 +100,16 @@ describe("derivePlaystyles — the shown list", () => { languages: 6, public_repos: 35, account_age_years: 6, + active_years: 6, }); - it("caps the list at 8 even when all 11 qualify", () => { + it("caps the list at 8 even when all 12 qualify", () => { expect(derivePlaystyles(everything)).toHaveLength(8); }); it("keeps an elite playstyle that the ratio sort alone would have cut", () => { // Polyglot at exactly 9 languages is elite, but only 1.8x its base — the - // WEAKEST ratio of the eleven here, so ranking by ratio alone would drop it + // WEAKEST ratio of the twelve here, so ranking by ratio alone would drop it // outside the top 8. The plus-first sort has to float it to the very top. const out = derivePlaystyles( signals({ From c4490e11a438f06708447f6c78a010d92957e6c9 Mon Sep 17 00:00:00 2001 From: Krishna Raj B Date: Mon, 17 Aug 2026 13:32:49 +0530 Subject: [PATCH 2/5] fix(scoring): map leaf icon for evergreen playstyle --- components/ScoutReport.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/ScoutReport.tsx b/components/ScoutReport.tsx index 8c691d1..958ebc0 100644 --- a/components/ScoutReport.tsx +++ b/components/ScoutReport.tsx @@ -10,6 +10,7 @@ import { GitPullRequest, Infinity as InfinityIcon, Languages, + Leaf, type LucideIcon, Shield, Star, @@ -34,6 +35,7 @@ const PLAYSTYLE_ICONS: Record = { languages: Languages, "folder-git": FolderGit2, clock: Clock, + leaf: Leaf, }; // Hide a logo/image that fails to load (e.g. a CDN miss) rather than show a broken icon. From 922f4b6fd11fd17360101444607ff72b12ffc02f Mon Sep 17 00:00:00 2001 From: Krishna Raj B Date: Mon, 17 Aug 2026 13:42:19 +0530 Subject: [PATCH 3/5] fix(scoring): calculate active_years exactly from contribution history --- lib/github/signals.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/github/signals.ts b/lib/github/signals.ts index 2cab6dd..b0d9bd7 100644 --- a/lib/github/signals.ts +++ b/lib/github/signals.ts @@ -21,14 +21,10 @@ export function signalsFromPayload(p: RawPayload, now = Date.now()): Signals { // styling/markup (CSS/HTML) — the #1 drives the card's language + logo. const rankedLanguages = rankLanguages(p.languageRepos); - const years = new Set(); - for (const r of p.repos) { - const c = yearOf(r.createdAt); - const pushed = yearOf(r.pushedAt); - if (c) years.add(c); - if (pushed) years.add(pushed); - } - const active_years = Math.min(Math.max(years.size, 1), Math.ceil(account_age_years) || 1); + const active_years = Math.max( + 1, + p.years.filter((y) => y.commits + y.prs + y.reviews + y.issues + y.restricted > 0).length + ); // Recent activity over the last year: every contribution type GitHub exposes, // including the private (restricted) count, so it matches the profile graph. From 057b8b1f243d89f8f7fa6adbe61c302539c1fe49 Mon Sep 17 00:00:00 2001 From: Krishna Raj B Date: Mon, 17 Aug 2026 13:44:49 +0530 Subject: [PATCH 4/5] chore(cache): bump cache version to invalidate stale active_years --- lib/scout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/scout.ts b/lib/scout.ts index e959fee..7e646b6 100644 --- a/lib/scout.ts +++ b/lib/scout.ts @@ -24,7 +24,7 @@ import type { Card } from "./scoring/types"; // Namespaced alongside gitfut:scouts:total. The version segment lets a deploy // that changes buildCard's output shape or scoring invalidate every entry at // once (bump it) instead of serving stale-shaped cards until their TTL lapses. -const CACHE_VERSION = "v2"; // v2: cards carry years + the awards cabinet +const CACHE_VERSION = "v3"; // v3: cards accurately calculate active_years const CARD_TTL_SECONDS = 120 * 60; // 2h — GitHub stats move slowly; longer TTL = fewer refetches of hot profiles under load. const normalizeLogin = (username: string) => username.trim().replace(/^@/, "").toLowerCase(); From 11d25143521e3e42c4c8ef26812e9874056a8cea Mon Sep 17 00:00:00 2001 From: Krishna Raj B Date: Mon, 17 Aug 2026 14:03:27 +0530 Subject: [PATCH 5/5] fix(testing): correct torvalds mock active_years --- lib/github/samples.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/github/samples.ts b/lib/github/samples.ts index 0e749ce..bc53599 100644 --- a/lib/github/samples.ts +++ b/lib/github/samples.ts @@ -36,7 +36,7 @@ const RAW: Signals[] = [ topLanguage: "C", recent_contributions: 3259, active_days_recent: 354, - active_years: 7, + active_years: 16, total_contributions_lifetime: 37435, prs_to_others: 0, reviews: 2,