@chessceo/mcp 0.31.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -42,6 +42,13 @@ const ENGINE_USAGE_DOC = loadBundledDoc("engine-usage.md", "Engine usage guide")
42
42
  const PREP_STRATEGY_DOC = loadBundledDoc("prep-strategy.md", "Prep strategy guide");
43
43
  const PREP_FILES_DOC = loadBundledDoc("prep-files-guide.md", "Prep files guide");
44
44
  const PGN_AUTHORING_DOC = loadBundledDoc("pgn-authoring.md", "PGN authoring guide");
45
+ // Reference PGNs authored by a strong human coach. LLM pulls these when
46
+ // it wants to see the commentary style, NAG discipline, and annotation
47
+ // density we want it to hit. Kept as raw PGN so the LLM can parse them
48
+ // against its own understanding of the game (comments, arrows, NAGs
49
+ // all intact) — not summarised into English.
50
+ const EXAMPLE_OVERVIEW_PGN = loadBundledDoc("examples/italian-fried-liver.pgn", "Italian Fried Liver overview example");
51
+ const EXAMPLE_REPERTOIRE_PGN = loadBundledDoc("examples/najdorf-6-f4-white.pgn", "Najdorf 6.f4 White repertoire example");
45
52
  // ── HTTP ────────────────────────────────────────────────────────────
46
53
  //
47
54
  // Two auth flavours coexist:
@@ -80,8 +87,14 @@ const AUTHED_TOOLS = new Set([
80
87
  "set_tag",
81
88
  "apply_mutations",
82
89
  "auto_evaluate",
90
+ "auto_evaluate_status",
91
+ "auto_evaluate_cancel",
83
92
  "quote_engine_eval",
84
93
  "predict_human_move",
94
+ "prepare_opponent",
95
+ "get_prep_position",
96
+ "list_prep_sessions",
97
+ "delete_prep_session",
85
98
  ]);
86
99
  function isAuthedToolCall(body) {
87
100
  if (!body || typeof body !== "object")
@@ -188,55 +201,80 @@ const TOOLS = [
188
201
  },
189
202
  },
190
203
  {
191
- name: "get_player_preparation",
192
- description: "For a given player, colour and starting position, return both the moves the player actually chose (frequency + win rate) and the underlying games. Position is specified either as a move sequence in SAN (`line`) or a raw FEN. Use `line` iteratively to walk the opening tree: call once with empty `line`, pick a move, call again with `line` extended by that move, etc.\n\n" +
193
- "GROUNDING: every claim about the opponent's repertoire must trace back to this tool's output. Don't assert 'they play sharply' or 'they hate isolated queen pawn' without pointing at the actual game counts / win rates in the response. Don't invent 'the opponent typically plays X' check first. Compute is cheap: run this on more branches instead of pattern-matching from a chess book.\n\n" +
194
- "AUTO-EVAL: if a cloud combo instance is running, the response includes `.eval` with a compact Stockfish + Lc0 read at the requested position (top move + score + PV) and the corresponding NAG. Do NOT fire a separate cloud_analyse for the same FEN — the eval is right there.\n\n" +
195
- "Reading the response CRITICAL:\n" +
196
- " Win % is one weight, not a verdict. Recommend 1.b3 over 1.d4 because 60% > 50% is wrong. Sample size matters (3 games at 66% is noise; 300 at 55% is signal); avgWhite / avgBlack per move show the rating context (a big score often means a rating gap, not repertoire truth).\n" +
197
- " Prep is symmetric information both sides see the same history. Assume the opponent knows the weakness you spotted; a weak opponent won't patch it, a strong or improving one already has (but structural weaknesses like 'bad in Catalan structures' hold anyway).\n" +
198
- "• Recency > career. The last 12-24 months dominate. This endpoint's compact/LLM view deliberately omits per-move `fashionScore` at the individual level it's trailing noise. The general DB endpoint (`get_position_stats`) keeps it, where it's real fashion signal (what the top field is playing this month).\n" +
199
- " Opponent will deviate early. Prep is a tree, not a line cover the 2 most likely replies at each real branching point, not one 20-move line.\n" +
200
- "• Surprise is a scalpel. Don't tell a lifelong 1.e4 player to switch to 1.d4 — meta-signal screams prep. Rare secondary lines within the user's existing repertoire (e.g. 6.Bc4 instead of usual 6.Bg5 vs the Najdorf) are where surprise is real.\n\n" +
201
- "For the full guide call the `read_prep_strategy_guide` tool.",
204
+ name: "prepare_opponent",
205
+ description: "Create a prep SESSION combining games from one or more sources FIDE database, Chess.com account, Lichess account with optional filters (colour, date range, time control). Returns a session `token` you pass to `get_prep_position` to query stats at any position within that filtered corpus.\n\n" +
206
+ "This is the main opponent-prep tool. Use it whenever a user asks 'prep me against X' call once with the right sources+filters, then walk the tree with `get_prep_position(session_token, ...)`. Sessions are cached on the server (list existing ones with `list_prep_sessions` to avoid rebuilding).\n\n" +
207
+ "SOURCES (1-10 per call, combined into one gameset):\n" +
208
+ "- `fide`needs `fideId`. Optional filters: `color`, `startMonth`/`endMonth`, `timeControl` (`classical`|`rapid`|`blitz`), `excludeOnline`.\n" +
209
+ "- `chesscom` needs `username`. Filters: `color`, `startMonth`/`endMonth` (**required** for chesscom/lichess), `timeControl`.\n" +
210
+ "- `lichess`needs `username`. Same filters as chesscom; `timeControl` also accepts `bullet` on Lichess.\n\n" +
211
+ "Multi-source example: one player with both a FIDE ID and a Lichess account two sources in one call, all their games combined into one session.\n\n" +
212
+ "GROUNDING: every claim about the opponent's repertoire must trace back to a `get_prep_position` call on this session. Don't assert 'they play sharply' or 'they hate isolated queen pawn' without pointing at actual game counts / win rates in the response. Prep is a two-player game — see `read_prep_strategy_guide` before recommending an opening plan.",
202
213
  inputSchema: {
203
214
  type: "object",
204
215
  properties: {
205
- fide_id: { type: "integer", description: "FIDE ID from search_player." },
206
- color: {
207
- type: "string",
208
- enum: ["white", "black"],
209
- description: "Which colour the player is analysed with.",
210
- },
211
- file_id: {
212
- type: "string",
213
- description: "Prep file id to address the position from. Combine with `node_id` — the server derives the FEN from the tree, so you don't paste a FEN string. **Prefer this over `fen`/`line`/`moves` when a prep file is open.**",
214
- },
215
- node_id: {
216
- type: "string",
217
- description: "Node id inside `file_id` whose position to query. Root is 'r'. When set, overrides `fen`/`line`/`moves`.",
218
- },
219
- line: {
220
- type: "string",
221
- description: "Move sequence in SAN from the starting position, space-separated, no move numbers required. Example: 'e4 e5 Nf3'. Leave empty for startpos. Alias: `moves` (same thing). Only used if `node_id` is not set.",
222
- },
223
- moves: {
224
- type: "string",
225
- description: "SAN moves to apply on top of `fen` (or on top of startpos if no fen). Same shape as `line`. Wins over `line` if both are set. Only used if `node_id` is not set.",
226
- },
227
- fen: {
228
- type: "string",
229
- description: "Starting position as FEN. Combine with `moves` to walk from there, or use alone. Only used if `node_id` is not set.",
230
- },
231
- limit: {
232
- type: "integer",
233
- minimum: 1,
234
- maximum: 10,
235
- description: "Number of games to return (max 10 per request; page with offset).",
216
+ sources: {
217
+ type: "array",
218
+ minItems: 1,
219
+ maxItems: 10,
220
+ description: "1-10 game sources, all combined into one filtered session.",
221
+ items: {
222
+ type: "object",
223
+ properties: {
224
+ type: { type: "string", enum: ["fide", "chesscom", "lichess"], description: "Source type." },
225
+ fide_id: { type: "integer", description: "FIDE ID (required for type='fide')." },
226
+ username: { type: "string", description: "Platform username (required for type='chesscom'/'lichess')." },
227
+ color: { type: "string", enum: ["white", "black"], description: "Filter: only games where this side is played by the source player. Omit for both colours." },
228
+ start_month: { type: "string", pattern: "^\\d{4}/\\d{2}$", description: "Filter: games from this month onwards, format 'YYYY/MM'. **Required** for chesscom/lichess." },
229
+ end_month: { type: "string", pattern: "^\\d{4}/\\d{2}$", description: "Filter: games up to this month, format 'YYYY/MM'. **Required** for chesscom/lichess." },
230
+ time_control: { type: "string", enum: ["classical", "rapid", "blitz", "bullet"], description: "Filter: only this time control. `bullet` is Lichess-only." },
231
+ exclude_online: { type: "boolean", description: "FIDE-only: exclude online-flagged games (default false)." },
232
+ },
233
+ required: ["type"],
234
+ },
236
235
  },
236
+ },
237
+ required: ["sources"],
238
+ },
239
+ },
240
+ {
241
+ name: "get_prep_position",
242
+ description: "Query one position within a prep session created by `prepare_opponent`. Returns move statistics (frequency + win rate + last-played date per move) plus the actual games played from that position, in one call.\n\n" +
243
+ "Position input: prefer `file_id`+`node_id` when inside a prep file (server derives FEN from the tree). Otherwise pass `fen`.\n\n" +
244
+ "AUTO-EVAL: if a cloud combo instance is running, the response includes `.eval` (Stockfish + Lc0 read at the position) so you don't need a separate cloud_analyse.\n\n" +
245
+ "Reading the response — CRITICAL:\n" +
246
+ "• Win % is one weight, not a verdict. Sample size matters (3 games at 66% is noise; 300 at 55% is signal).\n" +
247
+ "• Prep is symmetric information — both sides see the same history. Assume the opponent knows the weakness you spotted.\n" +
248
+ "• Recency > career. The last 12-24 months dominate — filter your session with `start_month` if the player's repertoire shifted.\n" +
249
+ "• Opponent will deviate early. Prep is a tree — cover the 2 most likely replies at each real branching point, not one 20-move line.\n\n" +
250
+ "For the full guide call `read_prep_strategy_guide`.",
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ session_token: { type: "string", description: "Session token from `prepare_opponent`." },
255
+ file_id: { type: "string", description: "Prep file id — combine with `node_id` for tree-addressed position lookup." },
256
+ node_id: { type: "string", description: "Node id inside `file_id`. Root is 'r'. When set, overrides `fen`." },
257
+ fen: { type: "string", description: "Position as FEN. Only used if `node_id` is not set." },
258
+ limit: { type: "integer", minimum: 1, maximum: 50, description: "Games to return (default 10)." },
237
259
  offset: { type: "integer", minimum: 0 },
238
260
  },
239
- required: ["fide_id", "color"],
261
+ required: ["session_token"],
262
+ },
263
+ },
264
+ {
265
+ name: "list_prep_sessions",
266
+ description: "List the caller's active prep sessions with their tokens and metadata. Call this BEFORE `prepare_opponent` to reuse an existing session instead of rebuilding — sessions cost real backend work for chesscom/lichess (downloading months of games), so re-using saves time. Response includes source description, game count, and creation time per session.",
267
+ inputSchema: { type: "object", properties: {} },
268
+ },
269
+ {
270
+ name: "delete_prep_session",
271
+ description: "Delete one prep session by token. Free-form cleanup — sessions do expire automatically, but this is useful when you're done with one or when you want to force a rebuild after upstream data changed.",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ session_token: { type: "string", description: "Token from `list_prep_sessions` or the response of `prepare_opponent`." },
276
+ },
277
+ required: ["session_token"],
240
278
  },
241
279
  },
242
280
  {
@@ -430,14 +468,14 @@ const TOOLS = [
430
468
  },
431
469
  {
432
470
  name: "cloud_analyse",
433
- description: "Runs a synchronous ~2s analysis on the user's running combo instance and returns both Stockfish and Lc0's final read for the FEN — depth, top-N candidate moves with scores (centipawns from side-to-move POV, or mate distance), and each engine's principal variation.\n\n" +
471
+ description: "Runs a synchronous ~2s analysis on the user's running combo instance and returns both Stockfish and Lc0's final read for the FEN — depth, top-N candidate moves with scores (**scoreCp is White-POV centipawns**: +20 = White is +0.20 pawns better regardless of whose turn it is; matches the sign convention used everywhere else in this MCP, including the stored ceoEval). Mate is White-POV plies-to-mate (+5 = White mates in 5). Also returns each engine's principal variation.\n\n" +
434
472
  "GROUNDING: every claim you make about a position must trace back to actual engine output from a call in THIS session. Don't invent evaluations, don't name 'best moves' you haven't seen the engine list, don't fabricate variations that 'look plausible.' Compute is cheap — call this 5-10 times while walking a tree rather than pattern-matching from your training data. When you don't have data for the position, either run the tool or say so; don't fill the gap with chess prose the user can't distinguish from measured output.\n\n" +
435
473
  "Auto-picks the caller's only running combo instance; errors clearly if there are zero (start one first with start_cloud_engine) or more than one (destroy the extras first).\n\n" +
436
474
  "How to read the response:\n" +
437
475
  "• Stockfish is objective truth — trust it for 'does this line hold?' 'is there a tactic?' 'is this endgame drawn?' A Stockfish 0.00 means 'objectively equal', NOT 'trivial draw' — one side can still be much harder to play in practice.\n" +
438
476
  "• Lc0 is practical eval — trust it for 'which side is easier?' 'which candidate is best when Stockfish shows several as equal?' Lc0 sees long-term positional factors Stockfish's fixed search can miss.\n" +
439
477
  "• When they agree → high confidence. When they disagree → look at both scores and reason WHY (Stockfish sharply higher = tactic Lc0 missed; Lc0 higher = long-term positional edge past Stockfish's horizon). Never dismiss either — the disagreement is the signal.\n\n" +
440
- "Contempt (`contempt`) skews Lc0 (only Lc0 — Stockfish always stays objective) toward White (positive) or Black (negative). Practical range -20..+20. Use it to find non-objective 'practical' ideas or when the user needs to steer toward fighting/solid lines with a specific colour. Do NOT quote a contempt-biased eval as objective — cross-check with Stockfish.\n\n" +
478
+ "Contempt (`contempt`) skews Lc0 (only Lc0 — Stockfish always stays objective) toward White (positive) or Black (negative). Signed 0-100 strength — same scale as the web UI's ContemptStrength slider (the server multiplies by 8 to produce Lc0's internal cp bias). Typical values: ±15 for a light nudge, ±30-60 for real fighting play, ±80-100 for maximum steer. Use it to find non-objective 'practical' ideas or when the user needs to lean toward fighting/solid lines with a specific colour. Do NOT quote a contempt-biased eval as objective — cross-check with Stockfish.\n\n" +
441
479
  "Also useful: pass `moves` on top of `fen` to explore a variation without computing FENs yourself (e.g. fen='<tabiya>', moves='b4 a5 c3'). And the flip-side-to-move threat check documented in the guide is a great free trick.\n\n" +
442
480
  "For the full guide including worked examples, call the `read_engine_usage_guide` tool.\n\n" +
443
481
  "Not for casual questions — this costs real money per second. Use `get_position_stats` for anything that doesn't require deep prep.\n\n" +
@@ -468,7 +506,7 @@ const TOOLS = [
468
506
  type: "integer",
469
507
  minimum: -100,
470
508
  maximum: 100,
471
- description: "Lc0 contempt bias. 0 = objective (default). Positive favours White, negative favours Black; stay within -20..+20 in practice. Not applied to Stockfish. See engine_usage_primer prompt for when to use.",
509
+ description: "Lc0 contempt bias. Signed 0-100 strength (same scale as the web UI's ContemptStrength slider — server multiplies by 8 to get the internal cp bias). 0 = objective (default). Positive favours White, negative favours Black. Typical: ±15 light nudge, ±30-60 real fighting play, ±80-100 maximum steer. Not applied to Stockfish. See engine_usage_primer for when to use.",
472
510
  },
473
511
  },
474
512
  },
@@ -493,7 +531,7 @@ const TOOLS = [
493
531
  name: "read_prep_file",
494
532
  description: "Read one prep file. Returns a compact tree (each node carries a stable `id`, `san`, `fen`, `ply`, optional `comment`/`nags`/`annotations`/`ceoEval`, and `children`) plus tags and the `version` for optimistic locking on subsequent mutations. NO raw PGN — all edits go through the mutation tools (add_move / add_line / set_comment / set_nags / set_annotations / delete_subtree / promote_variation / set_tag), which validate SAN and structure for you.\n\n" +
495
533
  "**Node addressing.** Every node has a stable `id` — root is `'r'`, every other node is an 8-hex-char content hash derived from its parent's id + its SAN. Sibling insertions, deletions and variation promotions do NOT change any other node's id. Pass this id as `node_id` (or `parent_id` for add_move / add_line) to every mutation and engine/DB tool.\n\n" +
496
- "**Every engine/DB tool accepts `file_id`+`node_id`** (get_position_stats, cloud_analyse, describe_position, predict_human_move, prep_snapshot, get_player_preparation, quote_engine_eval). Use it whenever a file is open — the server derives the FEN from the tree, so you can't 'analyse the wrong position' by mis-typing a FEN.",
534
+ "**Every engine/DB tool accepts `file_id`+`node_id`** (get_position_stats, cloud_analyse, describe_position, predict_human_move, prep_snapshot, get_prep_position, quote_engine_eval). Use it whenever a file is open — the server derives the FEN from the tree, so you can't 'analyse the wrong position' by mis-typing a FEN.",
497
535
  inputSchema: {
498
536
  type: "object",
499
537
  properties: {
@@ -686,10 +724,11 @@ const TOOLS = [
686
724
  },
687
725
  {
688
726
  name: "auto_evaluate",
689
- description: "Walk the tree from `node_id` (default `'r'` = whole file) and populate the persistent `ceoEval` (Stockfish + Lc0 numbers) on every descendant via cloud_analyse. Requires a running cloud combo instance.\n\n" +
690
- "**Does NOT set visible NAGs.** NAG placement is your call, not the engine's an opening tree full of 0.00 positions doesn't need a `$10` (=) glyph on every move (that's just noise on the board), and a `!` on a novelty or a `?!` on a risky committal is a judgment call the engine can't make. Use this tool to persist the raw numbers on every node, then re-read the file and hand-pick NAGs on the moves where a glyph carries real signal.\n\n" +
691
- "On re-read every evaluated node carries `ceoEval: { sf: {cp, depth}, lc0: {cp, depth} }` the numbers travel with the file. Read those back with quote_engine_eval before writing prose that references engine numbers. Use `only_missing=true` (default) to skip nodes already evaluated on repeat runs.\n\n" +
692
- "Costs real money hits cloud_analyse per node. A 200-node tree at 1.5s/node = ~5 min of engine time. Runs 4 evaluations in parallel to save wall time.",
727
+ description: "Walk the tree from `node_id` (default `'r'` = whole file) and populate the persistent `ceoEval` on every descendant via cloud_analyse. Requires a running cloud combo instance.\n\n" +
728
+ "**Async job returns immediately.** Response: `{ job_id, target_count, status: 'running', estimated_seconds }`. Then poll `auto_evaluate_status(job_id)` until `done: true`. Cancel a run with `auto_evaluate_cancel(job_id)` partial progress is preserved. Do useful other work between polls (write more of the tree, walk the opponent's repertoire) the engine runs in the background.\n\n" +
729
+ "Progress is checkpointed to the prep file every 8 successfully-evaluated nodes, so a cancel / crash / MCP restart mid-run leaves the tree partially populated rather than losing everything. On MCP restart the job record disappears; re-run auto_evaluate and `only_missing=true` naturally skips what was already saved.\n\n" +
730
+ "**Does NOT set visible NAGs.** NAG placement is your call, not the engine's — an opening tree full of 0.00 positions doesn't need a `$10` (=) glyph on every move. Use quote_engine_eval on individual nodes before writing prose that references engine numbers.\n\n" +
731
+ "Costs real money — one cloud_analyse per node. A 200-node walk at default movetime is ~5 min of engine time (calls serialise on the per-combo semaphore in the backend).",
693
732
  inputSchema: {
694
733
  type: "object",
695
734
  properties: {
@@ -697,11 +736,33 @@ const TOOLS = [
697
736
  node_id: { type: "string", description: "Subtree root (default 'r' = whole file)." },
698
737
  only_missing: { type: "boolean", description: "Skip nodes that already carry a stored ceoEval (default true)." },
699
738
  movetime_ms: { type: "integer", minimum: 500, maximum: 5000, description: "Per-node cloud_analyse think time (default 1500)." },
700
- expected_version: { type: "integer" },
701
739
  },
702
740
  required: ["id"],
703
741
  },
704
742
  },
743
+ {
744
+ name: "auto_evaluate_status",
745
+ description: "Poll the status of an auto_evaluate job. Response: `{ status: 'running' | 'done' | 'cancelled' | 'error' | 'not_found', target_count, evaluated, errored, remaining, done, error?, version? }`. When `status: 'not_found'` the job either expired (kept ~15 min after completion), never existed, or the MCP restarted since it was created — re-run auto_evaluate.\n\n" +
746
+ "Typical poll cadence: every 3-5 s for small walks, every 10-30 s for large ones. Don't hammer — status is a pure in-memory read but polling doesn't speed the engine up.",
747
+ inputSchema: {
748
+ type: "object",
749
+ properties: {
750
+ job_id: { type: "string", description: "Job id from the `auto_evaluate` response." },
751
+ },
752
+ required: ["job_id"],
753
+ },
754
+ },
755
+ {
756
+ name: "auto_evaluate_cancel",
757
+ description: "Ask a running auto_evaluate job to stop as soon as its current node finishes. Whatever progress was completed before cancellation is durably saved (checkpoint on cancel). Idempotent — cancelling an already-finished job is a no-op with a clear note in the response.",
758
+ inputSchema: {
759
+ type: "object",
760
+ properties: {
761
+ job_id: { type: "string", description: "Job id from the `auto_evaluate` response." },
762
+ },
763
+ required: ["job_id"],
764
+ },
765
+ },
705
766
  {
706
767
  name: "quote_engine_eval",
707
768
  description: "Return the stored engine eval for a node, or null if that node was never analysed. **Call this before writing prose or NAGs that quote engine numbers** — if it returns null, you have no measurement to cite. Do NOT infer an eval for the node from siblings or children; either analyse it (cloud_analyse with node_id) or omit the number from your prose.\n\n" +
@@ -749,6 +810,13 @@ const TOOLS = [
749
810
  description: "Returns the guide on how to write correct, useful PGN — mainline discipline, variations as moves (never prose describing moves), NAG symbols including novelty ($146), unclear ($13), compensation ($44) and the standard set, ChessBase arrow/coloured-square syntax ([%cal] / [%csl]), and common pitfalls the parser will reject. Call this ONCE per session before any save_prep_file call, or any time you're producing PGN output for the user.",
750
811
  inputSchema: { type: "object", properties: {} },
751
812
  },
813
+ {
814
+ name: "read_example_prep_files",
815
+ description: "Returns two full reference PGNs authored by a strong human coach — one a general opening overview (Italian Fried Liver, both sides, 1600+ audience) and one a one-sided repertoire (Najdorf 6.f4 for White, 2200+ audience). Bundled with the MCP; not the user's files.\n\n" +
816
+ "Read these ONCE per session before writing your first substantial prep file. The authoring guide (read_pgn_authoring_guide) tells you the rules; these examples show you what the rules look like when applied by someone who knows what they're doing — comment density, when to cite games by player name, how to use `$146` / `$3` / `$44` sparingly and correctly, when a bare `[%csl Rf7]` says everything, how to acknowledge transpositions, how to phrase practical guidance vs objective evaluation. Study the rhythm before writing your own.\n\n" +
817
+ "Response: `{ overview: <pgn>, repertoire: <pgn> }`. Raw PGN — comments, arrows, NAGs, and stored evals all intact.",
818
+ inputSchema: { type: "object", properties: {} },
819
+ },
752
820
  {
753
821
  name: "prep_snapshot",
754
822
  description: "One call, three parallel fetches at the same position: opponent's stats on their side, your stats on your side, and the 11.7M-game general database at that position. Use this while walking the opening tree — one round trip instead of three separate calls, and you can compare the three views directly (e.g. opponent has 2 games here but the general DB has 8k → prep candidate).\n\n" +
@@ -954,12 +1022,33 @@ async function applyBatchMutations(args) {
954
1022
  const savedRow = saved;
955
1023
  return { ok: true, results, version: savedRow.version };
956
1024
  }
957
- // Auto-evaluate: walk the tree from `path`, run cloud_analyse on each node,
958
- // stash the compact per-engine eval in the node's `ceoEval` field (which
959
- // survives across sessions and appears on every read_prep_file), and
960
- // derive the NAG from the SF score. Both writes go via a single batch
961
- // mutation at the end so a 200-node evaluate is one save.
1025
+ const evalJobs = new Map();
1026
+ // GC finished jobs after this long so status polling remains useful
1027
+ // for a while but the map doesn't grow unbounded across long uptimes.
1028
+ const EVAL_JOB_TTL_MS = 15 * 60 * 1000;
1029
+ // Checkpoint interval save progress every N successfully-evaluated
1030
+ // nodes so a mid-run kill leaves the tree partially populated. Small
1031
+ // enough that <15s of work is at risk per checkpoint on a slow combo,
1032
+ // large enough that the save overhead stays a small fraction of the
1033
+ // per-node cost.
1034
+ const SAVE_EVERY_N = 8;
1035
+ function newEvalJobId() {
1036
+ // 12 hex chars, low collision (same 32-bit width as node ids ×1.5).
1037
+ const rand = Math.random().toString(16).slice(2, 8);
1038
+ return `evj_${Date.now().toString(16)}${rand}`;
1039
+ }
1040
+ // Sweep expired jobs on every start/status call — cheap, doesn't need
1041
+ // a background timer, keeps the map bounded to active + recent jobs.
1042
+ function reapExpiredEvalJobs() {
1043
+ const now = Date.now();
1044
+ for (const [k, j] of evalJobs) {
1045
+ if (j.finishedAt && now - j.finishedAt > EVAL_JOB_TTL_MS) {
1046
+ evalJobs.delete(k);
1047
+ }
1048
+ }
1049
+ }
962
1050
  async function autoEvaluate(args) {
1051
+ reapExpiredEvalJobs();
963
1052
  const id = String(args.id);
964
1053
  const startNodeId = typeof args.node_id === "string" && args.node_id.length > 0
965
1054
  ? String(args.node_id)
@@ -987,45 +1076,159 @@ async function autoEvaluate(args) {
987
1076
  // If the caller anchored at the root, skip evaluating the root itself
988
1077
  // (no move); otherwise the anchor node IS a real move and gets evaluated.
989
1078
  walk(startNode, startNode.id === ROOT_ID);
990
- if (targets.length === 0)
991
- return { ok: true, evaluated: 0, skipped: 0, version: g.version };
992
- // Dispatch cloud_analyse in parallel with a 4-way cap so we don't
993
- // over-saturate the engine-ws connection cap (single-client per port).
994
- const stored = [];
995
- const CONCURRENCY = 4;
996
- let cursor = 0;
997
- async function worker() {
998
- while (cursor < targets.length) {
999
- const idx = cursor++;
1000
- const t = targets[idx];
1079
+ // Nothing to do → return a done job synthetically so the caller doesn't
1080
+ // need to special-case the empty response.
1081
+ if (targets.length === 0) {
1082
+ const jobId = newEvalJobId();
1083
+ evalJobs.set(jobId, {
1084
+ id: jobId,
1085
+ fileId: id,
1086
+ status: "done",
1087
+ targetCount: 0,
1088
+ evaluated: 0,
1089
+ errored: 0,
1090
+ finalVersion: g.version,
1091
+ startedAt: Date.now(),
1092
+ finishedAt: Date.now(),
1093
+ cancelled: false,
1094
+ });
1095
+ return { job_id: jobId, target_count: 0, status: "done", version: g.version };
1096
+ }
1097
+ const jobId = newEvalJobId();
1098
+ const job = {
1099
+ id: jobId,
1100
+ fileId: id,
1101
+ status: "running",
1102
+ targetCount: targets.length,
1103
+ evaluated: 0,
1104
+ errored: 0,
1105
+ startedAt: Date.now(),
1106
+ cancelled: false,
1107
+ };
1108
+ evalJobs.set(jobId, job);
1109
+ // Unawaited — runs concurrently with the tool response. Any thrown
1110
+ // error gets recorded on the job so the LLM's status poll surfaces
1111
+ // it instead of the process seeing an unhandled rejection.
1112
+ void runEvalJob(job, id, targets, movetimeMs).catch(err => {
1113
+ job.status = "error";
1114
+ job.error = err instanceof Error ? err.message : String(err);
1115
+ job.finishedAt = Date.now();
1116
+ });
1117
+ return {
1118
+ job_id: jobId,
1119
+ target_count: targets.length,
1120
+ status: "running",
1121
+ // Rough time estimate at the current default movetime. Serialization
1122
+ // on the per-combo semaphore means walltime ≈ target_count × movetime.
1123
+ estimated_seconds: Math.round((targets.length * movetimeMs) / 1000),
1124
+ };
1125
+ }
1126
+ // Worker body — walks targets sequentially (concurrency > 1 is a lie
1127
+ // against the per-combo semaphore in the backend anyway), checkpoints
1128
+ // every SAVE_EVERY_N successfully-evaluated nodes so partial progress
1129
+ // is durable, and re-anchors the version after each save.
1130
+ async function runEvalJob(job, fileId, targets, movetimeMs) {
1131
+ const pending = [];
1132
+ const flush = async () => {
1133
+ if (pending.length === 0)
1134
+ return;
1135
+ // No expected_version — auto_evaluate treats concurrent edits by
1136
+ // the LLM as last-write-wins on the ceoEval field specifically.
1137
+ // Safe because set_ceo_eval is idempotent per node and other
1138
+ // mutations (add_move / set_comment / etc.) don't touch ceoEval.
1139
+ const saved = await applyBatchMutations({
1140
+ id: fileId,
1141
+ mutations: pending,
1142
+ });
1143
+ const sr = saved;
1144
+ if (typeof sr.version === "number")
1145
+ job.finalVersion = sr.version;
1146
+ pending.length = 0;
1147
+ };
1148
+ for (const t of targets) {
1149
+ if (job.cancelled)
1150
+ break;
1151
+ try {
1152
+ const analysis = await authedRequest("POST", "/api/agent/cloud-engines/analyse", { fen: t.fen, movetime_ms: movetimeMs, multipv: 1 });
1153
+ const ev = analysisToStoredEval(analysis, t.fen);
1154
+ if (ev) {
1155
+ pending.push({ op: "set_ceo_eval", node_id: t.nodeId, ceoEval: ev });
1156
+ job.evaluated++;
1157
+ }
1158
+ else {
1159
+ job.errored++;
1160
+ }
1161
+ }
1162
+ catch {
1163
+ // Per-node failure — record and continue; a bad FEN or a transient
1164
+ // engine hiccup on one node shouldn't kill the whole walk.
1165
+ job.errored++;
1166
+ }
1167
+ if (pending.length >= SAVE_EVERY_N) {
1001
1168
  try {
1002
- const analysis = await authedRequest("POST", "/api/agent/cloud-engines/analyse", { fen: t.fen, movetime_ms: movetimeMs, multipv: 1 });
1003
- const ev = analysisToStoredEval(analysis, t.fen);
1004
- if (ev)
1005
- stored.push({ nodeId: t.nodeId, ev });
1169
+ await flush();
1006
1170
  }
1007
1171
  catch {
1008
- // best-effort a single failed node shouldn't kill the batch
1172
+ // Save failure is bad but not fatal try again on the next
1173
+ // checkpoint or at the end. Progress remains in `pending`
1174
+ // so nothing is lost as long as the process stays alive.
1009
1175
  }
1010
1176
  }
1011
1177
  }
1012
- await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
1013
- if (stored.length === 0)
1014
- return { ok: true, evaluated: 0, skipped: targets.length, version: g.version };
1015
- // Persist the raw numbers only — never touch visible NAGs. NAG glyphs
1016
- // are the LLM's editorial call (novelty, sharp choice, real mistake),
1017
- // not an automatic mapping from the engine number. Stamping `$10` on
1018
- // every 0.00 position in an opening tree just clutters the board.
1019
- // The threshold-derived NAG still lives INSIDE `ceoEval.nag` so the
1020
- // LLM can read it back and decide whether to promote it to a visible
1021
- // NAG on a case-by-case basis.
1022
- const batchMutations = [];
1023
- for (const s of stored) {
1024
- batchMutations.push({ op: "set_ceo_eval", node_id: s.nodeId, ceoEval: s.ev });
1178
+ // Final flush regardless of cancellation durably persist whatever
1179
+ // work was completed before the user asked to stop.
1180
+ try {
1181
+ await flush();
1182
+ }
1183
+ catch (err) {
1184
+ job.error = err instanceof Error ? err.message : String(err);
1185
+ job.status = "error";
1186
+ job.finishedAt = Date.now();
1187
+ return;
1188
+ }
1189
+ job.status = job.cancelled ? "cancelled" : "done";
1190
+ job.finishedAt = Date.now();
1191
+ }
1192
+ function autoEvaluateStatus(args) {
1193
+ reapExpiredEvalJobs();
1194
+ const jobId = String(args.job_id || "").trim();
1195
+ if (!jobId)
1196
+ throw new Error("`job_id` is required");
1197
+ const job = evalJobs.get(jobId);
1198
+ if (!job) {
1199
+ return {
1200
+ status: "not_found",
1201
+ note: "Job unknown — either expired (kept ~15 min after completion), never existed, or the MCP process restarted since it was created. Re-run auto_evaluate to start over; the `only_missing` default will skip nodes already evaluated in the prep file.",
1202
+ };
1203
+ }
1204
+ return {
1205
+ job_id: job.id,
1206
+ status: job.status,
1207
+ target_count: job.targetCount,
1208
+ evaluated: job.evaluated,
1209
+ errored: job.errored,
1210
+ remaining: Math.max(0, job.targetCount - job.evaluated - job.errored),
1211
+ done: job.status !== "running",
1212
+ error: job.error,
1213
+ version: job.finalVersion,
1214
+ started_at_ms: job.startedAt,
1215
+ finished_at_ms: job.finishedAt,
1216
+ };
1217
+ }
1218
+ function autoEvaluateCancel(args) {
1219
+ const jobId = String(args.job_id || "").trim();
1220
+ if (!jobId)
1221
+ throw new Error("`job_id` is required");
1222
+ const job = evalJobs.get(jobId);
1223
+ if (!job)
1224
+ return { status: "not_found" };
1225
+ if (job.status !== "running") {
1226
+ return { status: job.status, note: "Job already finished; nothing to cancel." };
1025
1227
  }
1026
- const saveResult = await applyBatchMutations({ id, mutations: batchMutations, expected_version: g.version });
1027
- const sr = saveResult;
1028
- return { ok: true, evaluated: stored.length, skipped: targets.length - stored.length, version: sr.version };
1228
+ job.cancelled = true;
1229
+ // status transitions to "cancelled" on the next per-node iteration
1230
+ // inside runEvalJob, after the final flush persists progress.
1231
+ return { status: "cancelling", evaluated_so_far: job.evaluated };
1029
1232
  }
1030
1233
  // Walk chess.js-free: resolve a path against a tree, throw if invalid.
1031
1234
  function pathIntoTree(root, path) {
@@ -1298,6 +1501,46 @@ function resolveFenFromArgs(args) {
1298
1501
  }
1299
1502
  return board.fen();
1300
1503
  }
1504
+ // Normalize one MCP `prepare_opponent` source into the shape the backend's
1505
+ // /api/chess/prep/prepare-multi expects. Handles two impedance mismatches:
1506
+ // - snake_case → camelCase (fide_id → fideId, start_month → startMonth, etc.)
1507
+ // - the unified `time_control` string → per-source-type filter:
1508
+ // * fide / chesscom → timeFormats: ["Classical" | "Rapid" | "Blitz"]
1509
+ // * lichess → perfType: "classical" | "rapid" | "blitz" | "bullet"
1510
+ // Backend validates required fields per source type, so we don't need to
1511
+ // pre-reject missing username/fideId here — it'll come back as a 400 the
1512
+ // LLM can act on.
1513
+ function normalizeSourceForBackend(src, idx) {
1514
+ const type = typeof src.type === "string" ? src.type : "";
1515
+ if (type !== "fide" && type !== "chesscom" && type !== "lichess") {
1516
+ throw new Error(`sources[${idx}].type must be one of fide|chesscom|lichess (got ${JSON.stringify(src.type)})`);
1517
+ }
1518
+ const out = { type };
1519
+ if (typeof src.fide_id === "number")
1520
+ out.fideId = src.fide_id;
1521
+ if (typeof src.username === "string" && src.username.trim() !== "")
1522
+ out.username = src.username.trim();
1523
+ if (typeof src.color === "string" && (src.color === "white" || src.color === "black"))
1524
+ out.color = src.color;
1525
+ if (typeof src.start_month === "string" && src.start_month.trim() !== "")
1526
+ out.startMonth = src.start_month.trim();
1527
+ if (typeof src.end_month === "string" && src.end_month.trim() !== "")
1528
+ out.endMonth = src.end_month.trim();
1529
+ if (typeof src.exclude_online === "boolean")
1530
+ out.excludeOnline = src.exclude_online;
1531
+ const tc = typeof src.time_control === "string" ? src.time_control : "";
1532
+ if (tc) {
1533
+ if (type === "lichess") {
1534
+ out.perfType = tc;
1535
+ }
1536
+ else {
1537
+ // fide + chesscom take a titlecased timeFormats array.
1538
+ const titled = tc.charAt(0).toUpperCase() + tc.slice(1);
1539
+ out.timeFormats = [titled];
1540
+ }
1541
+ }
1542
+ return out;
1543
+ }
1301
1544
  // Async resolver used by every engine / DB tool. If the caller supplied
1302
1545
  // file_id + node_id (preferred), load the file, resolve the node, and
1303
1546
  // return both the FEN and a handle we can use to persist ceoEval later.
@@ -1392,27 +1635,31 @@ async function callToolInner(name, args) {
1392
1635
  return get("/api/chess/players/search/simple", { q: String(args.name), view: "llm" });
1393
1636
  case "get_player_profile":
1394
1637
  return get("/api/chess/players/profile", { fideId: Number(args.fide_id) });
1395
- case "get_player_preparation": {
1396
- // Node-first: if file_id+node_id was given, derive the FEN from the
1397
- // tree (never trust an LLM-supplied FEN when a node reference is
1398
- // available). Otherwise fall back to fen/moves/line.
1638
+ case "prepare_opponent": {
1639
+ const rawSources = Array.isArray(args.sources) ? args.sources : [];
1640
+ if (rawSources.length === 0)
1641
+ throw new Error("`sources` array required");
1642
+ const sources = rawSources.map((s, i) => normalizeSourceForBackend(s, i));
1643
+ return authedRequest("POST", "/api/chess/prep/prepare-multi", { sources });
1644
+ }
1645
+ case "get_prep_position": {
1646
+ const token = String(args.session_token || "").trim();
1647
+ if (!token)
1648
+ throw new Error("`session_token` required — create one with prepare_opponent");
1399
1649
  const resolved = await resolveFromNodeOrFen(args);
1400
- const effectiveFen = resolved.fen;
1401
- const params = {
1402
- fideId: Number(args.fide_id),
1403
- color: String(args.color),
1404
- compact: "true",
1405
- fen: effectiveFen,
1650
+ const fen = resolved.fen;
1651
+ const qs = {
1652
+ token,
1653
+ fen,
1654
+ limit: typeof args.limit === "number" ? args.limit : 10,
1406
1655
  };
1407
- if (typeof args.limit === "number")
1408
- params.limit = args.limit;
1409
1656
  if (typeof args.offset === "number")
1410
- params.offset = args.offset;
1657
+ qs.offset = args.offset;
1411
1658
  const [raw, ev] = await Promise.all([
1412
- get("/api/chess/prep/by-player", params),
1413
- fetchCompactEval(effectiveFen),
1659
+ get("/api/chess/prep/unified", qs),
1660
+ fetchCompactEval(fen),
1414
1661
  ]);
1415
- const converted = convertAvailableMovesToSAN(raw, effectiveFen);
1662
+ const converted = convertAvailableMovesToSAN(raw, fen);
1416
1663
  if (converted && typeof converted === "object") {
1417
1664
  trimGamesMovetext(converted);
1418
1665
  stripPositionResponse(converted);
@@ -1421,6 +1668,14 @@ async function callToolInner(name, args) {
1421
1668
  }
1422
1669
  return converted;
1423
1670
  }
1671
+ case "list_prep_sessions":
1672
+ return authedRequest("GET", "/api/chess/prep/sessions");
1673
+ case "delete_prep_session": {
1674
+ const token = String(args.session_token || "").trim();
1675
+ if (!token)
1676
+ throw new Error("`session_token` required");
1677
+ return authedRequest("DELETE", `/api/chess/prep/sessions/${encodeURIComponent(token)}`);
1678
+ }
1424
1679
  case "get_position_stats": {
1425
1680
  const resolved = await resolveFromNodeOrFen(args);
1426
1681
  const fen = resolved.fen;
@@ -1574,6 +1829,10 @@ async function callToolInner(name, args) {
1574
1829
  return applyBatchMutations(args);
1575
1830
  case "auto_evaluate":
1576
1831
  return autoEvaluate(args);
1832
+ case "auto_evaluate_status":
1833
+ return autoEvaluateStatus(args);
1834
+ case "auto_evaluate_cancel":
1835
+ return autoEvaluateCancel(args);
1577
1836
  case "quote_engine_eval": {
1578
1837
  const fileId = String(args.id);
1579
1838
  const nodeId = argNodeId(args);
@@ -1595,6 +1854,8 @@ async function callToolInner(name, args) {
1595
1854
  return { guide: PREP_FILES_DOC };
1596
1855
  case "read_pgn_authoring_guide":
1597
1856
  return { guide: PGN_AUTHORING_DOC };
1857
+ case "read_example_prep_files":
1858
+ return { overview: EXAMPLE_OVERVIEW_PGN, repertoire: EXAMPLE_REPERTOIRE_PGN };
1598
1859
  case "prep_snapshot": {
1599
1860
  const me = Number(args.fide_id_me);
1600
1861
  const opp = Number(args.fide_id_opponent);
@@ -1698,7 +1959,7 @@ Preparation workflow — follow the steps in order and be explicit about which t
1698
1959
  - Rapid and blitz reveal what they play under time pressure but may include experiments.
1699
1960
  - Online games are useful but noisier (blitz gambits, alt accounts).
1700
1961
 
1701
- 4. **Walk the opponent's repertoire against ${me}'s color.** Call \`get_player_preparation\` on the opponent for the color they'll have in this game. Iterate: start from move 1, pick the opponent's most-played reply, call again with \`line\` extended. Look for:
1962
+ 4. **Walk the opponent's repertoire against ${me}'s color.** Call \`prepare_opponent\` once with the opponent's FIDE ID (add \`chesscom\` / \`lichess\` sources if you know their handles), filtered to the color they'll have in this game — plus \`start_month\` (last 12-24 months) and \`time_control: "classical"\` if you want the strongest signal. That returns a session \`token\`. Then walk the tree with \`get_prep_position(session_token=token, node_id="r")\`, pick the opponent's most-played reply, call again as you descend. Look for:
1702
1963
  - **Weak lines**: variations where the opponent scores below 40% as their side.
1703
1964
  - **Shallow lines**: openings the opponent has played only a few times — probably less deeply prepared.
1704
1965
  - **Abandoned lines**: openings they used to play but stopped. Something went wrong; may not want to revisit.
@@ -1740,7 +2001,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
1740
2001
  1. \`search_player\` to get their FIDE ID.
1741
2002
  2. \`get_player_profile\` — pull rating history, career splits by color and time control, opening repertoire, opponent analysis, top events, notable wins and losses.
1742
2003
  3. Weight the data: recent (last 12-24 months) > older, classical OTB > rapid/blitz > online.
1743
- 4. \`get_player_preparation\` for both colors from the starting position to summarise their opening choices with actual frequencies and win rates.
2004
+ 4. \`prepare_opponent\` twice (once per colour, or once with two sources), then \`get_prep_position(session_token, node_id="r")\` to summarise their opening choices with actual frequencies and win rates. Filter with \`start_month\` if you only care about their current repertoire.
1744
2005
  5. Deliver: current strength, characteristic openings, one-sentence style read, biggest wins, biggest losses / recurring weakness. Cite the numbers.`;
1745
2006
  break;
1746
2007
  }
@@ -14,12 +14,27 @@ import { makeFen } from "chessops/fen";
14
14
  import { cloneOnPath, deriveNodeId, getNode, PathError } from "./paths.js";
15
15
  export class MutationError extends Error {
16
16
  }
17
- // Add a SAN move as a new child under `parentPath`. Appends — becomes
18
- // a variation if the parent already has children (mainline is
19
- // children[0]). Use promoteVariation afterwards to switch mainline.
20
- // Returns the new node's stable id (derived from parent.id + san).
17
+ // Add a SAN move as a new child under `parentPath`. **Idempotent on
18
+ // SAN**: if a child with this SAN already exists under the parent, we
19
+ // return that existing child's id and leave the file unchanged. Same
20
+ // SAN from the same position IS the same move in chess — appending a
21
+ // duplicate sibling would (a) let the LLM create ambiguous "two lines
22
+ // both starting with d5" trees and (b) produce two nodes with the
23
+ // same content-derived id. Frontend's MoveService.playMove has the
24
+ // same "append new or navigate to existing" behaviour; this mirrors it.
25
+ //
26
+ // If there's no existing child with that SAN, we validate the move,
27
+ // derive the new id from (parent.id, san), and append. Because
28
+ // mainline is children[0], any appended move becomes a variation
29
+ // unless the parent had no children yet; use promoteVariation to
30
+ // switch mainline afterwards.
21
31
  export function addMove(file, parentPath, san) {
22
32
  const parent = getNode(file.root, parentPath);
33
+ // Idempotence: join to an existing child with the same SAN.
34
+ const existing = parent.children.find(c => c.san === san);
35
+ if (existing) {
36
+ return { file, id: existing.id };
37
+ }
23
38
  const move = validateSan(parent.fen, san);
24
39
  const posSetup = parseFen(parent.fen);
25
40
  if (posSetup.isErr)
@@ -46,9 +61,16 @@ export function addMove(file, parentPath, san) {
46
61
  };
47
62
  }
48
63
  // Add a linear sequence of moves under `parentPath` — one call for a
49
- // whole variation instead of N add_move calls. Each move becomes the
50
- // mainline child (children[0]) of the previous. Returns the ids and
51
- // SANs of every added node so the LLM can address any of them next.
64
+ // whole variation instead of N add_move calls. Inherits addMove's
65
+ // idempotence: if the line's prefix overlaps an existing branch, we
66
+ // join to it and only start creating new nodes at the divergence
67
+ // point. So two `add_line` calls sharing a prefix (e.g. `[d5, Bb5,
68
+ // Nd7]` and `[d5, Bb5, Ne4]`) build a proper Y-shape — one d5 → one
69
+ // Bb5 → two siblings from there — not two duplicate chains.
70
+ //
71
+ // Returns the ids and SANs of every node along the resulting line
72
+ // (both joined and newly-created), so the LLM can address any of
73
+ // them next.
52
74
  export function addLine(file, parentPath, sans) {
53
75
  if (sans.length === 0)
54
76
  throw new MutationError("add_line requires at least one san");
@@ -58,11 +80,14 @@ export function addLine(file, parentPath, sans) {
58
80
  for (const san of sans) {
59
81
  const step = addMove(cur, curPath, san);
60
82
  cur = step.file;
61
- // The move we just appended is the parent's last child — extend
62
- // the walked path with that child index so the next move lands on
63
- // top of it (extending the line, not creating a sibling).
83
+ // Find the child we just addressed (whether we appended a new
84
+ // node or joined to an existing one) by its id, so the next
85
+ // iteration extends from the RIGHT node — not necessarily the
86
+ // parent's last child.
64
87
  const parent = getNode(cur.root, curPath);
65
- const childIdx = parent.children.length - 1;
88
+ const childIdx = parent.children.findIndex(c => c.id === step.id);
89
+ if (childIdx < 0)
90
+ throw new MutationError(`internal: node ${step.id} not found after addMove`);
66
91
  curPath = [...curPath, childIdx];
67
92
  line.push({ id: step.id, san });
68
93
  }
package/dist/pgn/paths.js CHANGED
@@ -13,11 +13,15 @@
13
13
  import { createHash } from "node:crypto";
14
14
  // Special ID for the root position (starting FEN, no move played).
15
15
  export const ROOT_ID = "r";
16
- // 8-hex-char content hash. Root is "r". Every other node's id is
17
- // derived from (parent.id, san). Two nodes with the same (parent, san)
18
- // can't co-exist as siblings in chess anyway, so within-parent
19
- // collisions are impossible. Cross-tree collisions in 32 bits at 1000
20
- // nodes are ~10^-4 we throw on the rare hit rather than mask it.
16
+ // 8-hex-char (32-bit) content hash. Root is "r". Every other node's
17
+ // id is derived from (parent.id, san). Two children of the same parent
18
+ // with the same SAN are impossible in real chess same SAN from the
19
+ // same position IS the same move so within-parent same-id events
20
+ // mean the caller tried to duplicate a move that already exists;
21
+ // `addMove` handles that by joining to the existing child rather than
22
+ // appending a duplicate (see mutations.ts). Cross-tree birthday
23
+ // collisions in 32 bits at 1000 nodes are ~10^-4 — rare, and thrown
24
+ // at parse time rather than silently masked.
21
25
  export function deriveNodeId(parentId, san) {
22
26
  const h = createHash("sha256");
23
27
  h.update(parentId);
@@ -70,19 +70,24 @@ Trust Lc0 for:
70
70
 
71
71
  Contempt is an Lc0 option that skews its evaluation and move choice toward one side. Passing `contempt` to `cloud_analyse` sets it on the Lc0 leg only; Stockfish always analyzes objectively.
72
72
 
73
- **Range:** -100 to +100 accepted; **practical range: -20 to +20**. Beyond ±20 Lc0 starts picking objectively bad moves that only look good under an extreme bias.
73
+ **Scale:** signed 0-100 strength the same value the user's web UI shows on its ContemptStrength slider. The server multiplies by 8 to produce Lc0's internal centipawn bias, so `contempt=25` sends 200 cp, `contempt=50` sends 400 cp, `contempt=100` sends 800 cp. Positive favours White, negative favours Black, zero is objective.
74
74
 
75
- - **Positive contempt** (e.g. `+15`): Lc0 assumes it's playing *from White's side* against equal or slightly weaker opposition. It picks more ambitious, complicated moves and avoids quick simplifications. The eval reads higher than pure objective. Concrete effect: with White, Lc0 will less often steer into the Exchange Slav or a symmetrical Berlin.
76
- - **Negative contempt** (e.g. `-15`): Same but from Black's side. Lc0 will play sharper, more provocative lines with Black more likely to pick a Sveshnikov over a Petroff, or a Grünfeld over a Slav.
75
+ **Practical magnitudes:**
76
+ - `±10-20` a light nudge. Small preference shift, mostly for tie-breaking between candidates Lc0 already sees as near-equal.
77
+ - `±30-60` — real fighting play. Lc0 avoids simplifications, prefers keeping tension, favours the more ambitious side of a choice.
78
+ - `±80-100` — maximum steer. Use when the user needs Lc0 to surface *only* fighting ideas for a must-win, at the cost of ignoring objectively safer choices.
77
79
  - **Zero** (default): pure objective Lc0.
78
80
 
81
+ - **Positive contempt** (e.g. `+30`): Lc0 assumes it's playing *from White's side* against equal or slightly weaker opposition. It picks more ambitious, complicated moves and avoids quick simplifications. The eval reads higher than pure objective. Concrete effect: with White, Lc0 will less often steer into the Exchange Slav or a symmetrical Berlin.
82
+ - **Negative contempt** (e.g. `-30`): Same but from Black's side. Lc0 will play sharper, more provocative lines with Black — more likely to pick a Sveshnikov over a Petroff, or a Grünfeld over a Slav.
83
+
79
84
  The eval Lc0 returns when contempt is set is **not** the objective eval — it's Lc0's assessment under the contempt bias. If you also want the objective read, look at the Stockfish leg of the same response.
80
85
 
81
86
  ### When to set contempt
82
87
 
83
- - **Finding new ideas** — set contempt in the direction you're preparing to see moves the objective engine wouldn't consider "safe enough." Lines Lc0 rejects at contempt=0 but likes at contempt=±15 are candidate surprises.
84
- - **Building a solid repertoire** — small positive contempt for your color if the user wants to take positions seriously (real, competitive games) without gambling on unclear complications.
85
- - **Playing for a win with the "worse" side** — negative contempt if the user needs to avoid drawing lines with Black in a must-win situation.
88
+ - **Finding new ideas** — set contempt in the direction you're preparing to see moves the objective engine wouldn't consider "safe enough." Lines Lc0 rejects at contempt=0 but likes at contempt=±30 are candidate surprises.
89
+ - **Building a solid repertoire** — small positive contempt (`+10` to `+20`) for your color if the user wants to take positions seriously (real, competitive games) without gambling on unclear complications.
90
+ - **Playing for a win with the "worse" side** — negative contempt (`-30` to `-60`) if the user needs to avoid drawing lines with Black in a must-win situation.
86
91
  - **Never with zero direction** — always know *why* you're skewing before you set contempt. It's a specific tool for a specific question, not a default knob.
87
92
 
88
93
  ## Tricks worth knowing
@@ -106,10 +111,10 @@ Two calls:
106
111
  1. `cloud_analyse(fen=<position after 6...e5>, multipv=2)` — get both engines' top choices with movetime=2000.
107
112
  2. If Stockfish scores them equal but Lc0 prefers one by 0.10-0.20, that's your practical answer. The user will find that line harder to face.
108
113
 
109
- If the user is specifically preparing to *play* the black side in a must-win, add `contempt=-15` on a follow-up call to see which lines Lc0 finds most fighting for Black. Compare against Stockfish's objective read to make sure the fighting choice isn't just losing.
114
+ If the user is specifically preparing to *play* the black side in a must-win, add `contempt=-30` (or up to `-60` for a harder steer) on a follow-up call to see which lines Lc0 finds most fighting for Black. Compare against Stockfish's objective read to make sure the fighting choice isn't just losing.
110
115
 
111
116
  ## What NOT to do
112
117
 
113
- - **Don't quote Lc0's contempt-biased eval as objective.** If you tell the user "Lc0 gives Black +0.30 here" without disclosing you set contempt=-15, that's misleading.
118
+ - **Don't quote Lc0's contempt-biased eval as objective.** If you tell the user "Lc0 gives Black +0.30 here" without disclosing you set contempt=-30, that's misleading.
114
119
  - **Don't run cloud analysis just for casual questions.** `cloud_analyse` costs the user real money per second. If the question is "is 1.e4 or 1.d4 better?", the free `analyse` (single Stockfish, 2s) or `get_position_stats` (11.7M-game database) is enough.
115
120
  - **Don't ignore the disagreement.** When Stockfish and Lc0 diverge sharply, that's exactly when you should explain *why* to the user — not paper over it.
@@ -0,0 +1,12 @@
1
+ [Event "1600+"]
2
+ [Site "?"]
3
+ [Date "2026.05.11"]
4
+ [Round "?"]
5
+ [White "Overview: Italian Fried Liver"]
6
+ [Black "?"]
7
+ [Result "*"]
8
+ [ECO "C57"]
9
+ [Opening "Italian Game"]
10
+ [Annotator "Chess.ceo"]
11
+
12
+ 1. e4 e5 2. Nf3 Nc6 3. Bc4 Nf6 4. Ng5 {[%csl Rf7]} d5 (4... Bc5 $6 {The Traxler variation} 5. Bxf7+ {Seen as the modern refutation.} (5. Nxf7 {Most principled. Interesting is that not long ago computers were saying this is winning for white. But modern strong engines almost immediately proclaim 0,00} Bxf2+ 6. Kf1 (6. Kxf2 {This usually leads to a draw} Nxe4+ 7. Kg1 (7. Ke3 Qh4 8. g3 Nxg3 9. hxg3 Qd4+ 10. Kf3 O-O) 7... Qh4 8. g3 Nxg3 9. Nxh8 Nd4 10. hxg3 Qxg3+ 11. Kf1 Qf4+ $10) 6... Qe7 7. Nxh8 d5 8. exd5 Nd4 9. d6 $1 (9. c3 {Most played, and weak engines still like this move, but fail to spot} Bg4 10. Qa4+ Nd7 $1 {And black is winning} 11. Kxf2 Qh4+) 9... Qxd6 (9... cxd6 $2 10. Kxf2 Ng4+ 11. Kg1 $18 {and the d6 pawn blocks Qc5}) 10. Nf7 {and here both} Qe7 {and} (10... Qc5 {are okay for black with plenty left to analyze})) (5. Nxh7) 5... Ke7 {Now white has 3 squares to return to.} 6. Bc4 $1 {By far the least played, but clearly the best, now white can always counter Bg4 with Be2. [%cal Gc4e2]} (6. Bd5 {The most played move. Back in the day this was known as the way to refute the Traxler, and played by Karpov against Beliavsky.} Rf8 7. O-O d6 $13 {Also the computer admits that black has compensation here, with Qe8-Qg6 and Bg4 coming.} 8. h3 Qe8 9. c3 Qg6 10. d4 Bb6 $13) (6. Bb3 {A good move, but white has to remain accurate} d6 7. d3 (7. Nf7 Qf8 8. Nxh8 $4 Bxf2+ $19) (7. O-O $2 Bg4) 7... Qe8 8. c3 Rf8 9. Nf3 Qg6 10. O-O Bg4 11. Nbd2 $16 {Although objectively black has no compensation for the pawn, it does seem black still has some practical chances.} Kd8 12. Kh1 Qh5 13. Qe2 Ne7) 6... Rf8 7. Nc3 Qe8 8. O-O h6 9. Nf3 d6 10. Be2 $18 {White is a clean pawn up.}) (4... Nxe4 $2 {A cheap trick} 5. Bxf7+ (5. Nxf7 Qh4 {is what black hopes for}) (5. Nxe4 $4 d5) 5... Ke7 6. d4 $1 {And white is winning.}) 5. exd5 Na5 (5... Nd4 6. c3 b5 (6... h6 $5 {A new idea played first by John Ludvig Hammer in 2024} 7. cxd4 hxg5 8. d3 $146 {Seems like a fine way to play} exd4 9. Bxg5 Qe7+ 10. Qe2 Qxe2+ 11. Kxe2 $14) 7. Bf1 Nxd5 8. cxd4 Qxg5 9. Bxb5+ Kd8 {Here its easy for white to mess up the move order. If black wants to go exd4, you should be ready for d3 is a way to remember that white should castle first.} 10. O-O $1 (10. Qf3 $2 exd4 11. O-O Rb8 12. Bc4 Bb7 13. d3 Ne3) 10... Bb7 (10... exd4 11. d3 $16) 11. Qf3 Rb8 12. dxe5 Ne3 13. Qh3 Qxg2+ 14. Qxg2 Nxg2 15. d4 {This endgame looks good for white, and yes black has to be careful. but at high depth engines have shown black can hold here, e.g} f6 16. Be2 Nh4 17. f4 Nf5 18. Nc3 Nxd4 19. Rd1 c5 20. Be3 fxe5 21. fxe5 Kc7 22. Rac1 Nxe2+ 23. Nxe2 Bf3 24. Bxc5 Bxc5+ 25. Rxc5+ Kb6 26. Rc2 Rbd8) (5... b5 6. Bf1 Nd4 7. c3 {Position is analyzed via 5...Nd4. Click on the 'Transposition' button or press 'T' to transpose!}) (5... Nxd5 $6 {The proper Fried Liver variation. Often seen as a beginners mistake, it chronically leaves the f7 square weak.} 6. Nxf7 (6. d4 {less critical, but also strong for white.} Nxd4 7. c3 b5 8. Bd3 h6 9. Nxf7 Kxf7 10. cxd4 $14 {Blacks king is still exposed.}) 6... Kxf7 7. Qf3+ Ke6 8. Nc3 Nb4 (8... Ne7 $2 9. d4 {Black is unable to finished their development with the knight on e7.}) 9. O-O c6 {And now modern correspondence games continue like this:} 10. d4 Qf6 11. Qd1 Ke7 12. Re1 h6 13. Rxe5+ Kd8 14. Ne4 Qg6 15. a3 Bf5 16. Ng3 Bxc2 17. Qf3 Nd3 18. Rf5 Bd6 19. Bxd3 Bxd3 20. Qxd3 Kc7 {Where white is up a clean pawn, but black has finally coordinated their pieces. All correspondence games here ended in a draw.}) (5... Bg4 6. Nxf7 $1 $18) 6. Bb5+ c6 7. dxc6 bxc6 8. Bd3 {The modern main line} (8. Qf3 {Highly unusual way of playing, where half of whites pieces are undeveloped, and the other 3 are scattered over the board, still it defends against cxb5, attacks c6, and prepares for Ne4 if h6 is played.} Be7 {Here there is quite a peculiar line} (8... cxb5 {is a new line, where after} 9. Qxa8 {Both} Be7 ({And} 9... Qc7 {lead to sharp and theoretical positions.})) (8... h6 {Another way to sacrifice the exchange.} 9. Ne4 cxb5 10. Nxf6+ gxf6 11. Qxa8 Qd7 $44) 9. Bd3 O-O 10. Nc3 Rb8 $5 11. Bf5 $1 (11. O-O Rb4 $15) 11... Bb7 12. Qh3 (12. Nce4 g6 13. Qh3 h5 14. d3 c5) 12... g6 13. g4 {Difficult to understand what is happening here. Engine continues as follows:} c5 14. Rg1 c4 15. Rg3 Qd4 16. Ne2 Qd5 17. Kf1 h5 18. Nc3 Qc6 19. b4 Kg7 20. b5 Qh1+ 21. Rg1 hxg4 22. Bxg4 Rh8 23. Rxh1 Rxh3 24. Bxh3 Bxh1 25. Bg2 {White is a pawn up, but its difficult to develop properly.}) (8. Be2 {The old main line. it puts the bishop on a safe place, but at the same time whites knight now cannot retreat comfortably} h6 9. Nf3 (9. Nh3 {Famously played by Fischer. whites knight is misplaced, but safe for now. black has many sensible ways to play.} Bb4 $5 $146) 9... e4 10. Ne5 Qd4 (10... Bc5 {and}) (10... Bd6 {are both good.}) 11. f4 Bc5 12. Rf1 {the 'trick' white hopes for, suddenly white is aiming to play with c3-b4 or c3-d4, and blacks pieces seem misplaced. Still, black has good compensation here with the right moves.} Bb6 $1 (12... Qd8 13. d4 $1 Bb6 14. c3) 13. c3 Qd8 14. b4 (14. d4 exd3 15. Qxd3 Qc7 16. Nd2 O-O $44) 14... Nb7 15. Nxc6 Qc7 16. Ne5 O-O {With a very unclear position.}) 8... Nd5 (8... Ng4 {Aiming to force matters.} 9. Ne4 (9. Nh3 {This move has not been played recently, but is also testing against Ng4}) 9... f5 10. Be2 h5 11. h3 fxe4 12. hxg4 Bc5 13. b4 Qd4 14. bxc5 O-O 15. O-O Qxa1 16. Nc3 e3 17. gxh5 exf2+ 18. Rxf2 Rxf2 19. Kxf2 Be6 20. Ba3 Qxd1 21. Bxd1 {This endgame has been reached many times, with mixed results.}) 9. Nf3 (9. h4 {Another modern line, again giving black a lot of options, but none are easy.} h6 (9... Bc5 10. Qf3 f5 11. Nc3 O-O 12. Nxd5 Qxd5 13. Qxd5+ cxd5 14. c3) (9... Be7 10. Qf3 f5 11. Bxf5 Rf8 12. Qh5+ g6 13. Bxg6+ hxg6 14. Qxg6+ Kd7 {This position has also been played quite a bit, and it seems that black has to be more careful than white.}) (9... Qc7 10. Nc3 (10. b3 $5 h6 11. Ne4 f5 12. Nec3 Nf4 13. Bf1 Bb7 14. g3 c5 15. Rg1 Ne6 16. Qh5+ Qf7) 10... h6 11. Nxd5 cxd5 12. Qh5 Bc5 13. b4 Bxb4 14. Nxf7 O-O 15. Nxe5 Rf5 16. Qe8+ Rf8 $10) 10. Qh5 {is the idea} Qf6 11. Ne4 Qe6 {and here white can make a draw with Ng5, or continue with} 12. b3 $13) 9... Bd6 (9... Nf4 $2 {White hasn't castled so can still retreat with} 10. Bf1 $1 $14 {followed by d3 or d4}) 10. O-O {And here black has 2 main ways of playing} (10. Nc3 {A tricky move order. against Nf4 lines it will probably transpose, but against castle it will lead to different positions.} O-O (10... f5 11. Nxd5 cxd5 12. b4 Nc6 13. Bb5 {White scores well here.}) 11. Be2 f5 12. d3 {And theory continues}) 10... Nf4 (10... O-O {The most forcing, and objectively maybe the best. but it is quite complex.} 11. Re1 f5 {[%cal Ge5e4]} 12. Nxe5 Qf6 13. Nf3 g5 14. c4 (14. g3 f4 15. Nc3 fxg3 16. hxg3 Qxf3 17. Qxf3 Rxf3 18. Be4 Rf7 19. Nxd5 cxd5 20. Bxd5 Bb7 21. Bxf7+ Kxf7 $10) (14. Nc3 g4 15. Rb1 $13 {Another way of playing, taking big risks just to get black out of book.}) 14... Nf4 15. Bf1 g4 16. d4 gxf3 17. Qxf3 Ne6 18. Qc3 $13) 11. Re1 Nxd3 (11... O-O $2 12. Bf1 {Again whites bishop escapes.}) 12. cxd3 O-O 13. Nc3 (13. Nxe5 {Leads to a forced draw in multiple ways.} c5 14. Nc3 Bb7 15. b3 Re8 16. Nc4 Rxe1+ 17. Qxe1 Nxc4 18. dxc4 Bxh2+ 19. Kxh2 Qh4+ 20. Kg1 Bxg2 21. Kxg2 Qg4+) 13... Re8 14. Ne4 c5 15. b3 Bf8 16. Ba3 Nc6 $10 {With Nb4 next, black has enough compensation for the pawn.} *
@@ -0,0 +1,18 @@
1
+ [Event "2200+"]
2
+ [Site "?"]
3
+ [Date "2026.05.11"]
4
+ [Round "?"]
5
+ [White "⚪ Repertoire"]
6
+ [Black "Najdorf 6.f4"]
7
+ [Result "*"]
8
+ [FEN "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"]
9
+ [WhiteFideId "-1"]
10
+ [ECO "B92"]
11
+ [Opening "Sicilian Defense"]
12
+ [Annotator "Chess.ceo"]
13
+ [PlyCount "33"]
14
+ [GameId "2231000632463411"]
15
+ [EventDate "2021.??.??"]
16
+ [SourceVersionDate "2025.10.09"]
17
+
18
+ 1. e4 c5 2. Nf3 d6 3. d4 cxd4 4. Nxd4 Nf6 5. Nc3 a6 6. f4 {Known as the Amsterdam attack, with 6.f4 white immediately starts expanding on the kingside whilst staying flexible with the pieces.} e5 (6... g6 {A rare but good move, its the move Nepomniachtchi sticks to. White has to play concrete:} 7. e5 dxe5 8. fxe5 Nd5 (8... Ng4 9. e6 Bxe6 (9... f5 10. h3 Ne5 11. Be3 Nbc6 12. Nxc6 Qxd1+ 13. Rxd1 bxc6 14. Bf4 Bg7 15. Kf2 {Blacks knight is in trouble [%csl Re5]}) 10. Nxe6 Qxd1+ 11. Nxd1 fxe6 12. Bc4 Nc6 13. c3 {A pleasant endgame for white definitely, but nothing extraordinary.} Nce5 (13... Bh6 14. Bxh6 Nxh6 15. O-O Rf8 16. Nf2) 14. Bxe6 Nd3+ 15. Ke2 Nxc1+ 16. Rxc1 Bh6 17. Rb1 Nf6 18. Nf2 {keep pressin}) 9. Bc4 $5 {A more aggressive option} (9. Qf3 Nxc3 10. Qxc3 Bg7 11. Bf4 O-O 12. O-O-O Qb6 {So far 2 games of Nepo, and here} 13. a4 $146 {Is interesting, planning to go a5 next, blacks queen is in an awkward position.}) 9... Nxc3 10. bxc3 Bg7 (10... Qc7 11. Qe2 b5 (11... Bg7 12. O-O O-O 13. e6 f5 14. Bf4) 12. Bxf7+ (12. Bd5 Qxc3+ 13. Qd2 Qxd2+) 12... Kxf7 13. Qf3+ (13. O-O+ Ke8 14. Be3 e6 15. a4 $44) 13... Ke8 14. e6 Qc4 15. Qf7+ (15. Bb2 Ra7 (15... Nc6 16. Qf7+ Kd8 17. Nxc6+ Qxc6 18. O-O-O+ Kc7 19. c4 Bxe6 $10) 16. Qf7+ Kd8 17. Qf4 Qc7 18. Qf2 Qe5+ 19. Ne2 Rb7 20. Rd1+ Kc7 21. Ba1 $132) 15... Kd8 16. Bf4 Qxc3+ 17. Ke2 Qc4+ 18. Ke1 $10) (10... Nc6 $1 $146 11. Bf4 (11. O-O Nxe5 12. Bb3 Bg7 13. Bf4 O-O 14. Qe2) (11. Nxc6 Qxd1+ 12. Kxd1 {Equal just} Bg4+ 13. Ke1 bxc6 14. Rb1 Bg7 15. Bf4) 11... Bg7 (11... Qa5 12. O-O Qc5 13. Qd3 Bg7 14. Kh1 O-O 15. e6) (11... Na5 12. Bb3) 12. Nxc6 Qxd1+ 13. Rxd1 bxc6 14. Rb1 O-O 15. Ke2 Be6 16. Bxe6 fxe6 17. Rb4) 11. O-O O-O 12. Qd3 {black is cooked} Qc7 (12... Bxe5 13. Bh6 Bf6 14. Bxf8 Qxf8 15. Rae1 Bd7 16. Nb3) (12... Bg4 {difficult to find} 13. Qe4 (13. h3 Nd7 14. hxg4 Nxe5 15. Qe2 Nxc4 16. Qxc4 Rc8 17. Qb4 Rxc3)) (12... Qe8 13. Bg5 Nc6 14. Nxc6 bxc6 15. h4) 13. e6) (6... Qc7 {A tricky waiting move} 7. f5 $1 {A rare move, playing against e6/e5, and at the same time preparing g4 or Bg5} (7. Qf3 $6 {not great because black can still go} e5 {in one go.}) 7... Nc6 {Best move} (7... e5 8. fxe6 fxe6 9. Be2 {Blacks position is not easy} (9. Be3 Be7 (9... d5 10. exd5 Bb4) 10. g4 d5 11. g5 Nxe4 12. Nxe4 dxe4 13. Bg2) 9... Be7 (9... Nc6 10. Nxc6 Qxc6 11. Bg5 Be7 12. Bh5+ g6 13. Bf3) 10. Bh5+ (10. Bg4 {Interesting to make black find Qc4 first} Nxg4 (10... Qc4 11. Be2) 11. Qxg4 Bf6 12. e5 {Here bailout is likely} Bxe5 (12... dxe5 13. Nxe6 Qd7 14. Nd5 Qxd5 15. Nc7+ Kd8 16. Qxc8+ Kxc8 17. Nxd5 Bd8 18. Be3) 13. O-O Qc4 14. Be3 (14. Nce2 Nc6 15. c3) 14... Nc6 15. Rad1 (15. Ne4 Nxd4 16. Rad1 Rf8 17. Rfe1 (17. Qh5+ g6 18. Rxf8+ Kxf8 19. Qxh7 $10)) 15... Nxd4 16. Bxd4 Bd7 17. Ne4 (17. Rf2 O-O-O 18. Ne4 Bc6 19. Bxe5 $10) 17... Bxd4+ 18. Kh1 Qd5 19. c3 Rf8 20. Rfe1 O-O-O 21. Rxd4 Qe5 22. Nxd6+ Kc7 23. Rdd1 Qf4 24. Qxg7 Kb8 25. Nxb7 Rg8 26. Qe7 Rxg2 27. Qd6+) 10... g6 11. Be2 b5 (11... O-O 12. Bh6 Rf7 13. Rf1 $14 {[%cal Gd1d3,Ge1c1]}) 12. a3 e5 13. Nf3 Nbd7 14. Bh6 Ng4 15. Qd2) (7... b5 8. g4 Nbd7 (8... Bb7 9. Bg2 b4 (9... Nbd7) 10. Nd5 Nxd5 11. exd5 $18 Qc4 12. Nc6) 9. a3 (9. Bg2 b4 10. Nd5 Nxd5 11. exd5 g6 12. O-O gxf5 13. a3 Ne5 14. gxf5 Rg8 15. axb4 Bb7) 9... Bb7 10. Bg2 h6 (10... d5 11. Nxd5) 11. O-O {[%csl Rf8]} Qc4 12. Nb3 {[%cal Gb3a5]}) (7... g6 8. g4 d5 {Was played in Yu Yangi - Wei Yi, but after} (8... Bg7 $2 9. g5 Nfd7 10. Nd5 Qd8 11. Be3 $16) (8... b5 9. g5 b4 10. Ncb5 axb5 11. gxf6 Qb7 12. Bg2 gxf5 13. Qe2 Rg8 14. Bf3 Nc6 15. exf5 Nxd4 16. Bxb7 Nxe2 17. Bxa8) 9. Bg2 Nxe4 10. Ndb5 $3 {difficult to spot} (10. Nxd5 Qa5+) 10... axb5 11. Nxd5 Qa5+ 12. b4 {White is winning}) (7... Nbd7 8. g4 b5 {trs} (8... h6 9. Qe2 b5 10. Bf4)) 8. Bg5 Qb6 (8... e6 9. fxe6 fxe6 10. Nxc6) (8... Bd7 9. Bxf6 gxf6 10. Nd5 Qd8 11. Bc4 $16) 9. Nxc6 bxc6 10. Qd2 {black has difficulties developing.} e6 {Korobov} (10... g6 {corr} 11. Na4 (11. Bxf6 exf6 12. O-O-O Rb8 13. b3 gxf5 (13... h5 14. Kb1 Qb4 15. Qe3) 14. exf5 h5 (14... d5 15. Bc4 Ba3+ 16. Kb1 O-O 17. Rhf1 Re8 18. Bxd5 cxd5 19. Nxd5 Qd8 20. Qh6 Rb6 21. Nxb6 Qxb6 22. Rf3 Kh8 23. Rg3 Rg8 24. Rxg8+ Kxg8 25. g4 a5 26. h4) 15. Qe1+ (15. Qe2+ Kd8 (15... Be7 16. Rxd6 Qc5 17. Ne4) 16. Qc4 Be7 17. Qxf7 Qb4 18. Ne2 d5 19. Nd4 Bd7 20. Kb1) 15... Be7 16. Rxd6 Qc5 17. Rd3 Bxf5 18. Re3 Kf8 19. Kb1 Rg8 20. g3 a5 21. Bd3 Be6 22. Rf1) 11... Qb7 (11... Qc7 12. Bxf6 exf6 13. O-O-O) 12. Bxf6 exf6 13. O-O-O Rb8 {[%cal Gb7b4]} 14. a3 gxf5 (14... Be7 15. g4) 15. exf5 Bxf5 16. Bd3 Be6 17. Rhe1 h5 18. Kb1) (10... Qxb2 11. Rb1 Qa3 12. Be2) (10... Rb8 11. Bc4 Qb4 12. b3 $16) 11. O-O-O Rb8 (11... Be7 12. Bxf6 gxf6 (12... Bxf6 13. Na4) 13. Bc4 $16 {[%csl Re7]}) 12. b3 Be7 (12... d5 13. Qe1 Bb4 14. Qg3 Nh5 15. Qf3 Qa5 16. Rd3 Bxc3 17. Rxc3 dxe4 18. Qd1 h6 19. Bh4 O-O 20. Kb2 Nf4 21. Bc4 Qxf5 22. g4 Qe5 23. Bg3) 13. Bf4 (13. Bxf6 gxf6 $13 {Bishop cant go to b3 anymore}) 13... e5 14. Bg5 O-O 15. g4 d5 16. Bxf6 Bxf6 17. exd5 Qb4 18. Qe3 (18. g5 Be7 19. f6 Bc5 20. Kb1 Rd8 21. Qe1 Bg4 22. Be2) 18... e4 19. Nxe4 Qa3+ 20. Kd2 Qa5+ 21. Ke2 (21. Kc1) 21... cxd5 22. Nxf6+ gxf6 23. Kf2) (6... e6 {a common continuation, but usually in the Najdorf, e6 setups give white enough time to put their pieces in good positions, and here its no exception.} 7. Qf3 {And black is already in danger. White just plans to finish development with Be3, long castle and g4.} (7. g4 {is too early:} e5 $1 8. Nf5 Nc6 {[%cal Gg7g6]}) 7... Qc7 (7... Qb6 8. a3 $1 {This way white doesn't have to move their knight from d4.} Nc6 (8... Qxd4 9. Be3) (8... Be7 9. Bd3 O-O 10. Be3 Qxb2 11. Kd2 $1 {Threatening Rhb1 winning the queen, and after} Qb6 12. Nxe6 $18 {White wins the exchange.}) 9. Nxc6 bxc6 10. e5 $14) (7... Be7 8. Be3) (7... e5 {Rare move suggested by the engine, but after} 8. fxe5 dxe5 9. Nf5 $14 {Whites position is pleasant.}) 8. g4 {White has easy play} Nc6 (8... b5 9. g5 Nfd7 10. a3 Bb7 11. Be3 Nc6 12. Bh3 $16) 9. Nxc6 bxc6 10. g5 Nd7 11. Bd2 $16 {[%cal Ge1c1,Gh2h4]}) (6... Nbd7 {Just like e6, this move feels a bit passive, but here the upside is that black can still go e5 in one go.} 7. Be3 (7. Be2 e5 (7... g6 8. Be3) (7... b5 8. O-O Bb7 9. Bf3 e6 (9... e5)) 8. fxe5 Nxe5 {A typical Najdorf structure, which in this case is fine for black.}) (7. g4 Nc5 8. f5 Nfxe4 9. Nxe4 Nxe4 10. Bg2 d5 11. O-O e6 12. fxe6 Bxe6 13. Nxe6 fxe6 14. Be3) (7. Qf3 $6 e5 $10) 7... Nc5 (7... e5 8. Nf5 {is good for white because our bishop on e3 prevents Nc5}) (7... e6 $2 {Played but again here its too slow} 8. g4 $16) (7... g6 8. Be2 {Important to start with this, to not allow ideas connected with Ng4} (8. Qe2 Bg7 (8... e5 9. fxe5 dxe5 10. Nb3 Bb4 $13 11. Qf3) 9. O-O-O O-O 10. g4 e5 11. Nb3 exf4 12. Bxf4 Ne8 13. Qd2 Ne5 14. h3 Be6 15. Be2) 8... Bg7 (8... Qc7 9. O-O Bg7 (9... b5 10. e5) 10. g4 h6 (10... Nc5 11. Bf3 h5 12. g5 (12. gxh5 Bh3 13. hxg6 fxg6) 12... Ng4 13. Nd5 Qd8 14. f5 Nxe3 15. Nxe3 Be5) 11. g5 hxg5 12. fxg5 d5 13. Rf4 Nxe4 14. Nxd5 Qd6 15. Nf3 f5 16. gxf6 Ndxf6 17. Nxf6+ Nxf6 18. Qxd6 (18. Rd4 $36) 18... exd6 19. Bd3 $14) 9. Qd2 O-O 10. O-O-O $14 {White has easy ideas. [%cal Gh2h4,Gh4h5,Ge2f3]}) (7... Qc7 8. g4 $1 Nc5 9. Nd5 $1 Nxd5 10. exd5 {+0,5}) 8. Bc4 {Most testing} (8. Bd3 {Also possible, probably with a slightly better position}) 8... Bg4 (8... Ncxe4 $2 {too greedy:} 9. Nxe4 Nxe4 10. Qh5 g6 11. Qd5 e6 12. Qxe4 d5 {black wins back the piece but after} 13. Qf3 dxc4 14. O-O-O {White has a crushing attack [%cal Gf4f5,Gh1e1]}) (8... e6 9. e5) 9. Nf3 Qc7 (9... b5 10. e5 $1 bxc4 11. exf6 gxf6 12. h3 {Blacks position is very difficult to handle, with white soon finishing development. [%cal Gd1e2,Ge1c1]}) 10. h3 Bxf3 11. Qxf3 e6 12. Bxc5 Qxc5 13. Bb3 $14 O-O-O 14. O-O-O Nd7 15. Kb1 g6 16. Ne2 Bg7 17. c3) (6... Nc6 {Playing a Rauzer with the inclusion of f4-a6} 7. Be3 (7. e5 {Pleasant endgame} dxe5 8. Nxc6 Qxd1+ 9. Nxd1 bxc6 10. fxe5 Nd5 (10... Nd7 11. e6) (10... Ng4 11. Bf4 g5 $1 12. Bxg5 h5 {odd looking} 13. h3 Nxe5 14. Nc3 {[%cal Ge1c1]}) 11. c4 (11. Ne3) 11... Nb4 (11... Nb6 12. Be3) 12. Ne3 a5 13. a3 Na6 14. b4) (7. Nxc6 bxc6 8. e5 Nd7 9. Qf3 d5 10. Bd3 e6 11. O-O) (7. Be2 g6 8. Nxc6 bxc6 9. e5 Nd7 10. exd6 Bg7 11. O-O O-O $44) 7... g6 8. Qd2 (8. h3 Bg7 9. Qf3 Nxd4 (9... O-O 10. O-O-O Bd7 11. Qf2 Nxd4 12. Bxd4 Bc6 13. Bd3) 10. Bxd4 b5 11. O-O-O Bb7 12. h4 h5 13. Nd5 Bxd5 14. exd5 O-O 15. Bxf6 Bxf6 16. Kb1 Qa5 17. c3 b4 18. c4) 8... Bg7 9. O-O-O O-O 10. Be2 Bd7 11. Bf3 Rc8 12. Nxc6 bxc6 13. Rhe1 Qa5 14. e5 dxe5 15. fxe5 Qxe5 16. Bd4 Qa5 17. Rxe7 Be6 18. Qe3 Qb4 19. Rxe6 fxe6 20. Qxe6+ Kh8 21. a3 Qb8 22. Bxc6 Qxh2 23. Bf3) 7. Nb3 $5 {A move that has picked up some steam lately, with just 400 games, many black players will still be surprised by this move.} (7. Nf3 Nbd7 8. a4 {Is the main line.}) 7... Nc6 $1 {Probably the safest move, it prevents the endgame with fxe5, and at the same time keeps the c8 bishops diagonal open preventing g4.} (7... Be7 {The most played move, and also in the Najdorf style, staying flexible with the b8 knight for now.} 8. fxe5 $1 {Quite unexpected, because usually endgames are fine for black in the Sicilian.} dxe5 9. Qxd8+ Bxd8 10. Be3 {But this endgame is not easy for black, their main problem here is the bishop on c8, it doesn't have a good square to develop to.} Nbd7 (10... Nc6 11. O-O-O (11. Be2 {allows} Bg4 $10) 11... O-O (11... Bg4 12. Rd3 $14 {b7 is weak and also we can h3 next asking black the difficult question as to where to go. [%cal Gb3c5,Gh2h3] [%csl Rb7]}) 12. Be2 b5 {Was played in Lodici-Stella, but here after a calm waiting move like} 13. a3 $14 {Black can't finish their development.}) (10... O-O 11. O-O-O b5 12. Be2 Bg4 (12... Bb7 13. a3) 13. Rhe1) (10... Bd7 $5 {Interesting way of playing [%cal Gd7c6]} 11. Be2 Bc6 12. Bf3 Nbd7 13. O-O-O {Still black is passive here} Rc8 (13... a5 14. Nd2) (13... O-O 14. g4 $36 {[%cal Gh2h4,Gg4g5]}) 14. Kb1 b5 (14... h5 15. h3 Kf8 16. g4 hxg4 17. hxg4 Rxh1 18. Rxh1 Kg8 19. g5 Nh7 20. Rg1 f5 21. gxf6 Nhxf6 22. Nd5) 15. a3 $14) (10... Be6 {Important to pay attention here, if black gets to play Nbd7 they got their perfect setup} 11. Nc5 $1 b5 (11... b6 12. Nxe6 fxe6 13. O-O-O {White was clearly better in Maurizzi-Lamard}) (11... Bc8 $1 12. Bd3 b6 13. Nb3 {0.00SF 0.2 Lc0} Nbd7 14. Nd2 Be7 15. Rf1) 12. Nxe6 fxe6 13. a4 $1 (13. a3 $2 Nc6 {[%cal Ga8b8,Gd8b6,Ge8e7]}) 13... b4 14. Na2 $14) (10... h5 {Cool waiting move} 11. h3 {Seems good enough} (11. O-O-O $6 Bg4 $1 {This is the trick, black gets to go Nbd7 next move})) (10... b5 $2 11. a4 b4 12. Nd5 Nxd5 13. exd5 $16 {Long castle next, its difficult for black to coordinate.}) (10... Ng4 11. Bg1 Nf6 {engine likes this} 12. Bd3) 11. Be2 {Waiting with long castle makes sense, so that b5 can be answered with a4} b5 (11... b6 12. O-O-O Bb7 13. Rhf1 Rc8 14. a4 $14) 12. a4 b4 13. Nd5 Nxd5 14. exd5 $16 {Same as against 10...b5, this position is good for white.}) (7... Nbd7 {Either a good move, or a dubious one depending if black knows their next move} 8. g4 d5 $1 (8... h6 {Most played, but passive:} 9. g5 hxg5 (9... Ng8 $7 10. Qh5 g6 11. Qh4 Bg7 12. Qg3 Ne7 13. gxh6 Bxh6 14. h4 $36) 10. fxg5 Nh5 11. Qf3 {Black is in big trouble. [%cal Gc1e3,Ge1c1]}) 9. exd5 Nb6 (9... Bb4 $2 10. Bg2 Nb6 11. fxe5 Nfxd5 12. O-O $18 {White won 2/2 here.}) 10. g5 (10. Bg2 Bxg4 11. Qd3 Bd6 12. f5) 10... Nfxd5 11. Bg2 Nxf4 (11... Bb4 12. O-O) 12. Bxf4 exf4 13. Qxd8+ (13. O-O Qxd1 14. Raxd1 Be7 15. Rxf4 O-O 16. h4) 13... Kxd8 {And the engines quickly claim complete equality, but after} 14. a4 {Black is the one who has to be careful} Kc7 $146 {Best move according to the engine, but requires steel nerves:} (14... Bg4 {Played in Bulmaga - Dinara Wagner} 15. O-O Bd6 16. h3 $146 Be6 (16... Bh5 17. Na5) 17. Rad1 Nc4 18. Ne4 Kc7 19. Nxd6 Nxd6 20. Nc5 $14) 15. a5 Nc4 16. Nd5+ Kb8 17. O-O Bd6 18. Ra4 (18. Rad1) 18... Ne3 19. Nxe3 fxe3 20. Rxf7 $36 {Apparently an equal position, but its definitely black who has to remain careful.} Ra7 $1 21. Rd4 (21. Nd4 Bc5 (21... e2 22. Kf2) 22. Kf1) 21... e2 (21... Re8 22. Rf1) 22. Re4 b6 23. Rxa7 Kxa7 24. Rxe2 Rf8 25. c3) (7... Qc7 8. Be2 {Blacks position is not easy} b5 (8... Nbd7 9. O-O b5 (9... Be7 10. g4 $14 (10. Kh1 b6 $13 (10... O-O 11. g4) (10... b5 11. a4 b4 12. Nd5 Nxd5 13. exd5 O-O 14. Bd2 Rb8 15. c4) (10... h6 {decent} 11. a4 O-O 12. a5) 11. fxe5 dxe5 12. Nd5 Nxd5 13. Qxd5 O-O 14. Rxf7 Rxf7 15. Bc4 Kh8 16. Qxf7 Nf6 17. Bg5) 10... Qc6 11. Bf3) (9... exf4 10. Bxf4 Be7 11. Qe1 $14 {[%cal Gg1h1,Gb3d4,Ge1g3]}) 10. a4 b4 11. fxe5 Nxe5 12. Nd5 Nxd5 13. exd5 g6 14. Kh1 Bg7 15. Bd2 Bf5 16. Rxf5 {EA} gxf5 17. Bxb4 (17. Nd4 Ng4 18. Bxg4 Bxd4 19. Bxf5 Bxb2 20. Qe2+ Be5 21. Re1 a5 22. Qh5 h6 23. h3) 17... Ng4 18. Bxg4 fxg4 19. Qe2+ Be5 20. Na5 O-O 21. Qxg4+ Kh8 22. Qc4) (8... Be6 $2 9. g4 $16) (8... Be7 $2 9. g4 $16) 9. O-O Bb7 (9... Nbd7 {trs}) 10. a4 b4 11. Nd5 Nxd5 (11... Bxd5 12. exd5 Nbd7 13. g4 $36) 12. exd5 Nd7 13. fxe5 {V Nesty} (13. c4 bxc3 14. bxc3 Be7 15. c4 Bf6 (15... O-O 16. fxe5 Nxe5 17. Be3 $14) 16. fxe5 (16. f5 O-O 17. Be3) 16... Bxe5 17. Rb1 O-O 18. Bd2) 13... dxe5 (13... Nxe5 $2 14. a5 $18) 14. c4 (14. Bg5) 14... bxc3 15. bxc3 Nb6 (15... Bd6 16. c4 O-O 17. Be3 Nc5 18. Kh1 a5 19. Qc2 $18) (15... Qxc3 16. Bg5 (16. d6 Rd8 (16... Rb8 17. Ba3) 17. Ba3 Nf6 18. Nc5 (18. Rb1 Qe3+ 19. Kh1 Bxd6 20. Bxd6 Rxd6 21. Qxd6 Bxg2+ 22. Kxg2 Qxe2+ $10) 18... Bc6 19. Qc1) 16... Bd6 17. Kh1 O-O 18. Rc1 Qb4 19. Rc4 Qb6 20. a5 Qa7 21. Bd3 g6 22. Bf5 gxf5 23. Rh4 f6 24. Rxh7 Rf7 25. Rxf7 Kxf7 26. Qh5+ Ke7 27. Qh7+ Ke8 28. Qg6+ Ke7 29. Bxf6+ Nxf6 30. Qg7+ Ke8 31. Qxf6 Bxd5 32. Qxd6 Rd8 33. Qxe5+ Qe7 34. Qg3) 16. c4 Nxc4 17. Kh1 (17. Ra2) (17. Qd3 Rc8 18. Ra2) 17... Be7 (17... Bb4 18. a5 Rc8 19. Ra4) (17... Bd6 18. a5) 18. Qd3 Nd6 19. Bb2 Bf6 20. Rac1 Qd8 21. Ba3) 8. Bd3 {The safest move.} (8. f5 {Going for complications, but quite risky} Be7 (8... b5 9. a4 (9. Bg5 Be7 10. Qd3 Bb7 11. O-O-O Rc8 12. Bxf6 Bxf6 13. Qxd6 Qxd6 14. Rxd6 Ke7 15. Rd1 Nd4 16. Kb1) 9... bxa4 10. Rxa4 Be7 11. Bc4) (8... d5 9. Bg5 Bb4 (9... Nb4 10. a3 d4 11. axb4 Bxb4 12. Bxf6 Qxf6 13. Bb5+ Ke7 14. Qf3 Be6 15. Nxd4 axb5 16. Rxa8 Rxa8 17. Nxe6 Bxc3+ 18. Qxc3 Qh4+ 19. Kd2 Qf2+ 20. Kd3 fxe6 21. Qxe5 Rd8+ 22. Kc3 Qe3+) (9... dxe4 10. Qxd8+ Kxd8 11. Bc4 Ke8 12. Bxf6 gxf6 13. Nxe4 Be7 14. Rf1) 10. Bxf6 Qxf6 (10... gxf6 11. Qxd5 Qxd5 12. exd5 Ne7 13. O-O-O) 11. Qxd5 Qh6 12. Bc4) 9. Bd3 (9. Bg5 Nxe4 10. Nxe4 Bxg5 11. Nxd6+ Ke7 $15 12. Qh5 Qxd6 13. Qxg5+ Qf6 14. Qxf6+ Kxf6 15. Bd3) (9. g4 h6 (9... Nd7) 10. Be3 b5 (10... h5 11. h3 hxg4 12. Qd2) 11. h4 Bb7 12. Bg2 b4 13. Nd5 Nxd5 14. exd5 Bxh4+ 15. Rxh4 Qxh4+ 16. Bf2 Qh2 17. dxc6 Qxg2 18. cxb7 Qxb7 19. Qxd6 Qe4+ 20. Kf1) (9. Nd5 O-O 10. Bd3) 9... O-O (9... g6 10. Bg5 gxf5 11. exf5 d5 12. O-O Rg8 13. Bh4 b5 14. Kh1 Bb7 15. Bxf6 Bxf6 16. Nc5 Ra7 17. Qh5 Ne7 18. Qh6 $18 {Mamedov-Volokitin}) 10. Be3 {Movahed & Idani} (10. a3 $146 d5 (10... b5 11. g4 Nd7 12. Rg1) 11. O-O dxe4 12. Nxe4 Nd4 (12... Nxe4 13. Bxe4) 13. Be3 Nxb3 14. cxb3 Bd7 15. Nxf6+ Bxf6 16. Be4 Bc6 17. Bxc6 bxc6 18. Rc1) (10. Nd5 Nxd5 11. exd5) (10. O-O Nb4 11. Bg5 d5 12. Bxf6 Bxf6 13. Nxd5 Nxd5 14. exd5 Qxd5 15. Qe2) 10... Re8 (10... b5 11. Nd5 Bb7 (11... Nxd5 12. exd5 Na5 (12... Nb4 13. Be4) 13. O-O Nc4 14. Bxc4 bxc4 15. Nd2 a5 16. Qg4) 12. O-O Nxd5 13. exd5 Nb4 14. Be4 Rc8 15. c3 Rc4 16. Bf3 e4 17. cxb4 exf3 18. Na5 Rxb4 19. Nxb7 Qc7 20. Qd2 Rc4 21. Na5 Rc2 22. Qd3 Rxg2+ 23. Kh1 Qxa5 24. Rxf3 Rg4 (24... Qb4 25. Kxg2 Qxb2+ 26. Qd2 Qxa1 27. Bd4 Qb1 28. Bxg7 Rc8 29. Rg3 Qe4+ 30. Kg1 $10) 25. h3 Rh4 26. f6) (10... a5 11. O-O (11. a3) 11... a4 12. Nd2 Nb4 13. Nc4) 11. Qf3 a5 (11... d5 12. Nxd5 Nxd5 13. exd5 e4 14. Bxe4) (11... Nb4 12. O-O-O Nxd3+ 13. Rxd3 b5 14. Bg5 b4 15. Bxf6 Bxf6 16. Nd5) (11... b5 12. O-O-O Bb7 13. Kb1 Nb4 (13... Rc8 14. g4 b4 15. Nd5 Nxd5 16. exd5) 14. Bg5) 12. O-O a4 (12... Nb4) 13. Nd2 a3 14. b3) (8. Be2 Be7 9. O-O O-O (9... Be6 10. f5) (9... exf4 10. Bxf4 O-O 11. Qe1 Be6 12. Rd1 Qc7 13. Kh1 Ne5 14. Nd4 b5 15. a3 Rac8 16. Qg3) 10. Kh1 b5 (10... exf4 11. Bxf4 {[%cal Gd1e1,Ga1d1]}) (10... Re8 11. a4 h6 12. Be3 exf4 13. Rxf4 Be6 14. Nd5 Nd7) 11. a4 b4 (11... Bb7 12. axb5 axb5 13. Rxa8 Qxa8 14. Bxb5 Na7 15. Bd3 Nxe4 16. Nxe4 Bxe4 17. Bxe4 Qxe4 18. Re1 Qf5 19. Na5) 12. Nd5 Bb7 (12... Nxe4 13. Bf3 f5 14. Be3 Be6 15. g4) 13. Be3 Nxd5 (13... Nxe4 14. Bb6 Qd7 (14... Qc8 15. fxe5 dxe5 16. Bd3 f5 17. Na5) 15. Bf3 f5 16. Bxe4 fxe4 17. Nc5 Qc8 18. Nxb7 Qxb7 19. fxe5 Rxf1+ 20. Qxf1 Nxe5 21. Rd1) 14. exd5 Na5 15. Nxa5 Qxa5 16. fxe5 dxe5 17. d6) (8. Bc4 Be7 9. O-O O-O 10. f5 b5 11. Bd5 Bb7 12. Be3 Rc8 13. Rf3 Kh8 (13... Nxd5) (13... Ng4 14. Kh1 Nxe3 15. Rxe3) 14. Rh3) 8... Be7 9. O-O (9. f5 O-O 10. O-O Nb4 11. Bg5) (9. Nd5 Nxd5 (9... exf4 {Black is fine here also}) 10. exd5 Nb4 11. Be4 f5 12. Bf3 a5 13. a4 Qb6 (13... O-O 14. c3 Na6 15. O-O) 14. c3 Na6 15. Nd2 exf4 (15... Qc5 16. Nb3) 16. Nc4 Qc7 17. b3 O-O 18. Bxf4) 9... O-O (9... exf4 {Brkic} 10. Bxf4 (10. Nd5 Nxd5 (10... g5 11. Bd2) 11. exd5 Ne5 12. Bxf4 O-O) 10... O-O 11. Qe1 {Equal position can just follow standard pattern [%cal Ga1d1,Gg1h1,Ge1g3]} Ne5 (11... Be6 12. Rd1 Nd7 13. Be2 Nde5 14. Nd5 $14) 12. Rd1 Be6 (12... Qc7 13. Kh1) 13. Kh1 Nfd7 14. Nd4 Rc8 15. Be2 Re8 (15... Qb6 16. b3) (15... h6 16. Qg3 Bh4 17. Qe3 Re8) 16. Qg3 Bh4 17. Qe3 Nc4 (17... Bf6 18. Qg3) (17... h6 18. a3) 18. Bxc4 Bxc4 19. Rg1) (9... Nb4 10. Be2 O-O 11. Kh1) 10. Nd5 $5 {Although blacks positions is really fine here, at least there are some complications.} (10. h3) (10. Kh1 exf4 11. Bxf4 Re8 12. Qe1) 10... Nxd5 (10... Re8 11. Kh1 (11. f5 Nxd5 12. exd5 e4 13. Bxe4 Bf6 $44) 11... exf4 (11... Nxd5 12. exd5 Nb4 13. Be4 f5 14. Bf3 a5 15. c3 Na6 16. a4 Bf6 17. Be3) 12. Bxf4 Nxd5 13. exd5 Ne5 14. Bxe5 dxe5 15. Bxh7+ Kxh7 16. Qh5+ $10) (10... exf4 11. Nxe7+ Qxe7 12. Bxf4 h6 13. Qe1 $14) (10... h6 11. f5 $14) 11. exd5 Nb4 12. Be4 f5 13. Bf3 a5 14. c3 Na6 15. a4 Qb6+ (15... Bf6) 16. Kh1 Nc5 (16... e4 17. Be2 Bf6 18. Bb5 Rf7 19. Qe2) 17. Nxc5 (17. Ra3) 17... Qxc5 18. Re1 Qc7 19. Qb3 exf4 (19... e4 20. Be2) 20. Bxf4 g5 21. Be3 Bf6 22. g3 Qg7 23. Re2 Bd7 24. Qxb7 Be5 25. Bxg5 $10 *
@@ -22,8 +22,8 @@ Reading:
22
22
  Building (use these — one call for many ops):
23
23
 
24
24
  - **`apply_mutations(id, mutations[])`** — batch. This is the primary build tool. Send 100 mutations in one call → one load-parse-mutate-save cycle instead of 100. When you're writing a repertoire from scratch, EVERYTHING should go through this. Individual mutation tools are for surgical follow-up edits, not for bulk work.
25
- - **`add_line(id, parent_id, sans[])`** — a whole linear variation in one op. Cleaner than N `add_move` ops for straight lines.
26
- - **`auto_evaluate(id, node_id?)`** — walk from a node and populate the persistent `ceoEval` (Stockfish + Lc0 numbers) on every descendant via cloud_analyse. **Does NOT set visible NAGs** — the numbers land in a hidden `[%ceo-eval]` tag so you can read them back later. NAG placement (`$1` `!`, `$14` `⩲`, `$16` `±`, `$18` `+−`, etc.) is your editorial call. Skip nodes that already have a stored eval by default.
25
+ - **`add_line(id, parent_id, sans[])`** — a whole linear variation in one op. Cleaner than N `add_move` ops for straight lines. Overlapping prefixes with an existing branch **join** (same SAN from the same position IS the same move) — see below.
26
+ - **`auto_evaluate(id, node_id?)`** — walk from a node and populate the persistent `ceoEval` (Stockfish + Lc0 numbers) on every descendant via cloud_analyse. **Async job**: returns `{job_id, target_count, estimated_seconds}` immediately, does the work in the background. Poll with `auto_evaluate_status(job_id)` until `done:true`; cancel with `auto_evaluate_cancel(job_id)` if you change your mind (partial progress is checkpointed every 8 nodes so a cancel never loses more than a handful of evaluations). **Does NOT set visible NAGs** — the numbers land in a hidden `[%ceo-eval]` tag so you can read them back later. NAG placement (`$1` `!`, `$14` `⩲`, `$16` `±`, `$18` `+−`, etc.) is your editorial call. Skip nodes that already have a stored eval by default.
27
27
 
28
28
  Per-op mutation tools (single-op calls, use for edits after the bulk build):
29
29
 
@@ -42,8 +42,8 @@ All mutations **auto-save** with optimistic locking. Response includes the new `
42
42
 
43
43
  1. `read_prep_file` — see what's there. Every node has an `id` you'll pass to the mutation and engine/DB tools.
44
44
  2. `apply_mutations([...])` — one call with your whole intended build (a mix of `add_move` / `add_line` for structure, plus any `set_comment`/`set_annotations` you already know at author time, plus any `set_nags` where you already have a clear judgment — novelty `$146`, `!?` speculative sac, obvious `?` blunder in a sideline you're rejecting).
45
- 3. `auto_evaluate(id)` — walk the tree and PERSIST engine numbers on every node. Does not touch visible NAGs. Cheap way to get every position's Stockfish + Lc0 read baked into the file for later reference.
46
- 4. Re-read the file and add NAGs where they carry real signal (see NAG discipline below). Use individual mutation tools for surgical follow-ups (fix one comment, add one arrow, promote a specific variation, prune a branch).
45
+ 3. `auto_evaluate(id)` — spawns a background job that PERSISTS engine numbers on every node. Does not touch visible NAGs. Cheap way to get every position's Stockfish + Lc0 read baked into the file for later reference. Grab the returned `job_id` and either (a) poll `auto_evaluate_status(job_id)` every ~10-30s until done, or (b) fire and do useful work meanwhile (write more of the tree, walk the opponent's repertoire) and check back later — engine walk-time serialises on the per-combo semaphore, so it takes roughly `target_count × movetime_ms` in wall time.
46
+ 4. Once the job reports `done:true`, re-read the file and add NAGs where they carry real signal (see NAG discipline below). Use individual mutation tools for surgical follow-ups (fix one comment, add one arrow, promote a specific variation, prune a branch).
47
47
 
48
48
  The build-cost math: a 200-move file via individual `add_move` calls is 200 saves ≈ 100+ seconds of tool overhead. The same file via one `apply_mutations` call is one save ≈ 500ms. Use batch by default.
49
49
 
@@ -76,6 +76,67 @@ If you know the tree ahead of time (writing from scratch), the deterministic rec
76
76
  {op: "add_line", parent_id: NF3, sans: ["Nc6","Bb5","Bd7"]}
77
77
  ```
78
78
 
79
+ ### add_move / add_line join on existing SAN
80
+
81
+ `add_move` and `add_line` are **idempotent on SAN**: if a child with that SAN already exists under the parent, they return the existing child's id rather than creating a duplicate. In chess, same SAN from the same position IS the same move — two sibling children with SAN "d5" would be nonsensical.
82
+
83
+ This means you can freely send `add_line` batches that share a prefix — they build a clean Y-shape at the divergence point, not duplicate spines.
84
+
85
+ ```
86
+ // Two Ruy Lopez lines that share the first 5 plies, diverging at Black's 3rd move:
87
+ {op: "add_line", parent_id: "r", sans: ["e4","e5","Nf3","Nc6","Bb5","Nf6","O-O"]} // Berlin
88
+ {op: "add_line", parent_id: "r", sans: ["e4","e5","Nf3","Nc6","Bb5","a6","Ba4"]} // Steinitz
89
+
90
+ // Result: single spine e4→e5→Nf3→Nc6→Bb5, then Bb5 has TWO children (Nf6, a6).
91
+ // Both add_line responses report every id on the resulting line, including
92
+ // the joined-prefix nodes, so you can address any of them next.
93
+ ```
94
+
95
+ **Consequence for building repertoires**: you don't have to hand-decompose overlapping variations into a mainline + variation pair. Just send each variation as its own `add_line` from the same starting `parent_id`; the tree will de-dup the shared prefix automatically. Use `promote_variation` afterwards to pick which continuation is the mainline at the branch.
96
+
97
+ ## What good looks like
98
+
99
+ Before writing your first substantial file, call `read_example_prep_files` — it returns two reference PGNs by a strong human coach. Read the full files; the rhythm and comment density are hard to convey in isolated snippets. What follows is a taste, not a substitute.
100
+
101
+ **From an Italian Fried Liver overview (both sides, 1600+ audience):**
102
+
103
+ ```
104
+ 5. Nxf7 {Most principled. Interesting is that not long ago computers were saying this
105
+ is winning for white. But modern strong engines almost immediately proclaim 0.00}
106
+ Bxf2+ 6. Kf1 Qe7 7. Nxh8 d5 8. exd5 Nd4
107
+ 9. d6 $1
108
+ (9. c3 {Most played, and weak engines still like this move, but fail to spot}
109
+ Bg4 10. Qa4+ Nd7 $1 {And black is winning} 11. Kxf2 Qh4+)
110
+ 9... Qxd6 (9... cxd6 $2 10. Kxf2 Ng4+ 11. Kg1 $18 {and the d6 pawn blocks Qc5})
111
+ ```
112
+
113
+ Note the register: *"weak engines still like this move, but fail to spot"* — one sentence orients the reader and cites the failure mode. `$1` marks the resource, `$2` marks the trap, `$18` marks the resulting winning position — every glyph earns its place. No `{Stockfish gives 0.00 depth 22}` verbose engine chatter.
114
+
115
+ **From a Najdorf 6.f4 White repertoire (2200+ audience):**
116
+
117
+ ```
118
+ 6... e6 {a common continuation, but usually in the Najdorf, e6 setups give white
119
+ enough time to put their pieces in good positions, and here its no exception.}
120
+ 7. Qf3 {And black is already in danger. White just plans to finish development with
121
+ Be3, long castle and g4.}
122
+ (7. g4 {is too early:} e5 $1 8. Nf5 Nc6 {[%cal Gg7g6]})
123
+ 7... Qc7
124
+ 8. g4 {White has easy play}
125
+ Nc6 9. Nxc6 bxc6 10. g5 Nd7 11. Bd2 $16 {[%cal Ge1c1,Gh2h4]}
126
+ ```
127
+
128
+ Two `[%cal]` arrows on the last move show the whole plan (castle long, push h4). *"White has easy play"* is a five-word verdict that beats three sentences of hedged eval prose. The parenthesised sideline shows exactly WHY 7.g4 is premature — one variation, one point.
129
+
130
+ **What both files share:**
131
+
132
+ - Named citations: Karpov–Beliavsky, Nepomniachtchi, Mamedov–Volokitin, John Ludvig Hammer 2024. Concrete anchors the reader can look up.
133
+ - Practical framing: *"objectively black has no compensation for the pawn, it does seem black still has some practical chances"* — separates objective from practical explicitly.
134
+ - Move-order guidance in the reader's voice: *"Here it's easy for white to mess up the move order. If black wants to go exd4, you should be ready — d3 is a way to remember that white should castle first."*
135
+ - Terse verdicts at line ends: *"White is a clean pawn up."* *"black is cooked"* *"All correspondence games here ended in a draw."* *"With a very unclear position."*
136
+ - Zero decorative annotation: `[%csl Rf7]` on the Fried Liver f7 attack is one square, one colour, all signal.
137
+
138
+ Study `read_example_prep_files` before writing at scale. The gap between "correct" prep and "prep worth reading" is entirely commentary discipline, and these two files are the target.
139
+
79
140
  ## Variations vs prose
80
141
 
81
142
  **Variations are moves, not sentences.** This was the biggest failure mode of raw-PGN authoring — LLMs writing "if Black plays Be6 White responds with f3" as prose. In the tree model there's no such temptation: variations are `add_move` calls at the branching node.
@@ -133,7 +194,7 @@ Engine numbers go into the NAG, not into prose. Convert the eval to the correct
133
194
  | `\|eval\| ≥ 1.3` | `$18` (+−) | `$19` (−+) |
134
195
  | Sharp, hard to evaluate | `$13` (∞) | `$13` (∞) |
135
196
 
136
- **NAGs are editorial, not automatic.** `auto_evaluate` persists engine numbers on every node but does NOT set visible NAGs — because a repertoire full of 0.00 opening theory does not want a `$10` (=) glyph plastered on every move. That's noise, not signal. NAGs are your call: mark moves that carry a judgment a reader can't derive by looking at the board.
197
+ **NAGs are editorial, not automatic.** `auto_evaluate` (and its `auto_evaluate_status` poll) persists engine numbers on every node but does NOT set visible NAGs — because a repertoire full of 0.00 opening theory does not want a `$10` (=) glyph plastered on every move. That's noise, not signal. NAGs are your call: mark moves that carry a judgment a reader can't derive by looking at the board.
137
198
 
138
199
  Where NAGs actually earn their place:
139
200
 
@@ -149,7 +210,7 @@ Where NAGs are noise: every `$10` "=" glyph on every equal position. If the whol
149
210
 
150
211
  When reading back a file, `node.ceoEval.nag` carries the threshold-derived NAG (`$10` / `$14` / `$16` / `$18` etc.) — treat it as a *suggestion*, not an automatic write. Promote it to a visible NAG only when a glyph on that move actually helps the reader.
151
212
 
152
- **Persisted evals travel with the file.** Every node with an eval gets `ceoEval: { sf: {cp: 25, depth: 32}, lc0: {cp: 30, depth: 18}, nag: "$14" }` on subsequent `read_prep_file` calls. Stored as `[%ceo-eval sf=+0.25/32 lc0=+0.30/18 nag=$14]` inside the PGN comment (an escape tag the app hides from the board view, just like [%cal] / [%csl]). So a re-read after auto_evaluate gives you every position's number without re-running cloud_analyse. Query tools (get_position_stats, get_player_preparation, prep_snapshot) also auto-attach a live `eval` at the request position when a cloud engine is running.
213
+ **Persisted evals travel with the file.** Every node with an eval gets `ceoEval: { sf: {cp: 25, depth: 32}, lc0: {cp: 30, depth: 18}, nag: "$14" }` on subsequent `read_prep_file` calls. Stored as `[%ceo-eval sf=+0.25/32 lc0=+0.30/18 nag=$14]` inside the PGN comment (an escape tag the app hides from the board view, just like [%cal] / [%csl]). So a re-read after auto_evaluate gives you every position's number without re-running cloud_analyse. Query tools (get_position_stats, get_prep_position, prep_snapshot) also auto-attach a live `eval` at the request position when a cloud engine is running.
153
214
 
154
215
  **Engine numbers in prose: brief only.** If you must reference a number, keep it compact:
155
216
 
@@ -160,12 +221,14 @@ When reading back a file, `node.ceoEval.nag` carries the threshold-derived NAG (
160
221
 
161
222
  **Only quote engine numbers for positions you actually called `cloud_analyse` on** (or that carry a `ceoEval` from an earlier auto_evaluate, or an auto-attached `.eval` from a query tool). Do NOT infer an eval for a parent position from its children and then write "both engines 0.00" on the parent — that reads as a measurement but is a guess. If the position wasn't analysed, either analyse it or omit the number: prose can say "both continuations run to 0.00, so this looks balanced" without claiming an eval that wasn't taken.
162
223
 
163
- **When you set `contempt`, attribute it to Lc0 only.** Contempt affects Lc0 exclusively — Stockfish always analyses objectively. So don't write "both engines at contempt −15" — that misleads the reader into thinking SF was biased too. Correct phrasings:
224
+ **When you set `contempt`, attribute it to Lc0 only.** Contempt affects Lc0 exclusively — Stockfish always analyses objectively. So don't write "both engines at contempt −30" — that misleads the reader into thinking SF was biased too. Correct phrasings:
225
+
226
+ - `{Lc0 (contempt −30): 0.00 — even biased toward Black, still balanced. SF objective: 0.00.}`
227
+ - `{With Lc0 nudged toward Black (contempt −30) it still picks Qg6 at 0.00; SF's objective read also 0.00.}`
164
228
 
165
- - `{Lc0 (contempt −15): 0.00 even biased toward Black, still balanced. SF objective: 0.00.}`
166
- - `{With Lc0 nudged toward Black (contempt −15) it still picks Qg6 at 0.00; SF's objective read also 0.00.}`
229
+ If you only quote the Lc0 number, disclose the bias in the same sentence — `{Lc0 gives Black +0.30}` with an undisclosed `contempt=-30` is a false objectivity claim.
167
230
 
168
- If you only quote the Lc0 number, disclose the bias in the same sentence `{Lc0 gives Black +0.30}` with an undisclosed `contempt=-15` is a false objectivity claim.
231
+ Contempt scale is signed 0-100 (same as the web UI's ContemptStrength slider). Typical: `±10-20` a light nudge, `±30-60` real fighting play, `±80-100` maximum steer.
169
232
 
170
233
  ### Before you write any commentary: describe_position
171
234
 
@@ -4,20 +4,20 @@ Opening prep is not a monologue with the position-stats and player-prep endpoint
4
4
 
5
5
  ## Grounding: cite tools, don't fill gaps with chess prose
6
6
 
7
- Every recommendation you make must trace back to an actual tool call in this session — engine output from `cloud_analyse` / `analyse` for evaluations and lines, `get_player_preparation` / `get_position_stats` for statistics, `get_player_profile` / `get_head_to_head` for style and history. **Compute is cheap.** When you don't have the data to justify a claim, run the tool — do not paper over the gap with generic chess wisdom you didn't verify.
7
+ Every recommendation you make must trace back to an actual tool call in this session — engine output from `cloud_analyse` for evaluations and lines, `prepare_opponent` + `get_prep_position` for opponent statistics, `get_position_stats` for the general database, `get_player_profile` / `get_head_to_head` for style and history. **Compute is cheap.** When you don't have the data to justify a claim, run the tool — do not paper over the gap with generic chess wisdom you didn't verify.
8
8
 
9
9
  **Concrete failure modes to catch yourself doing:**
10
10
 
11
11
  - "This is a good line because Black gets the two bishops." → Did Lc0 or Stockfish score it that way? If not, drop the claim.
12
- - "Your opponent hates isolated queen pawn positions." → Did you actually look at their IQP games in `get_player_preparation`? Or is this a training-data pattern?
12
+ - "Your opponent hates isolated queen pawn positions." → Did you actually look at their IQP games via a `prepare_opponent` session? Or is this a training-data pattern?
13
13
  - "The Petroff is a solid choice here." → Did you check the opponent's score as White vs the Petroff, or against Black openings in general? If not, drop it.
14
14
  - "Aim for a Catalan setup, they historically struggle there." → Point at concrete games where they lost in Catalan structures. If you can't, don't say it.
15
- - Inventing move sequences that "look like typical prep" without walking the actual tree via `get_player_preparation` / `prep_snapshot`.
15
+ - Inventing move sequences that "look like typical prep" without walking the actual tree via `get_prep_position` / `prep_snapshot`.
16
16
  - Asserting an opponent's style ("sharp tactician", "endgame grinder") without pointing at their profile data or specific games.
17
17
 
18
18
  The failure mode is *authoritative-sounding recipe = 30% real tool output + 70% chess-book filler*. The user cannot tell which is which; from their side it all looks like analysis. That's worse than saying "I don't have data on this yet — should I run X?"
19
19
 
20
- **Do this instead:** cite the specific numbers ("opponent scores 32% as White vs the Sveshnikov over 47 games, 2023-2025"), the tool that produced them ("via `get_player_preparation`"), and reason from there. If the reasoning wants to extend beyond what the data supports, either run more tools or flag it as your read, not the data's.
20
+ **Do this instead:** cite the specific numbers ("opponent scores 32% as White vs the Sveshnikov over 47 games, 2023-2025"), the tool that produced them ("via a `prepare_opponent` session filtered to classical since 2024"), and reason from there. If the reasoning wants to extend beyond what the data supports, either run more tools or flag it as your read, not the data's.
21
21
 
22
22
  ## Numbers are inputs, not verdicts
23
23
 
@@ -27,6 +27,58 @@ The move statistics endpoints return win %, game counts, and (in the big DB) a `
27
27
  - **Score doesn't automatically indicate a level gap.** A 60% variation isn't necessarily "stronger players crushing weaker ones." Look at the per-move `avgWhite` / `avgBlack` fields (returned on every move statistic) before drawing conclusions about who is playing whom.
28
28
  - **Don't recommend the higher-percentage move just because it's higher.** If 1.b3 scores 60% and 1.d4 scores 50% against a given opponent, that is *not* on its own a case for playing 1.b3 — style, prep depth, transposition risk, and the practical questions below all matter more.
29
29
 
30
+ ## Which DB source: signal vs population
31
+
32
+ Two shards on `get_position_stats`, answering different questions:
33
+
34
+ - **`gm-classical`** (default): pre-aggregated GM classical games (both players ~2500+, real thinking time). Every move is signal, no noise. Sample sizes are smaller. Answers: *what do good players actually play here, and how well does each option score at that level?*
35
+ - **`main`**: the full 11.7M-game database. Wider coverage, cheaper positions included (blitz, weak opponents, blunder-fests). Noisier. Answers: *has this position/move been reached at all, and by whom?*
36
+
37
+ They're not either-or. Reasonable to hit both on the same position for different questions:
38
+
39
+ - **What GMs play here** → `gm-classical`. Even a small count (5-10 games at avgRating 2600) is often the honest answer for a specific tabiya — that's just how many GMs have gone there. Don't dismiss small numbers if the rating context supports them.
40
+ - **Does anyone play this at all** → `main`. Especially useful for a move you're considering that hasn't appeared in `gm-classical`. Zero in the GM DB doesn't automatically mean bad — it might just mean unfashionable. Check `main`: if it has some volume at reasonable avgRating (~2200+), the move has real support even without top-level play; if it's only 1400-rated games or bots, it doesn't.
41
+ - **Sanity-checking a candidate** → both. If a move you're considering shows 3 games in `gm-classical` but 800 in `main` at avgRating 2100, that's a coherent picture (unfashionable at the very top but played by strong players); if `main` shows the same 3 games and nothing else, the move is genuinely rare and you're pioneering.
42
+
43
+ When you do read a `main` result, always eyeball `avgRating` per move before quoting the win%. A 60% score across 200 games at avgRating 1400 is not a recommendation — it's a trap that catches beginners. The same 60% across 200 games at avgRating 2400 is a real finding.
44
+
45
+ Cross-reference tip: if `main` shows a specific move dominating that `gm-classical` doesn't touch, look at *who* — correspondence engines, blitz-only players, and bot accounts skew the `main` totals. A move with 5000 games in `main` at avgRating 1900 and zero in `gm-classical` is probably not something to recommend as GM prep.
46
+
47
+ ## Which prep source: match data density to what you're preparing
48
+
49
+ `prepare_opponent` accepts fide, chesscom, lichess sources — each with per-source colour, date-range, time-control filters. Which sources to combine depends on how much data the opponent actually has and what specific question you're answering:
50
+
51
+ - **Well-known player, well-known question** ("what does Firouzja play against 1.e4?"): FIDE, `time_control: "classical"`, `start_month` = 12–24 months ago. Cleanest signal, big enough sample. Don't dilute with online.
52
+ - **Well-known player, sideline question** ("what would he play in this obscure 20-ply position?"): FIDE all-time-controls first — classical is unlikely to have games in a rare position. If still nothing, add rapid; add online only if you must.
53
+ - **Junior / sub-2400 / comeback player**: FIDE classical alone might give you 5 games. Add FIDE rapid + chesscom + lichess (all filtered to the last 12 months). Signal density improves, but recognize online games are a mixed signal — see next section.
54
+ - **Multiple accounts / handles**: pass them all as sources in one call — `[{type:"fide",fide_id:X}, {type:"chesscom",username:"..."}, {type:"lichess",username:"..."}]`. Same session, more games, no rebuild, all combined into one filtered corpus.
55
+
56
+ The trade-off is always the same: signal quality vs data density. When density is high, prefer quality; when density is low, take what you can get and lower your confidence in the read explicitly ("only 12 games at this branch — the 66% is noisier than the 55% we saw at move 2").
57
+
58
+ ## Reading a chess.com / lichess profile: three shapes
59
+
60
+ Online games are a *mixed* signal. Before treating chesscom/lichess data the same weight as FIDE, spot which of these three profiles the opponent fits — misclassifying is worse than not using online data at all.
61
+
62
+ 1. **Consistent** — plays roughly the same repertoire online and OTB. Gold. Suddenly you have thousands of extra games to see reactions against sidelines and to see where he makes mistakes in the early middlegame under time pressure. Weight online data almost the same as classical FIDE.
63
+
64
+ 2. **Eclectic** — online they play everything under the sun; OTB they have a real narrow repertoire. Still usable, but for a *different* question. Online tells you: (a) which openings he has *seen* and roughly *knows*, (b) which moves he *actually recalls under pressure*, (c) where he makes early mistakes. Online does NOT reliably tell you what he'll play in the game.
65
+
66
+ 3. **Split personality** — online repertoire genuinely differs from OTB. Classic example: a 1.e4 Ruy Lopez player over the board who plays only the King's Gambit on lichess blitz because it's fun. Online is the playground; OTB is serious. **Recognize this and discount the online games entirely** for opening-choice prediction — he's not playing his lichess repertoire in a rated classical game. Online may still surface middlegame tendencies, but "what will he play" belongs to FIDE only.
67
+
68
+ How to distinguish, quickly: compare the top openings from `get_player_profile` (FIDE-based) against the moves he plays from the starting position in an online-only `prepare_opponent` session. High overlap → shape 1. Modest overlap with FIDE openings as a subset of online → shape 2. Different openings entirely → shape 3.
69
+
70
+ Write the profile-shape into the prep file's overview comment as soon as you decide it — future re-reads (and the user) benefit from knowing you already made the call.
71
+
72
+ ## Reversed colours as a scarcity trick
73
+
74
+ For a rare position where the opponent has 0–2 games as their side, it's often worth also checking what they've done in the same position from the OTHER colour. This doesn't tell you what they'd play — it tells you what they've SEEN. Useful when you have no direct data:
75
+
76
+ - **Surprise calculus**: "he's had this position from the White side against people playing it against him — so seeing it from the Black side isn't a real surprise, he knows the ideas."
77
+ - **Structural familiarity**: "he's played this pawn structure before, just from the other colour — his over-the-board understanding will carry over."
78
+ - **Cuts both ways**: if the opponent has played it a lot with the OTHER colour AND had good results, assume he knows both sides thoroughly.
79
+
80
+ Do this by dropping the `color` filter on `prepare_opponent`, or making a second session with the opposite colour — then compare game counts. When density is scarce, information from the "wrong" colour is worth more than no information at all. Just be honest about what it is: knowledge, not prediction.
81
+
30
82
  ## Prep is symmetric — both sides know the same things
31
83
 
32
84
  The single biggest LLM error in prep is treating it like writing a book: "here's what you should play against this opponent's weakness." A real chess opponent:
@@ -41,7 +93,7 @@ Every recommendation should be filtered through: "does this survive the fact tha
41
93
 
42
94
  The opponent's last 12-24 months matter far more than a 10-year career average. Repertoires evolve — a lifelong Najdorf player might have quietly become a Petroff player last year, and their old career stats will lie to you if you skim.
43
95
 
44
- **Related product note (2026-07-23):** the `get_player_preparation` endpoint (compact / LLM view) deliberately strips the per-move `fashionScore` field. At the individual-player level, fashion is trailing noise — the opponent's opening trend is already captured in game dates. `fashionScore` stays on the general-DB endpoint (`get_position_stats`), where it's genuinely useful: it's what the whole top field is playing this month.
96
+ **Related product note:** `get_prep_position` (from a `prepare_opponent` session) deliberately strips the per-move `fashionScore` field. At the individual-player level, fashion is trailing noise — the opponent's opening trend is already captured in game dates. `fashionScore` stays on the general-DB endpoint (`get_position_stats`), where it's genuinely useful: it's what the whole top field is playing this month.
45
97
 
46
98
  ## Prep is a tree, not a line
47
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chessceo/mcp",
3
- "version": "0.31.0",
3
+ "version": "0.33.1",
4
4
  "description": "Model Context Protocol server for chess.ceo — 11.7M+ games, ~1.5M FIDE player profiles, opening preparation, live broadcasts.",
5
5
  "type": "module",
6
6
  "bin": {