explorer / openapi.yaml
noobpwnftw's picture
Deploy ChessTB Explorer on Hugging Face Spaces
e16348e
Raw History Blame
20.9 kB
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 <ADMIN_TOKEN>"
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