You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add account-recency signal and stale-account confidence cap
Lichess and chess.com both expose a last-activity timestamp on the
same profile call already made (seenAt / last_online respectively),
so this is free to add: a candidate with hundreds of games is still
weak evidence of being a tournament entrant's *current* account if
none of those games happened in the last several years.
- lookup/lichess.py, lookup/chesscom.py: expose last_active (ISO date)
on _slim_profile.
- pipeline/resolver.py: _recency_score weights it into the composite
score (full credit within 90 days, tapering to zero by ~2 years of
inactivity), and _composite_score returns a third hard-cap flag
(recency_ok, alongside the existing games-count and rating ones)
that forces confidence to "low" once an account's been inactive for
5+ years, regardless of how well every other signal matches. Unknown
activity doesn't penalize, matching the existing caps' pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-**chess.com username guessing must drop middle names** — `lookup.chesscom.guess_usernames` splits `"Last, First Middle"` on the comma, but without trimming to just the first given name, every guess embeds a literal space (e.g. `"john derekheinichen"`) and 404s, silently breaking chess.com matching for anyone with a middle name on the entry list.
75
75
-**Resolver confidence factors in games played, not just name/rating/country** — `pipeline.resolver._composite_score` adds an optional games-count signal (`lookup.lichess`/`lookup.chesscom``_slim_profile` expose `games_count`); more games raises confidence, scaling up to `_GAMES_FOR_FULL_SCORE` (50). Separately, `_confidence_for` hard-caps confidence at `"low"` when games_count is known and below `_MIN_GAMES_FOR_HIGH` (5) — a same-name account with almost no games is too thin a sample to call "high confidence" even if name/rating/country line up perfectly, since that combination is just as consistent with a different person. chess.com's `_slim_profile` also now exposes `real_name` (the account's real-name field, distinct from `display_name` which falls back to the username) — a genuine real-name match is direct evidence and can upgrade a late, generic-looking username guess to high confidence, the same way Lichess's `real_name` already could.
76
76
-**A catastrophic rating mismatch is a second hard confidence cap, not just a scoring penalty** — a real production case exposed this: a generic guess ("john") landed on an unrelated stranger's real chess.com account, and because that account happened to have thousands of games and a "preferred" country, the composite score nearly reached "high" despite an ~1000-point rating gap. `_composite_score` now also returns `rating_ok` (`False` only when a rating comparison was actually made *and* came back fully clamped to the 0.0 floor — i.e. the gap is at or past `_RATING_TOLERANCE`), and `_confidence_for` caps confidence at `"low"` whenever `rating_ok` is `False`, the same way it does for a too-small `games_count`. No rating data at all leaves `rating_ok``True` — absence of evidence isn't evidence of a mismatch.
77
+
-**Account recency is a third signal and hard cap, distinct from games_count** — a lot of games proves the account is a real, active identity, but says nothing about whether it's *still* this player's active account: hundreds of games from 5+ years ago is weak evidence for a tournament entrant playing today. `lookup.lichess`/`lookup.chesscom``_slim_profile` now expose `last_active` (an ISO date, from Lichess's `seenAt` or chess.com's `last_online` — both already present on the same profile call, no extra request) — `_recency_score` scales it from 1.0 (active within `_RECENCY_FULL_SCORE_DAYS`, 90 days) down to 0.0 (inactive `_RECENCY_ZERO_SCORE_DAYS`, ~2 years, or more), and `_composite_score` returns a third hard-cap flag `recency_ok` (`False` only when `last_active` is known *and* older than `_STALE_ACCOUNT_DAYS`, 5 years) that `_confidence_for` treats the same way as `rating_ok`/too-few-games. No `last_active` data leaves `recency_ok``True`.
77
78
-**chess.com guessing scores every guess and keeps the best, not just the first hit** — previously `_resolve_chesscom_by_guessing` stopped at the first guess that resolved to a real profile, so a weak, coincidental match early in the guess list (e.g. a bare first name colliding with an unrelated stranger) could block a later, better-evidenced guess from ever being considered. Now it behaves like the Lichess candidate-ranking path: try every guess, score each hit, keep the best.
78
79
- **The country signal cross-checks actual FIDE nationality instead of assuming every entrant is US-based** — `lookup.uscf.get_fide_country(uscf_id)` scrapes the public USCF MSA member page (`https://www.uschess.org/msa/MbrDtlMain.php?<uscf_id>`, keyed by the `uscf_id` already scraped from the entry list — no name-based lookup or disambiguation needed) for the entrant's "FIDE Country" field, and converts the FIDE/IOC-style 3-letter federation code to ISO alpha-2 via `_FIDE_TO_ISO2` for comparison against a candidate's Lichess/chess.com country. `pipeline.runner.run_pipeline` looks this up once per player and passes it to both resolvers as `fide_country`; `_country_score` uses it when available and only falls back to the old blanket "US-preferred" heuristic when it isn't (most club-level players have no FIDE record at all). This was a real, demonstrated bug, not a hypothetical: the blanket US bias caused a correct non-US candidate with a 0.90 real-name match to lose to a wrong US candidate with only a 0.65 match, purely because of nationality — confirmed fixed against live data (score flipped from 0.75-wrong/0.64-correct to 0.53-wrong/0.93-correct once the actual FIDE nationality was known).
79
80
@@ -90,7 +91,7 @@ Full URLs are auto-detected; `--site` is only needed for ID shorthands.
90
91
91
92
`pipeline/resolver.py`:
92
93
-`_strip_title(name)` — strips a leading FIDE/USCF title
93
-
-`resolve_lichess(name, rating=None, searxng_url=None, fide_country=None)` / `resolve_chesscom(name, rating=None, searxng_url=None, fide_country=None)` → `(username, "high"|"low"|None, score, reasons)` — each candidate is scored on name/handle similarity (weight 0.5), rating closeness to `rating` when available (weight 0.3, tighter tolerance if it's a FIDE rating rather than an online blitz/rapid one), account country (weight 0.2 — see below), and games played (weight 0.2, scaling up to `_GAMES_FOR_FULL_SCORE`); missing signals are dropped from the weighted average rather than penalising the candidate. `score >= 0.55` → high, `>= 0.30` → low, else rejected (`None`) — except `_confidence_for` overrides a would-be "high" down to "low" when the account has fewer than `_MIN_GAMES_FOR_HIGH` games on record, or when the rating comparison came back catastrophically bad (`rating_ok is False`), regardless of score (see the games-count and rating-mismatch design notes above).
94
+
- `resolve_lichess(name, rating=None, searxng_url=None, fide_country=None)` / `resolve_chesscom(name, rating=None, searxng_url=None, fide_country=None)` → `(username, "high"|"low"|None, score, reasons)` — each candidate is scored on name/handle similarity (weight 0.5), rating closeness to `rating` when available (weight 0.3, tighter tolerance if it's a FIDE rating rather than an online blitz/rapid one), account country (weight 0.2 — see below), games played (weight 0.2, scaling up to `_GAMES_FOR_FULL_SCORE`), and account recency (weight 0.15, scaling from `_RECENCY_FULL_SCORE_DAYS` down to `_RECENCY_ZERO_SCORE_DAYS` — see below); missing signals are dropped from the weighted average rather than penalising the candidate. `score >= 0.55` → high, `>= 0.30` → low, else rejected (`None`) — except `_confidence_for` overrides a would-be "high" down to "low" when the account has fewer than `_MIN_GAMES_FOR_HIGH` games on record, when the rating comparison came back catastrophically bad (`rating_ok is False`), or when the account's been inactive for `_STALE_ACCOUNT_DAYS`+ (`recency_ok is False`), regardless of score (see the games-count, rating-mismatch, and recency design notes above).
94
95
- Lichess: scores all `search()` candidates (cheap name-only pass first, then fetches full profiles for just the top 2)
95
96
- chess.com: scores every guess that resolves to a real profile and keeps the best, on guess specificity ("firstlast" vs. a bare "first" stands in for name similarity, since every guess is mechanically derived from the name) *unless* the account has a genuine `real_name` on file, in which case an actual name-similarity score is used instead when it's higher
96
97
- Both: if `searxng_url` is given and the above didn't already reach high confidence, also try `find_usernames_via_search()` (SearXNG search for the player's name plus `lichess.org` or `chess.com`) and keep whichever candidate scores best — catches a personalized handle with no relation to the player's name (e.g. Magnus Carlsen's real Lichess account is the pseudonymous `DrNykterstein`), findable only via the account's linked real name, which neither Lichess's username-only autocomplete nor any mechanical chess.com guess would ever surface. **This path needs a running SearXNG instance** (`$SEARXNG_URL` / `--searxng-url`) — without one, personalized handles like this are simply not discoverable; see the SearXNG setup note below.
0 commit comments