fix: wrong player highlighted and wrong pairings due to FIDE ID / SNo mismatch
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 8s

- tracker.py _parse_art0_page: stop at first 6-10 digit number as FIDE ID;
  national IDs (also 6-10 digits) no longer overwrite it. FED/rating now
  scanned forward past the national ID column.
- parser.py fetch_tournament: fuzzy start-list name matching no longer
  overrides starting_sno from standings column 0. Fuzzy match is kept only
  for rating lookup. Exact name match still may override SNo (reliable).
- tracker.py rescan_existing_tournaments: after each art=0 re-fetch, update
  player_sno in subscriptions whose FIDE ID appears at a different SNo than
  stored (corrects drift caused by the old parsing bug).

Root cause: national ID of Gorshkov (SNo=26 in Odintsovo Blitz) matched
Vrubel's FIDE ID in the cache; subscription was created with wrong player_sno=26.
DB already patched directly: Vrubel→sno=39, Aslanbekov→sno=40.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
vrubel 2026-06-22 11:05:43 +00:00
parent 8058080c3a
commit e64e436b6f
2 changed files with 43 additions and 21 deletions

View file

@ -413,27 +413,27 @@ def fetch_tournament(url: str) -> dict:
for s in standings:
sname = _normalize_name(s['name'])
# Try exact match first
# Exact name match: safe to trust both SNo and rating from start list.
real_sno = name_to_sno.get(sname)
if real_sno is None:
# Fuzzy: find best partial match (first+last name overlap)
if real_sno:
s['starting_sno'] = real_sno
s['rating'] = start_players[real_sno].get('rating', s.get('rating', 0))
else:
# Fuzzy match is used ONLY for rating lookup — never overrides starting_sno.
# (starting_sno from column 0 of art=4 standings is already the correct
# starting number; overwriting it with a fuzzy-matched SNo corrupts
# the opponent history and leads to wrong pairings.)
sname_parts = set(sname.split())
best_sno = None
best_overlap = 0
best_sno, best_overlap = None, 0
for sl_name, sl_sno in name_to_sno.items():
sl_parts = set(sl_name.split())
overlap = len(sname_parts & sl_parts)
if overlap > best_overlap:
best_overlap = overlap
best_sno = sl_sno
real_sno = best_sno
best_overlap, best_sno = overlap, sl_sno
if best_sno and best_sno in start_players:
s['rating'] = start_players[best_sno].get('rating', s.get('rating', 0))
s['starting_sno'] = real_sno if real_sno else s.get('starting_sno', s['rank'])
# Get real rating from start list
if real_sno and real_sno in start_players:
s['rating'] = start_players[real_sno].get('rating', s.get('rating', 0))
elif 'rating' not in s:
if 'rating' not in s:
s['rating'] = 0
# Keep original results from standings as backup (for byes/forfeits)