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
Update docs for Step 6 changes; ignore local Claude Code session state
README.md / CLAUDE.md now describe: megabase whole-word matching and
rating disambiguation, the confidence-scoring rationale, the five game
sources, the games-browser board, and the web-search account-discovery
fallback. Also merges in the Step 7/8 roadmap entries added upstream.
.gitignore: .claude/ (Claude Code's local permission/session state,
not project source).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4.`lookup.broadcasts.find_games(name, brave_api_key)` → PGN strings from Lichess broadcast rounds mentioning the player (optional, needs an API key)
48
+
5.`analysis.openings.analyse_openings(pgn_strings, player)` + `analysis.stats.analyse_stats(pgn_strings, player)` → dicts, each opening-line row carrying a capped list of the underlying games (with a URL when one exists)
-**All analysis functions are pure** — they accept `list[str]` (PGN strings) and return dicts. No I/O. CLIs and `dossier/report.py` handle all sourcing.
47
-
-**Player name matching is case-insensitive substring** — `"smith"` matches `"Smith, John"`. This applies in both `scraper._HEADER_MAP` normalisation and `megabase.query` SQL `LIKE` queries.
53
+
-**All analysis functions are pure** — they accept `list[str]` (PGN strings) and return dicts. No I/O. CLIs and `dossier/report.py`/`pipeline/runner.py` handle all sourcing.
54
+
-**Player name matching is token-based, not plain substring** — `analysis.openings._name_matches(player, header_name)` requires every word in `player` to appear somewhere in `header_name`, so a tournament entry's truncated/compound surname (e.g. "Lagrave, Maxime" vs. a PGN's "Vachier-Lagrave, Maxime") still matches, while a same-surname different-person doesn't. `megabase.query.get_player_games` does the equivalent as ANDed SQL `LIKE` clauses, one per name token.
55
+
-**Tournament entry names get title-stripped before any matching** — `pipeline.resolver._strip_title` removes a leading FIDE/USCF title ("GM Vachier-Lagrave, Maxime" → "Vachier-Lagrave, Maxime"); titles never appear in PGN headers or usernames and would otherwise poison every downstream match.
56
+
-**Never split multi-game PGN text with a `\n(?=\[)` regex** — it splits between every header *line*, not between games, since a normal header block has no blank lines between tags. Always use `pgnutil.split_pgn_games()`, which round-trips through `chess.pgn.read_game()`.
48
57
-**`scraper.parse_entry_list`** requires at least one recognised column header from `_HEADER_MAP` before accepting a table, to skip nav/layout tables.
49
58
-**chess.com has no search API** — `lookup.chesscom.guess_usernames(name)` generates candidates from `Last, First` / `First Last` patterns and `find_profile()` tries each until one resolves.
50
-
-**Lichess rate limiting** — `lookup.lichess` sleeps 1s before game fetch requests.
59
+
-**Lichess rate limiting** — `lookup.lichess` sleeps 1s before game fetch requests. Its autocomplete endpoint is `/api/player/autocomplete` (not `/api/users/autocomplete`, which 404s) and 400s on a literal comma in the search term — `search()` strips it.
60
+
-**Lichess profile enrichment is opt-in per candidate** — `/api/player/autocomplete` returns no rating/country/real-name data, only `/api/user/{username}` does, and only if the account owner filled it in. `resolve_lichess` fetches the full profile for just the top 2 name-ranked candidates to bound request volume.
51
61
-**megabase index** is built once from a ChessBase PGN export (`python -m megabase.indexer mega.pgn`) and then queried read-only.
62
+
-**megabase name matching is whole-word, not substring, at the SQL level** — `megabase.query.get_player_games` wraps White/Black in comma delimiters and matches `LIKE '%,token,%'`; a bare `LIKE '%token%'` matches a short token *inside* an unrelated word (e.g. `"an"` inside `"Anderson"`), which on an 11M-game database turns one token into millions of false positives, not a rare edge case. Passing `rating` additionally drops candidates whose matched side's Elo (read straight from the PGN's `WhiteElo`/`BlackElo`) is more than `rating_tolerance` points off — even whole-word matching can't disambiguate two different real people who share a common name.
63
+
-**Games with no public URL get a local interactive board** — `pipeline.runner._ensure_game_links` collects them into one games-browser page per player at `<output_dir>/games/<slug>/index.html` (game list + click-to-load traversable board, pieces from `python-chess`'s bundled Cburnett SVG set — the same one Lichess's default theme uses) and injects a `GameURL` header pointing at that game's anchor; a real `GameURL`/`Link`/`Site` URL already on the PGN (Lichess, chess.com, Lichess broadcasts) is left alone.
64
+
-**Lichess broadcasts can't be searched by player name** — only by broadcast/tournament title (`/api/broadcast/search`) or by organizer username (`/api/broadcast/by/{username}`, not useful for a competitor). `lookup.broadcasts` works around this via a general web search (Brave Search API) for `"<name>" lichess.org/broadcast`, then fetches whatever round(s) it finds.
52
65
53
66
### Supported tournament sites
54
67
@@ -62,16 +75,21 @@ Full URLs are auto-detected; `--site` is only needed for ID shorthands.
62
75
### Step 6 pipeline details
63
76
64
77
`pipeline/resolver.py`:
65
-
-`_similarity(a, b)` — case-insensitive `SequenceMatcher` ratio on normalised strings
66
-
-`resolve_lichess(name)` → `(username, "high"|"low"|None)` — calls `lookup.lichess.search()`, scores top result against player name; `>=0.55` → high, `>=0.30` → low
67
-
-`resolve_chesscom(name)` → `(username, "high"|"low"|None)` — tries `guess_usernames()` patterns; first 2 hits → high, later → low
78
+
-`_strip_title(name)` — strips a leading FIDE/USCF title
79
+
-`resolve_lichess(name, rating=None, search_api_key=None)` / `resolve_chesscom(name, rating=None, search_api_key=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), and account country (weight 0.2, "US-preferred" since both supported tournament sites are US-based); missing signals are dropped from the weighted average rather than penalising the candidate. `score >= 0.55` → high, `>= 0.30` → low, else rejected (`None`).
80
+
- Lichess: scores all `search()` candidates (cheap name-only pass first, then fetches full profiles for just the top 2)
81
+
- chess.com: stops at the first guess that resolves to a real profile (guess specificity — "firstlast" vs. a bare "first" — stands in for name similarity, since every guess is mechanically derived from the name)
82
+
- Both: if `search_api_key` is given and the above didn't already reach high confidence, also try `find_usernames_via_search()` (Brave Search for `"<name>" lichess.org/@` or `chess.com/member`) 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
68
83
69
84
`pipeline/runner.py`:
70
85
-`run_pipeline(tournament, ...)` — full orchestration; returns `list[Path]` of written files
71
-
- Writes `<output_dir>/<slug>.md` per player and `combined.md` in markdown mode
72
-
- Low-confidence profiles get `"confidence": "low"` injected before being passed to `build_dossier()`
86
+
- Per player, pulls games from up to 5 sources in order: megabase, Lichess games, Lichess studies, chess.com games, Lichess broadcasts (only if `search_api_key` is set)
87
+
- Writes `<output_dir>/<slug>.{html,md,json}` per player and a combined file in html/markdown mode
88
+
-`exclude` filters the scraped player list by substring before the loop (e.g. to skip your own entry)
89
+
-`match_score`/`match_reasons` from the resolver get injected into each profile dict alongside `confidence`, and rendered next to it in the report
90
+
-`_ensure_game_links()` runs on the collected PGNs (html/markdown modes only) before `build_dossier()`, generating the per-player games-browser page for anything without a public URL
73
91
74
92
### Roadmap
75
93
76
-
- Steps 1–6 are complete.
77
-
- Remaining: MegaDatabase integration into Step 6, combined PDF output.
94
+
- Steps 1–6 are complete, including MegaDatabase integration.
0 commit comments