diff --git a/lib/scoring/engine.ts b/lib/scoring/engine.ts index 68ef9c5..ebe7e22 100644 --- a/lib/scoring/engine.ts +++ b/lib/scoring/engine.ts @@ -29,12 +29,19 @@ function rawStats(s: Signals): Stats { sho: 36 + 13 * Lg(s.total_stars_owned) + 5 * Lg(s.max_repo_stars), pas: 40 + 12 * Lg(s.prs_to_others) + 9 * Lg(s.followers), // DRI = genuine range, square-root scaled so breadth has diminishing returns: - // ~65 at one language, ~80 at ten, ~85 at fifteen. The old linear count - // saturated (8 languages already ~94, maxed at 9), letting a noisy signal own - // the card and crown every polyglot a Fantasista. - dri: 58 + 7 * Math.sqrt(s.languages), - def: 40 + 14 * Lg(s.reviews + s.issues_closed), - phy: 40 + 9 * Lg(s.total_contributions_lifetime) + 2.2 * Math.min(s.active_years, 12), + // ~51 at one language, ~75 at ten, ~83 at fifteen. Floor matches the other + // five stats (was 58, structurally out-scoring everything else and crowning + // almost every profile a CAM regardless of their actual signal shape). + dri: 40 + 11 * Math.sqrt(s.languages), + def: 40 + 15 * Lg(s.reviews + s.issues_closed), + // Lifetime volume accrues for any account with history, not just skilled + // ones, so both terms are kept modest (was 9x + 2.2x) — at that weight PHY + // saturated to 99 for anyone with a few years of steady activity and made + // Anchor/CDM the default family for almost everyone. At 5x + 0.5x, family + // spreads across Playmaker/Forward/Anchor for varied Signals (checked by + // hand via buildCard); extreme-tenure profiles can still spike PHY high + // enough to win Anchor, which is the intended outlier case, not the norm. + phy: 40 + 5 * Lg(s.total_contributions_lifetime) + 0.5 * Math.min(s.active_years, 12), }; for (const k of STATS) o[k] = clamp(Math.round(o[k]), 1, 99); return o; diff --git a/package-lock.json b/package-lock.json index e221ad3..7c2a63f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -640,9 +640,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -659,9 +656,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -678,9 +672,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -697,9 +688,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -716,9 +704,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -735,9 +720,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -754,9 +736,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -773,9 +752,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -792,9 +768,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -817,9 +790,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -842,9 +812,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -867,9 +834,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -892,9 +856,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -917,9 +878,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -942,9 +900,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -967,9 +922,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1207,9 +1159,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1226,9 +1175,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1245,9 +1191,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1264,9 +1207,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1459,9 +1399,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1479,9 +1416,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1499,9 +1433,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1519,9 +1450,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1539,9 +1467,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1559,9 +1484,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1827,9 +1749,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1847,9 +1766,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1867,9 +1783,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1887,9 +1800,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2458,9 +2368,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2475,9 +2382,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2492,9 +2396,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2509,9 +2410,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2526,9 +2424,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2543,9 +2438,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2560,9 +2452,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2577,9 +2466,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2594,9 +2480,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2611,9 +2494,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5565,9 +5445,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5589,9 +5466,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5613,9 +5487,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5637,9 +5508,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6000,9 +5868,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -6019,9 +5884,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -6038,9 +5900,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -6057,9 +5916,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -6076,9 +5932,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -6095,9 +5948,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -6114,9 +5964,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -6133,9 +5980,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -6152,9 +5996,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6177,9 +6018,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6202,9 +6040,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6227,9 +6062,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6252,9 +6084,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6277,9 +6106,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6302,9 +6128,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6327,9 +6150,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ diff --git a/tests/position-distribution.test.ts b/tests/position-distribution.test.ts new file mode 100644 index 0000000..e5d8a6b --- /dev/null +++ b/tests/position-distribution.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; +import { buildCard } from "@/lib/scoring/engine"; +import type { Signals } from "@/lib/scoring/types"; + +// Regression coverage for the DRI/PHY rebalance in lib/scoring/engine.ts: their +// raw-stat floors used to structurally out-score every other stat, so nearly +// any profile collapsed onto Playmaker/CAM (DRI's floor) or, after a partial +// fix, Anchor/CDM (PHY's log term). These tests pin the two things that +// actually matter — genuine signal shape drives family/position, and varied +// profiles don't all collapse onto one family. + +const mk = (over: Partial): Signals => ({ + login: "x", + name: "x", + avatarUrl: "", + location: null, + followers: 10, + account_age_years: 3, + public_repos: 10, + total_stars_owned: 2, + max_repo_stars: 2, + languages: 4, + recent_contributions: 50, + active_days_recent: 50, + active_years: 3, + total_contributions_lifetime: 800, + prs_to_others: 3, + reviews: 5, + issues_closed: 5, + recent_commits: 20, + recent_spike: false, + ...over, +}); + +describe("position distribution — varied signal shapes", () => { + it("a star/follower-heavy profile becomes a Forward", () => { + const card = buildCard(mk({ total_stars_owned: 3000, max_repo_stars: 1500, followers: 400 })); + expect(card.family).toBe("Forward"); + expect(["ST", "RW"]).toContain(card.position); + }); + + it("a language-breadth-heavy profile becomes a Playmaker", () => { + const card = buildCard(mk({ languages: 15 })); + expect(card.family).toBe("Playmaker"); + expect(["CM", "CAM"]).toContain(card.position); + }); + + it("a review/issue-heavy profile becomes an Anchor", () => { + const card = buildCard(mk({ reviews: 200, issues_closed: 100, prs_to_others: 20, followers: 60 })); + expect(card.family).toBe("Anchor"); + expect(["CB", "CDM"]).toContain(card.position); + }); + + it("does not collapse varied profiles onto a single family (CAM / CDM)", () => { + const profiles: Signals[] = [ + mk({}), // modest/typical + mk({ total_stars_owned: 3000, max_repo_stars: 1500, followers: 400 }), // star-magnet + mk({ languages: 15 }), // polyglot + mk({ reviews: 200, issues_closed: 100, prs_to_others: 20, followers: 60 }), // heavy-reviewer + mk({ total_contributions_lifetime: 15000, active_years: 10, account_age_years: 10 }), // long-grinder + mk({ recent_contributions: 2000, recent_commits: 1500 }), // prolific-committer + mk({ prs_to_others: 80, followers: 500 }), // connector + mk({ + followers: 1, + total_stars_owned: 0, + max_repo_stars: 0, + languages: 1, + reviews: 0, + issues_closed: 0, + prs_to_others: 0, + recent_contributions: 5, + total_contributions_lifetime: 50, + active_years: 1, + }), // low-everything + mk({ + followers: 100, + total_stars_owned: 100, + max_repo_stars: 50, + languages: 6, + reviews: 30, + issues_closed: 20, + prs_to_others: 15, + recent_contributions: 400, + total_contributions_lifetime: 4000, + active_years: 5, + }), // balanced-active + mk({ total_stars_owned: 8000, max_repo_stars: 8000, followers: 20, languages: 2 }), // one-viral-repo + ]; + + const families = profiles.map((s) => buildCard(s).family); + const positions = profiles.map((s) => buildCard(s).position); + + expect(new Set(families).size).toBeGreaterThan(1); + expect(new Set(positions).size).toBeGreaterThan(2); + // no single family should own every profile + for (const fam of new Set(families)) { + expect(families.filter((f) => f === fam).length).toBeLessThan(profiles.length); + } + }); +});