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. 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, 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. 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/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(); 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({