File size: 20,901 Bytes
c03d895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
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