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
2 changes: 2 additions & 0 deletions components/ScoutReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
GitPullRequest,
Infinity as InfinityIcon,
Languages,
Leaf,
type LucideIcon,
Shield,
Star,
Expand All @@ -34,6 +35,7 @@ const PLAYSTYLE_ICONS: Record<string, LucideIcon> = {
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.
Expand Down
2 changes: 1 addition & 1 deletion lib/github/samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 4 additions & 8 deletions lib/github/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>();
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.
Expand Down
1 change: 1 addition & 0 deletions lib/scoring/playstyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion lib/scout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 5 additions & 3 deletions tests/playstyles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand Down