openapi: "3.0.3" info: title: ChessTB Explorer API version: "1.0.0" description: | Chess endgame tablebase probe API. Submit a FEN position and receive ranked move lists for Distance to Zeroing (DTZ), Distance to Conversion (DTC), Distance to Mate (DTM), and DTM under the 50-move rule (DTM50). paths: /probe: post: operationId: probePosition summary: Probe a chess position (buffered response) parameters: - $ref: "#/components/parameters/XPrefetch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ProbeRequest" example: fen: "4k3/8/8/8/8/8/8/4K2R w - - 0 1" responses: "200": description: Successful probe content: application/json: schema: $ref: "#/components/schemas/ProbeResponse" "400": description: Invalid FEN, empty body, or missing tablebase coverage content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "413": description: Request body exceeds 4 KB limit "429": description: >- Rate limit exceeded -- PROBE_RATE_LIMIT per client IP, or the separate, tighter PREFETCH_RATE_LIMIT when the request carries "X-Prefetch: 1" (see that parameter above). Retrying after a short wait should succeed. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "502": description: >- Remote (URL) tablebase entries only: the tablebase's HTTP source couldn't be reached for this probe. Retrying may succeed. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "503": description: >- Two distinct causes, both with this status code: (1) tablebase not initialised (TABLEBASE_PATH failed to open) - no error_code is set on the response body in this case; (2) an in-flight probe of this (variant, FEN) pair by another concurrent request didn't finish within PROBE_TIMEOUT_SECS (error_code=probe_timeout). Retrying may succeed for case (2). content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "500": description: Internal server error /probe/stream: post: operationId: probePositionStream summary: Probe a chess position with SSE streaming progress description: | Identical to /probe but returns text/event-stream. Each SSE event has a JSON payload in its data field. Events are emitted as child positions are probed, followed by the final result or an error event. An error event with error_code=probe_timeout means a concurrent request was already probing the same (variant, FEN) pair and didn't finish in time; retrying is usually cheap and likely to resolve. parameters: - $ref: "#/components/parameters/XPrefetch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ProbeRequest" responses: "200": description: SSE stream content: text/event-stream: schema: oneOf: - $ref: "#/components/schemas/ProgressEvent" - $ref: "#/components/schemas/DoneEvent" - $ref: "#/components/schemas/ErrorEvent" "400": description: >- Invalid request body, returned before the stream opens: missing or non-string FEN, empty FEN, FEN over 100 characters, or unsupported variant. An invalid board or a position outside tablebase coverage is reported as an error event on the stream. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "429": description: >- Rate limit exceeded, returned before the stream opens -- PROBE_RATE_LIMIT per client IP, or the separate, tighter PREFETCH_RATE_LIMIT when the request carries "X-Prefetch: 1" (see that parameter above; this is the endpoint the frontend's hover-prefetch actually sets it on). Retrying after a short wait should succeed. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" /health: get: operationId: healthCheck summary: Tablebase readiness check responses: "200": description: Tablebase loaded and ready "503": description: Tablebase not initialised /admin/cache/stats: get: operationId: cacheStats summary: LRU cache hit-rate statistics security: - adminBearer: [] - adminSession: [] responses: "200": description: Cache statistics content: application/json: schema: $ref: "#/components/schemas/CacheStatsResponse" "401": description: Admin authentication required "503": description: Admin panel disabled (ADMIN_TOKEN not configured) /admin/cache/clear: post: operationId: cacheClear summary: Purge both in-process LRU caches, and the tablebase's own block cache if supported security: - adminBearer: [] - adminSession: [] responses: "200": description: Caches cleared content: application/json: schema: $ref: "#/components/schemas/CacheClearResponse" "401": description: Admin authentication required "503": description: Admin panel disabled (ADMIN_TOKEN not configured) /admin/login: post: operationId: adminLogin summary: Start an admin session requestBody: required: true content: application/json: schema: type: object required: [token] properties: token: type: string description: Value of ADMIN_TOKEN (config.py's "Admin" section) responses: "200": description: Session started; response sets the admin session cookie "401": description: Incorrect token "429": description: >- Rate limit exceeded (ADMIN_LOGIN_RATE_LIMIT, per client IP). Retrying after a short wait should succeed. "503": description: Admin panel disabled (ADMIN_TOKEN not configured) /admin/logout: post: operationId: adminLogout summary: End the admin session responses: "200": description: Session ended components: securitySchemes: adminBearer: type: http scheme: bearer description: "Authorization: Bearer " adminSession: type: apiKey in: cookie name: session description: Session cookie set by a successful POST /admin/login parameters: XPrefetch: name: X-Prefetch in: header required: false schema: type: string enum: ["1"] description: >- Set to "1" by the frontend's hover-prefetch (frontend/src/engine/tablebase.js) to route this request to PREFETCH_RATE_LIMIT instead of PROBE_RATE_LIMIT -- a separate, tighter per-client-IP budget, never an exemption from rate limiting (see app.py's _request_probe_limit). A real, user-initiated probe should not set this header. schemas: ProbeRequest: type: object required: [fen] properties: fen: type: string description: >- FEN notation (4–6 fields; shorter forms are auto-padded). The castling-availability field accepts standard KQkq letters (in that order) for variant="standard", or additionally Shredder-FEN/X-FEN rook-file letters (e.g. "A", "Ha") for variant="chess960". Either way, a letter naming a right with no matching king and rook on the board is rejected with a 400 response. A right that does have one is accepted and probed (see TABLEBASE_PATH in the project README). A FEN longer than 100 characters is rejected with a 400 response. example: "4k3/8/8/8/8/8/8/4K2R w - - 0 1" variant: type: string enum: ["standard", "chess960"] default: "standard" description: >- The rules variant "fen" is interpreted under. Never inferred from the FEN itself - the orthodox starting array is itself a legal Chess960 starting position, so the same FEN text is genuinely ambiguous without this field. Omitted or absent defaults to "standard". Board geometry, piece movement, and tablebase material selection are identical between the two variants - only the interpretation of castling rights/moves differs. example: "standard" MoveEntry: type: object properties: san: type: string description: Standard Algebraic Notation of the move plies: type: integer description: >- Distance in plies. 1 for a move that delivers immediate checkmate (the move itself is the mate); 0 for a move that ends the game immediately as a draw (stalemate or insufficient material), for an unprobed/unknown move, or when available is false. order: type: integer description: >- DTC only: pawn pushes still owed before this move's line reaches its next capture or promotion (see chess.chesstb's probe_dtc/ProbeResult docs). Always 0 for dtz/dtm/dtm50 entries. 0 for a DTC entry when the move itself is a capture or a promotion, or when it ends the game outright (checkmate or an immediate draw) - nothing more is owed once the game is decided or the conversion has already happened. A quiet pawn push does not settle the count: it only pays down one of the pushes still owed. is_mate: type: boolean description: True if this move delivers immediate checkmate outcome: type: string enum: [win, cursed_win, draw, blessed_loss, loss, unknown, not_available] available: type: boolean description: >- False when this material's tablebase has no table for this entry's metric (DTZ, DTC, DTM, or DTM50 - WDL is always available), in which case outcome is flatly not_available and plies/order are 0. No metric's row ever derives its answer from another metric. child_fen: type: string nullable: true description: FEN of the position after this move; null for terminal positions draw_reason: type: string nullable: true enum: [stalemate, insufficient_material, null] description: Reason for a draw outcome that ends the game immediately; null otherwise rounded: type: boolean description: >- Present on available moves_dtz entries only. True when plies may still be one ply off after cross-checking the moves against the position's dtz. False for zeroing moves, whose plies is exactly 1, and for moves whose value the cross-check determined. Summary: type: object description: >- Outcome distribution across all legal moves under the 50-move rule, using each resulting position's halfmove clock. Outcomes use DTM50 when available, otherwise DTC (including DTZ-derived DTC for pawnless material). Clock-independent draws, cursed wins and blessed losses count as draws. A move whose clock-aware outcome cannot be resolved counts as unknown, including failed child probes. properties: wins: {type: integer} draws: {type: integer} losses: {type: integer} unknown: {type: integer} ProbeResponse: type: object properties: wdl: type: integer enum: [-2, -1, 0, 1, 2] description: Root position WDL (2=win, 1=cursed win, 0=draw, -1=blessed loss, -2=loss) dtz: type: integer nullable: true description: >- Distance to Zeroing (DTZ) in plies; null when dtz_available is false dtz_available: type: boolean description: False when this material's tablebase has no DTZ table dtz_rounded: type: boolean nullable: true description: >- True when dtz may still be one ply off after the moves have been cross-checked against it. Only cursed wins and blessed losses can be rounded. Null when dtz_available is false. dtc: type: array items: type: integer nullable: true minItems: 3 maxItems: 3 description: >- [wdl_under_dtc, pawn_pushes_still_owed, plies_to_next_conversion]: Distance to Conversion (DTC), the chess.chesstb probe_dtc() pair (pawn pushes still owed, signed plies) preceded by the wdl class; [null, null, null] when dtc_available is false. DTC is always rule50-aware (probed at the position's own halfmove clock, like DTM50), so wdl_under_dtc can differ from the root wdl above the same way dtm50's own wdl can. dtc_available: type: boolean description: >- False when this material's tablebase has no DTC table. A .lzdtc table only exists for material with at least one pawn; chess.chesstb derives an equivalent DTC answer from DTZ/WDL for pawnless material instead, so this is False in practice only when a pawnful material's own DTC table hasn't been generated. dtm: type: integer nullable: true description: Distance to Mate in plies; null when dtm_available is false dtm_available: type: boolean description: False when this material's tablebase has no DTM table dtm50: type: array items: type: integer nullable: true minItems: 2 maxItems: 2 description: >- [wdl_under_50move_rule, plies_to_mate_under_50move_rule]; [null, null] when dtm50_available is false dtm50_available: type: boolean description: False when this material's tablebase has no DTM50 table draw_reason: type: string nullable: true enum: [stalemate, insufficient_material, null] description: Reason the root position itself is an immediate draw; null otherwise moves_dtz: type: array items: $ref: "#/components/schemas/MoveEntry" description: >- All legal moves ranked by Distance to Zeroing (best first). A quiet move that gives up castling rights into a cursed win or blessed loss takes its plies from chess.chesstb's cursed-exit rule: one or two more than the resulting position's dtz. A rounded value is exact whenever the position's dtz and the other moves leave a single possibility. moves_dtc: type: array items: $ref: "#/components/schemas/MoveEntry" description: >- All legal moves ranked by Distance to Conversion (best first); ranked by pawn pushes still owed first, then plies (DTC's own notion of a better line - see MoveEntry.order) moves_dtm: type: array items: $ref: "#/components/schemas/MoveEntry" description: All legal moves ranked by Distance to Mate (best first) moves_dtm50: type: array items: $ref: "#/components/schemas/MoveEntry" description: All legal moves ranked by DTM under the 50-move rule (best first) summary: $ref: "#/components/schemas/Summary" ErrorResponse: type: object properties: error: type: string description: Human-readable error message error_code: type: string enum: [missing_table, remote_unavailable, probe_timeout] description: >- Machine-readable error code (only present for specific errors). remote_unavailable only occurs with a remote (URL) TABLEBASE_PATH, when its HTTP source couldn't be reached. probe_timeout means a concurrent request was already probing this same FEN and didn't finish within PROBE_TIMEOUT_SECS; retrying is generally cheap and likely to succeed, since the original probe has usually finished and been cached by then. piece_count: type: integer nullable: true description: Number of pieces on the board (only when error_code=missing_table) ProgressEvent: type: object properties: status: {type: string, enum: [probing]} completed: {type: integer} total: {type: integer} DoneEvent: allOf: - $ref: "#/components/schemas/ProbeResponse" - type: object properties: status: {type: string, enum: [done]} ErrorEvent: type: object properties: status: {type: string, enum: [error]} error: {type: string} error_code: {type: string, enum: [missing_table, remote_unavailable, probe_timeout]} piece_count: {type: integer, nullable: true} CacheStatsResponse: type: object description: >- Point-in-time hit-rate counters for both in-process LRUs, plus the static config and tablebase tier info they're sized against. properties: evaluate_fen_cache: $ref: "#/components/schemas/CacheCounters" probe_fen_cache: $ref: "#/components/schemas/CacheCounters" config: type: object properties: evaluate_cache_size: {type: integer} probe_cache_size: {type: integer} block_cache_bytes: {type: integer} tablebase: type: object description: Whether the ordered source resolver is configured. properties: configured: {type: boolean} thread_pool: type: object properties: max_workers: {type: integer} parallel_threshold: {type: integer} probe_timeout_secs: {type: number} tablebase_cache: type: object description: >- Per-source cache statistics: block_cache plus one entry named source containing the resolver's own cache statistics. Omitted when nothing reports statistics. additionalProperties: true CacheCounters: type: object properties: currsize: {type: integer} maxsize: {type: integer} hits: {type: integer} misses: {type: integer} hit_rate: {type: number} CacheClearResponse: type: object properties: status: {type: string, enum: [ok]} message: {type: string} evaluate_fen_cache: $ref: "#/components/schemas/CacheCounters" probe_fen_cache: $ref: "#/components/schemas/CacheCounters" tablebase_internal_cache_cleared: type: boolean description: >- Whether the underlying chess.chesstb block cache was also cleared, not just this app's own two LRUs above. tablebase_cache: type: object description: >- Per-source cache statistics: block_cache plus one entry named source containing the resolver's own cache statistics. Omitted when nothing reports statistics. additionalProperties: true