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
Fix name-score false positive from stripping word boundaries
_name_score's surname-substring floor stripped all whitespace before
comparing, so a real name like "Simran Kolagad" collapsed to
"simrankolagad" and matched entrant "Imran, Sheikh Waali" as a raw
substring despite being an unrelated person. Real names have genuine
word boundaries; only username-style candidates need the old
substring-anywhere check.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- **Name-search queries must be natural-order and unquoted, with a bare domain** — `lookup.lichess.find_usernames_via_search` / `lookup.chesscom.find_usernames_via_search` reformat the entry-list "Last, First" name to natural "First Last" order (`_natural_name_order`) before searching, and build a query like `Marek Antoni Kowalski chess.com` — no exact-phrase quotes, no path fragment (`chess.com/member`, `lichess.org/@`). Confirmed empirically against a real unresolved case: exact-phrase-quoting the raw "Last, First" order finds nothing (real pages essentially never contain that literal comma-ordered substring), and separately, appending a path fragment as query text also kills recall on real search backends (it gets tokenized as unrelated keyword noise, e.g. "member", rather than treated as a URL/site hint) — either mistake alone is enough to silently miss an otherwise easily-findable profile page. A single self-hosted SearXNG instance backed by only 1–2 actually-responding engines (others get rate-limited/CAPTCHA'd quickly on a non-residential IP) is also inherently a bit flaky call-to-call — a miss is worth a retry before concluding the account isn't findable.
79
79
-**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.
80
80
-**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.
81
+
- **`_name_score`'s surname-substring floor must respect word boundaries when the candidate is a real name, not a username** — a real production false positive: entrant "Imran, Sheikh Waali" scored a 0.65 name-match floor against total stranger "Simran Kolagad"'s chess.com `real_name`, because the substring check stripped all whitespace before comparing (`"simran kolagad"` → `"simrankolagad"`, which contains `"imran"` as a raw substring even though the names are unrelated). Usernames genuinely need the old substring-anywhere check (`"jsmith"` for "Smith, John" has no delimiter to preserve), but a `real_name`/display name with a space in it has genuine word boundaries — `_name_score` now only applies the floor there when the surname matches a *whole word* of the candidate, the same whole-word-vs-substring principle already applied to megabase queries above. Only saved from surfacing as a false "high confidence" match that time because the account also happened to be 18+ years stale (see the recency cap below) — a differently-timed collision wouldn't have been. Covered by `TestNameScore` in `tests/test_pipeline.py`.
81
82
-**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.
82
83
-**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`.
83
84
-**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.
@@ -126,6 +127,7 @@ Full URLs are auto-detected; `--site` is only needed for ID shorthands.
126
127
- Remaining: combined PDF output. A SearXNG instance still needs a card-free signup-free way to run reliably 24/7 outside a sandbox with no Docker (the one used for development here was run from source in a venv — see the setup note below) — and even with search enabled, it depends on which upstream engines happen to be responding, so this stays a best-effort discovery path, not a guarantee.
127
128
- Fixed 2026-07-10, both confirmed against real players in the recurring `WQ260708` tournament: `lookup.broadcasts.find_broadcast_round_ids` had never picked up the natural-order/unquoted/bare-domain query fix already applied to `lichess.py`/`chesscom.py`'s search (it was still exact-phrase-quoting the raw "Last, First" order and appending a `/broadcast` path fragment), silently missing real, findable rounds — and a chess.com generic single-word guess (e.g. "john") could collide with an unrelated stranger's real account and score "high" purely from country/games/recency when no rating was on file to catch it, locking out the search path entirely before it ever ran. See `_MIN_NAME_SCORE_FOR_HIGH`/`name_ok` in `pipeline/resolver.py`.
128
129
- Also done 2026-07-10: broadcast games no longer link out to the live Lichess broadcast page (folded into the local games-browser instead) and are persisted per-player so a later run doesn't lose one SearXNG happens to miss; and the resolver/broadcast-search cache described in the caching roadmap item below is implemented (`pipeline/cache.py`) — the first, highest-impact slice of that item. Still open from that same item: megabase-query caching and any change to the deliberate 1s Lichess rate-limit sleeps themselves.
130
+
- Investigated 2026-07-14 — user-reported "drop in quality" in generated reports, specifically wrong/low-quality profile matches: audited the live `dossiers/WQ260708/` output (33 players, a 2026-07-11 scan) rather than guessing, and found `_name_score`'s surname-substring floor (see the design-decision note above) producing false-positive name matches against unrelated strangers' real names once word boundaries were erased by space-stripping — fixed. The four confidence hard caps (games/rating/recency/name_ok) added 2026-07-10 are working as designed and are *not* the regression — most of what they're capping down (accounts 8–19 years inactive) is correctly-suspect evidence, not a bug; `_name_score` was the one genuinely broken input feeding them.
0 commit comments