feat(leaderboard): public profiles, handles, achievements and tier scoring

Merged
HA
2mo ago
+32612-737

Moves the leaderboard off leaderboard_participants onto a handles + public_profiles pair, adds per-profile achievements, and replaces the min()-of-axes tier rule with a weighted score.

Profiles now sit at the root of the marketing namespace (superset.sh/<handle>), with a reserved-handle list guarding real routes, a markdown view at /md/user/<handle> for agents, search and self-pinning on the board, and a Super Fights page.

Decisions worth knowing

Several of these were previously explained in code comments, which this repo does not keep — recording them here instead.

  • Cost ceilings recalibrated from [15, 9, 7, 3.5] to [2000, 750, 300, 100]. The originals sat below the 2nd percentile of the live board: the median participant spends $452 per merged agent PR, so clearing $9 would have taken 419 merges in a month, capping every account with any PR telemetry at the bottom tier. The efficient achievement was recalibrated to match ([750, 300, 100]) and CATALOG_VERSION bumped so existing profiles re-evaluate on their next publish.
  • leaderboard_participants is kept declared but unused. It is never dropped, renamed or written to here, so the rows remain a fallback until the copy is confirmed good and the table is dropped deliberately. Note it stops receiving writes as of this PR, so it goes stale immediately — it is a rollback window, not a live mirror.
  • Migrations are ordered create, then copy-and-repoint. 0107 creates the tables. 0108 copies the rows and then moves the leaderboard_daily foreign keys onto public_profiles, in that order and in one transaction — the copy has to land before the constraints move, because the new FKs validate every leaderboard_daily row against public_profiles. Keeping both in one migration means you cannot end up with the data moved but the constraints not. The FK statements are drizzle-generated; the INSERT … SELECT copy is hand-written on top.
  • Profiles are rewritten in the proxy rather than living at [lang]/[handle]. (legal) is a route group, so (legal)/[slug] already occupies "one dynamic segment under [lang]" and Next rejects a second under a different param name. Rewriting keeps the legal pages statically generated while profiles revalidate.
  • RESERVED_HANDLES decides what is a profile. A bare segment resolves to a profile only when it is not a route, a locale, or a reserved word. reserved-handles.test.ts fails the build if a new route directory under [lang] is missing from the list — without it, shipping /careers would silently 404 into a profile lookup.
  • /api/viewer is a same-origin route so the browser attaches the session cookie without CORS credentials. The marketing pages stay ISR: personalisation is a client fetch, never a per-user render.

Known gaps

Deliberately left, rather than overlooked:

  • [lang]/fight has no Lingui coverage — it renders English in every locale. It is not in ENFORCED_DIRS, so CI does not catch it.
  • public_profiles.github_handle is only written by join, so participants migrated by 0108 have it null until they next go through that flow. Their profiles show no verified GitHub link.
  • /api/viewer is a REST route rather than tRPC. It proxies to leaderboard.viewer, so no business logic lives in it, but the response shape is asserted rather than derived.
  • searchParticipants interpolates the query into an ILIKE pattern without escaping %/_, and has no minimum length.
  • The markdown profile renders bio into an agent-facing document with only https?:// stripped, so markdown structure in a bio survives.

Verification

typecheck · check:i18n (all 17 locales compile strict) · lint:fix · sherif all pass. Scoped tests pass for trpc, marketing, i18n and the desktop leaderboard.

Repo-wide bun test has 303 failures that are environmental and present independent of this change — globalThis.navigator is read-only in this Bun version, and the git-backed desktop tests need real git operations.

The migrations have not been applied against a database. NEON_API_KEY is empty in this environment, so they were generated and verified drift-free (drizzle-kit generate reports no schema changes, snapshot chain intact) but never executed. They should be applied on a Neon branch before merge.

https://claude.ai/code/session_01BFDRwxVyxSyQEf9ErJYAcA

Summary by CodeRabbit

  • New Features

    • Added public developer profiles with bios, social links, contribution graphs, achievements, tier objectives, and agent-friendly views.
    • Added leaderboard search, pinned viewer results, metric-aware filtering, and profile navigation.
    • Added achievement notifications and profile editing for bios, X handles, and websites.
    • Added “Super Fights” with dinosaur battles, combat logs, sharing, and rematches.
  • Improvements

    • Updated tier scoring and progress displays with weighted metrics and clearer objectives.
    • Expanded localized copy across supported languages.
    • Improved profile URL handling, sitemap coverage, and temporary error responses.