{"openapi":"3.1.0","info":{"title":"Fenpai Open Read API","version":"v1","description":"Read-only OpenAPI discovery contract for bot/AI consumers. Discovery stays at /api/open/v1, while the richer public data surface is exposed under /api/v1/data."},"servers":[{"url":"/","description":"Current Fenpai server"}],"security":[{"bearerAuth":[]}],"paths":{"/api/open/v1/meta/version":{"get":{"summary":"Get Open Read API version metadata","security":[],"responses":{"200":{"description":"Open Read API metadata"}}}},"/api/open/v1/meta/capabilities":{"get":{"summary":"Get capability groups and first-wave endpoints","security":[],"responses":{"200":{"description":"Open Read API capability metadata"}}}},"/api/open/v1/meta/sync-health":{"get":{"summary":"Read PostgreSQL replica freshness and per-stream coverage","responses":{"200":{"description":"Replica mode, status, missing streams, stale streams, revisions, source timestamps, replica timestamps, and lag."},"401":{"description":"Bearer token is required."}}}},"/api/open/v1/openapi.json":{"get":{"summary":"Download the machine-readable OpenAPI document","security":[],"responses":{"200":{"description":"OpenAPI JSON document"}}}},"/api/open/v1/live/match":{"get":{"summary":"Read the current live match summary","responses":{"200":{"description":"Current match summary including a lightweight trackPool summary with trackPoolName, currentTrack, currentSelection, remainingTracks, and lastPlayedTrack."},"404":{"description":"The live match exists but is not public."},"500":{"description":"The server failed to build the live match summary."},"503":{"description":"The live state is temporarily unavailable."}}}},"/api/open/v1/live/tournament":{"get":{"summary":"Read the active tournament snapshot","description":"Returns the active tournament and stage, all groups or one group selected by groupId, A-P broadcast labels, P1-P8 player identities and competition accounts, standings, completed rounds, ordered track directory, record history, and data-quality warnings.","parameters":[{"name":"groupId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current tournament statistics snapshot."},"404":{"description":"The requested tournament group was not found."},"503":{"description":"Tournament statistics are unavailable."}}}},"/api/open/v1/tournaments":{"get":{"summary":"List every tournament and stage independently of the live binding","responses":{"200":{"description":"Bounded tournament and stage catalog."}},"description":" 从 2026-09-14 起，含对决阶梯（`bo9-duel-series`）的赛事额外带 `officialResult`：冠亚季军来自阶段规则里的 `duelSequence` 与 `routes`，逐环用规则引擎判定并按游戏账号跨场认人。阶梯对不上或有未决对决时输出 `provisional` 且不给 `podium`，不发布未经核实的领奖台。"}},"/api/open/v1/tournaments/{tournamentId}/stages/{stageId}":{"get":{"summary":"Read one explicit tournament stage with all groups and evidence","parameters":[{"name":"tournamentId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Explicit stage projection."},"404":{"description":"Tournament or stage was not found."}}}},"/api/open/v1/tournaments/{tournamentId}/stages/{stageId}/groups/{groupId}":{"get":{"summary":"Read one explicit tournament group including P1-P8 identities","parameters":[{"name":"tournamentId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stageId","in":"path","required":true,"schema":{"type":"string"}},{"name":"groupId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Explicit group projection."},"404":{"description":"Tournament, stage, or group was not found."}}}},"/api/open/v1/players":{"get":{"summary":"List canonical players for bot/AI lookup","responses":{"200":{"description":"Player listing"}}}},"/api/open/v1/players/{playerKey}":{"get":{"summary":"Read one player profile by canonical player key or display name","description":"Player profile. Track stats are track-scoped; cross-track average absolute time is intentionally omitted. 从 2026-09-14 起，选手统计里的均值、离散度与比率在「没有样本」时是 `null` 而不是 `0`：整场没有一次有效计时的选手，`avgTimeMs` / `timeStdDev` / `totalTimeMs` / `avgZScore` / `avgGapToTop` 都是 null（此前是 0，会在按平均圈速排序时排到最快）；不足两局算不出离散度时 `rankStdDev` / `consistencyScore` 是 null（此前 consistencyScore 会给 100，也就是「完全稳定」）。新增 `fieldSize` 表示这场比赛每局的出场人数；名次分布仍固定按 1-8 存，但在该赛制下不存在的名次，其比率是 null —— 两人对决里 `thirdRate` 到 `eighthRate` 与 `bottom3Rate` 都是 null，不是 0。计数类字段（roundsPlayed、totalScore、连胜等）仍然是 0，因为那是事实。","parameters":[{"name":"playerKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Player profile"},"404":{"description":"The requested player profile was not found."},"500":{"description":"The server failed to load the player profile."}}}},"/api/open/v1/matches":{"get":{"summary":"List archived matches with lightweight summaries","description":"Returns public historical match summaries. Duo/trio summaries include teams[] and winnerTeams[] so clients can identify teammates and spray-color teams without inferring from rank order. 从 2026-09-15 起，响应的 `data.nextCursor` 与 `meta.pagination` 支持游标分页：`meta.pagination.mode` 为 `cursor` 或 `offset`，`hasMore` 说明还有没有下一页，`versionDrift=true` 表示这一轮翻页途中底层数据变过（有人改分、拆身份或改规则），不是错误，但消费方应据此决定接着读还是从头重来。offset 分页保持原样，老消费方不受影响。","parameters":[{"name":"mode","in":"query","required":false,"description":"Optional mode filter.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Title keyword filter.","schema":{"type":"string"}},{"name":"player","in":"query","required":false,"description":"Player keyword filter.","schema":{"type":"string"}},{"name":"track","in":"query","required":false,"description":"Same-track filter after whitespace-insensitive normalization.","schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"description":"Pagination offset.","schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"description":"Pagination limit.","schema":{"type":"integer"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"游标分页：把上一页 `data.nextCursor` 原样传回来。列表按结束时间倒序，变动都发生在最前面，纯 offset 分页在直播中会重复发送（新封存一场）或静默跳过（撤回一场）记录；游标记的是「上一页最后一条是谁」，不受影响。与 offset 同时给出时游标优先。游标绑定了筛选条件，换了 mode/player/track/q 还沿用旧游标会返回 400 `cursor_filter_mismatch`。"},{"name":"snapshotId","in":"query","required":false,"schema":{"type":"string"},"description":"把这次读取钉在某一代公开语料上。句柄取自 `data.snapshotId` 或 `meta.version.snapshotId`。钉住且仍然有效时 `meta.version.consistency` 为 `verified_snapshot`，并附 `verifiedSnapshot`；该代际已经换掉则返回 **409 `snapshot_expired`**（`details` 带 `requestedSnapshotId` 与 `currentSnapshotId`），请不带 snapshotId 从头读。**旧代际不保留**（`retained: false`）—— 公开 API 由影子副本服务，一代历史压缩后 34 MB，副本刷新时是先清旧代再拉新代的，同时放两代会顶破内存上限；影子库里每条流也只留最新一代。所以这个参数的保证是「要么给你那一代，要么拒绝」，不是「服务端替你留着」。同一个参数在 /api/v1/data 上含义完全相同。"}],"responses":{"200":{"description":"Match list"},"500":{"description":"The server failed to load the match list."}}}},"/api/open/v1/matches/{matchId}":{"get":{"summary":"Get one archived match by id","description":"Returns one public historical match detail. Historical details include event metadata, duo/trio team context, teammate ids, spray colors, Chinese-readable finishTimeText fields, and title phrases such as 35图 or 抢300 that must be interpreted with the terminology dictionary instead of being paraphrased as stages. 从 2026-09-14 起，明细额外带 `resultSemantics`：按结构化赛事规则算出的 `officialResult`（status/basis/ruleId/ruleVersion）、`scoreLeader`（可并列、带 complete 标记）、`seriesScore`（已确认胜局与未决局数）、`competition`（discipline/format/bestOf/winsRequired/targetPoints）、以及 4v4 的 `displaySetScore`（带第几盘与是否已结束）。逐局新增 `validity`、`timeRankComplete`、`scoreRankComplete`，逐条成绩新增 `timeRank`、`scoreRank` 与 participation / finishStatus / timeAvailability / pointsAvailability 四个可用性维度。既有的 `winner`、`rank`、`officialRank`、`score` 一个字未改；`winner` 仍是按总分排序得出的领先者，不是正式赛果，请改读 `resultSemantics.officialResult`。列表接口尚未带这些字段。 从 2026-09-14 起，选手统计里的均值、离散度与比率在「没有样本」时是 `null` 而不是 `0`：整场没有一次有效计时的选手，`avgTimeMs` / `timeStdDev` / `totalTimeMs` / `avgZScore` / `avgGapToTop` 都是 null（此前是 0，会在按平均圈速排序时排到最快）；不足两局算不出离散度时 `rankStdDev` / `consistencyScore` 是 null（此前 consistencyScore 会给 100，也就是「完全稳定」）。新增 `fieldSize` 表示这场比赛每局的出场人数；名次分布仍固定按 1-8 存，但在该赛制下不存在的名次，其比率是 null —— 两人对决里 `thirdRate` 到 `eighthRate` 与 `bottom3Rate` 都是 null，不是 0。计数类字段（roundsPlayed、totalScore、连胜等）仍然是 0，因为那是事实。","parameters":[{"name":"matchId","in":"path","required":true,"schema":{"type":"string"}},{"name":"snapshotId","in":"query","required":false,"schema":{"type":"string"},"description":"把这次读取钉在某一代公开语料上。句柄取自 `data.snapshotId` 或 `meta.version.snapshotId`。钉住且仍然有效时 `meta.version.consistency` 为 `verified_snapshot`，并附 `verifiedSnapshot`；该代际已经换掉则返回 **409 `snapshot_expired`**（`details` 带 `requestedSnapshotId` 与 `currentSnapshotId`），请不带 snapshotId 从头读。**旧代际不保留**（`retained: false`）—— 公开 API 由影子副本服务，一代历史压缩后 34 MB，副本刷新时是先清旧代再拉新代的，同时放两代会顶破内存上限；影子库里每条流也只留最新一代。所以这个参数的保证是「要么给你那一代，要么拒绝」，不是「服务端替你留着」。同一个参数在 /api/v1/data 上含义完全相同。"}],"responses":{"200":{"description":"Match detail"},"404":{"description":"请求的比赛不存在，或从来没有公开过。已经公开过又撤回的请看 410。"},"410":{"description":"这场比赛公开过、现已撤回。错误码 `match_withdrawn`，`details` 带 `matchId` 与 `withdrawnAt`。收到它请删掉本地缓存的那一份 —— 与 404（从不存在，或从未公开）是不同的含义。"},"500":{"description":"The server failed to load the match detail."}}}},"/api/open/v1/leaderboards/players":{"get":{"summary":"Get the global player leaderboard","parameters":[{"name":"sortBy","in":"query","required":false,"description":"Sort key such as firstPlaceCount or firstPlaceRate. Legacy values wins and firstRate are compatibility aliases for round first-place count/rate, not whole-match wins.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Optional leaderboard row limit. Omit it to return the full player leaderboard.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Player leaderboard. firstPlaceCount means round first-place count, and firstPlaceRate means round first-place rate."}}}},"/api/open/v1/leaderboards/rating":{"get":{"summary":"Get the player rating leaderboard (skill and result ratings)","description":"Two ratings on a 0-100 scale with 50 as the baseline. skillRating answers \"who is fast\": it aggregates each round's z-score, which is a driver's finish time relative to the other drivers in that same round on that same track, so it is comparable across tracks. resultRating answers \"who wins\": it aggregates rank percentile. Both are adjusted for opponent strength, weighted by a 180-day half-life, and shrunk toward the baseline when a driver has few rounds, then calibrated against the population spread. team4v4 matches are excluded because they carry no individual ranks. Every row carries drivers[], a plain-language account of where the score came from, and the response carries the method block with the parameters used.","parameters":[{"name":"board","in":"query","required":false,"description":"main (default) lists drivers with at least 3 matches and 50 rounds; rookie lists everyone else; all merges both. The two boards use the same algorithm — they are split because a driver whose entire record is one match faced a fixed field and is not comparable with drivers who race regularly.","schema":{"type":"string","enum":["main","rookie","all"],"default":"main"}},{"name":"sortBy","in":"query","required":false,"description":"skill (default) ranks by skillRating; result ranks by resultRating.","schema":{"type":"string","enum":["skill","result"],"default":"skill"}},{"name":"minConfidence","in":"query","required":false,"description":"Minimum confidence to include: high, medium or low (default). Confidence follows the effective round count; the board split already removes thin samples from the main board.","schema":{"type":"string","enum":["high","medium","low"],"default":"low"}},{"name":"limit","in":"query","required":false,"description":"Optional row limit. Omit it to return every eligible player.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Rating leaderboard with per-row drivers[] and the method block describing how the ratings were produced."},"500":{"description":"The rating leaderboard could not be built."}}}},"/api/open/v1/insights/head-to-head":{"get":{"summary":"Compare two players using historical head-to-head analysis","description":"Head-to-head data is round-based. encounters, winsAgainst, lossesAgainst, and winRate describe direct round results between the two players, not whole-match records.","parameters":[{"name":"playerA","in":"query","required":true,"schema":{"type":"string"}},{"name":"playerB","in":"query","required":true,"schema":{"type":"string"}},{"name":"track","in":"query","required":false,"description":"Optional same-track filter after whitespace-insensitive normalization.","schema":{"type":"string"}}],"responses":{"200":{"description":"Head-to-head insight. Prefer roundEncounters, roundWinsAgainst, roundLossesAgainst, and roundWinRate when present."},"400":{"description":"Missing required playerA/playerB query params."},"404":{"description":"No public head-to-head data matched the request."},"500":{"description":"The server failed to load the head-to-head insight."}}}},"/api/open/v1/overview":{"get":{"summary":"Read global history overview metrics","responses":{"200":{"description":"Global overview"},"500":{"description":"The server failed to load the overview."}}}},"/api/v1/data":{"get":{"summary":"Read the authenticated public data API capability document","responses":{"200":{"description":"Capability document for the /api/v1/data base path"},"401":{"description":"Bearer token is required."}}}},"/api/v1/data/current-match":{"get":{"summary":"Read the current public live match summary","responses":{"200":{"description":"Current public match summary including a lightweight trackPool summary with trackPoolName, currentTrack, currentSelection, remainingTracks, and lastPlayedTrack."},"404":{"description":"The live match exists but is not public."},"500":{"description":"The server failed to build the current public match summary."},"503":{"description":"The live state is temporarily unavailable."}}}},"/api/v1/data/sync-health":{"get":{"summary":"Read PostgreSQL replica freshness and per-stream coverage","responses":{"200":{"description":"Authenticated replica health projection."}}}},"/api/v1/data/current-tournament":{"get":{"summary":"Read the active tournament snapshot","description":"Authenticated equivalent of the active tournament projection, including A-P groups, P1-P8 player identities and competition accounts, standings, completed rounds, ordered track directory, record history, and data-quality warnings.","parameters":[{"name":"groupId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current tournament statistics snapshot."},"404":{"description":"The requested tournament group was not found."},"503":{"description":"Tournament statistics are unavailable."}}}},"/api/v1/data/tournaments":{"get":{"summary":"List every tournament and stage independently of the live binding","responses":{"200":{"description":"Bounded tournament and stage catalog."}},"description":" 从 2026-09-14 起，含对决阶梯（`bo9-duel-series`）的赛事额外带 `officialResult`：冠亚季军来自阶段规则里的 `duelSequence` 与 `routes`，逐环用规则引擎判定并按游戏账号跨场认人。阶梯对不上或有未决对决时输出 `provisional` 且不给 `podium`，不发布未经核实的领奖台。"}},"/api/v1/data/tournaments/{tournamentId}/stages/{stageId}":{"get":{"summary":"Read one explicit tournament stage with all groups and evidence","parameters":[{"name":"tournamentId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Explicit stage projection."},"404":{"description":"Tournament or stage was not found."}}}},"/api/v1/data/tournaments/{tournamentId}/stages/{stageId}/groups/{groupId}":{"get":{"summary":"Read one explicit tournament group including P1-P8 identities","parameters":[{"name":"tournamentId","in":"path","required":true,"schema":{"type":"string"}},{"name":"stageId","in":"path","required":true,"schema":{"type":"string"}},{"name":"groupId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Explicit group projection."},"404":{"description":"Tournament, stage, or group was not found."}}}},"/api/v1/data/players":{"get":{"summary":"List public players with localized display names","parameters":[{"name":"lang","in":"query","required":false,"description":"Localization target such as zh-Hans, zh-Hant, en, ko, or ja.","schema":{"type":"string"}}],"responses":{"200":{"description":"Localized public player directory"}}}},"/api/v1/data/dictionaries":{"get":{"summary":"List public dictionary namespaces","responses":{"200":{"description":"Dictionary namespace index including players, tracks, metrics, and terminology."}}}},"/api/v1/data/dictionaries/{namespace}":{"get":{"summary":"Read one public dictionary namespace such as players, tracks, metrics, or terminology","parameters":[{"name":"namespace","in":"path","required":true,"description":"Dictionary namespace name.","schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"description":"Localization target for displayName values.","schema":{"type":"string"}}],"responses":{"200":{"description":"Dictionary namespace entries. metrics is the field glossary for public stat names and legacy aliases. terminology is the competition wording glossary for 局/小局/场/比赛/轮次, 35图, 抢300, and RT/未完成, and may expose preferredPhrasing plus avoidPhrasing metadata."},"404":{"description":"The requested dictionary namespace was not found."},"500":{"description":"The server failed to load the dictionary namespace."}}}},"/api/v1/data/tracks":{"get":{"summary":"List public tracks with localized names and metadata","parameters":[{"name":"lang","in":"query","required":false,"description":"Localization target. Requests may pass global language codes such as zh-Hans, zh-Hant, en, ko, or ja, but track translation objects themselves only expose zh-Hans, zh-Hant, and ko and otherwise fall back to zh-Hans.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Keyword search across track names and themes.","schema":{"type":"string"}},{"name":"theme","in":"query","required":false,"description":"Filter by theme key or localized theme name (Simplified Chinese, Traditional Chinese, or Korean).","schema":{"type":"string"}}],"responses":{"200":{"description":"Localized track catalog"},"500":{"description":"The server failed to load the public track catalog."}}}},"/api/v1/data/players/{name}/profile":{"get":{"summary":"Read one public player profile by display name","parameters":[{"name":"name","in":"path","required":true,"description":"Player display name or lookup token.","schema":{"type":"string"}}],"responses":{"200":{"description":"Player profile. Track stats are track-scoped; cross-track average absolute time is intentionally omitted."}}}},"/api/v1/data/players/{name}/recent-form":{"get":{"summary":"Read fixed player recent-form windows","parameters":[{"name":"name","in":"path","required":true,"description":"Player display name or lookup token.","schema":{"type":"string"}},{"name":"mode","in":"query","required":false,"description":"Optional mode filter such as individual, duo, or trio.","schema":{"type":"string"}}],"responses":{"200":{"description":"Player recent-form bundle with fixed windows 3d/7d/10d/14d/30d/all."}}}},"/api/v1/data/players/{name}/recent-form/window":{"get":{"summary":"Read one custom player recent-form window","parameters":[{"name":"name","in":"path","required":true,"description":"Player display name or lookup token.","schema":{"type":"string"}},{"name":"days","in":"query","required":false,"description":"Advanced rolling-day window. Use a positive integer such as 17 when you need one custom recent slice instead of the default fixed bundle.","schema":{"type":"integer"}},{"name":"startDate","in":"query","required":false,"description":"Advanced explicit date-range start in YYYY-MM-DD format. Must be paired with endDate, and when both are present they take precedence over days.","schema":{"type":"string"}},{"name":"endDate","in":"query","required":false,"description":"Advanced explicit date-range end in YYYY-MM-DD format. Must be paired with startDate.","schema":{"type":"string"}},{"name":"mode","in":"query","required":false,"description":"Optional mode filter such as individual, duo, or trio.","schema":{"type":"string"}}],"responses":{"200":{"description":"Player recent-form payload for one custom window. Supports rolling-day and date-range queries and returns one normalized window object instead of the fixed bundle."}}}},"/api/v1/data/players/{name}/track-stats/{track}":{"get":{"summary":"Read a player track-specific statistics entry","parameters":[{"name":"name","in":"path","required":true,"description":"Player display name or lookup token.","schema":{"type":"string"}},{"name":"track","in":"path","required":true,"description":"Track name.","schema":{"type":"string"}}],"responses":{"200":{"description":"Player track statistics for one track. Includes bestTimeMs/bestTimeText and should only be compared within the same track."}}}},"/api/v1/data/players/{name}/consistency":{"get":{"summary":"Read a player consistency summary","parameters":[{"name":"name","in":"path","required":true,"description":"Player display name or lookup token.","schema":{"type":"string"}}],"responses":{"200":{"description":"Player consistency summary"}}}},"/api/v1/data/players/{name}/records":{"get":{"summary":"Read a player notable records bundle","parameters":[{"name":"name","in":"path","required":true,"description":"Player display name or lookup token.","schema":{"type":"string"}}],"responses":{"200":{"description":"Player records bundle"}}}},"/api/v1/data/players/{name}/rates":{"get":{"summary":"Read a player rate metrics bundle","parameters":[{"name":"name","in":"path","required":true,"description":"Player display name or lookup token.","schema":{"type":"string"}}],"responses":{"200":{"description":"Player rates bundle"}}}},"/api/v1/data/players/{name}/speed-profile":{"get":{"summary":"Read a player same-track speed profile","parameters":[{"name":"name","in":"path","required":true,"description":"Player display name or lookup token.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Maximum rows to return for ranked sublists such as topByFirstPlaceRate (legacy alias: topByWinRate).","schema":{"type":"integer"}}],"responses":{"200":{"description":"Player same-track speed profile. Returns trackRecords instead of a cross-track raw-time ranking such as topByPb."}}}},"/api/v1/data/head-to-head":{"get":{"summary":"Compare two players with historical head-to-head data","description":"Head-to-head data is round-based. encounters, winsAgainst, lossesAgainst, and winRate describe direct round results between the two players, not whole-match records.","parameters":[{"name":"playerA","in":"query","required":true,"description":"First player display name.","schema":{"type":"string"}},{"name":"playerB","in":"query","required":true,"description":"Second player display name.","schema":{"type":"string"}},{"name":"track","in":"query","required":false,"description":"Optional track filter.","schema":{"type":"string"}}],"responses":{"200":{"description":"Head-to-head analysis. Prefer roundEncounters, roundWinsAgainst, roundLossesAgainst, and roundWinRate when present."}}}},"/api/v1/data/leaderboard":{"get":{"summary":"Read the public player leaderboard","parameters":[{"name":"sortBy","in":"query","required":false,"description":"Sort metric such as firstPlaceCount or firstPlaceRate. Legacy values wins and firstRate are compatibility aliases for round first-place count/rate, not whole-match wins.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Optional maximum rows to return. Omit it to return the full player leaderboard.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Public leaderboard. firstPlaceCount means round first-place count, and firstPlaceRate means round first-place rate."}}}},"/api/v1/data/recent-form-ranking":{"get":{"summary":"Read fixed public recent-form ranking windows","parameters":[{"name":"limit","in":"query","required":false,"description":"Optional maximum rows to return inside each fixed window. Omit it to keep the full ranking list for every returned window.","schema":{"type":"integer"}},{"name":"mode","in":"query","required":false,"description":"Optional mode filter such as individual or duo.","schema":{"type":"string"}}],"responses":{"200":{"description":"Recent-form ranking bundle with fixed windows 3d/7d/10d/14d/30d/all. winRate/firstPlaceRate use a 0-1 ratio, while firstPlaceRatePercent uses a 0-100 percentage."}}}},"/api/v1/data/recent-form-ranking/window":{"get":{"summary":"Read one custom public recent-form ranking window","parameters":[{"name":"limit","in":"query","required":false,"description":"Optional maximum rows to return. Omit it to keep the full qualifying player list for this custom window.","schema":{"type":"integer"}},{"name":"days","in":"query","required":false,"description":"Advanced rolling-day window. Use a positive integer when you need one custom recent slice instead of the default fixed bundle.","schema":{"type":"integer"}},{"name":"startDate","in":"query","required":false,"description":"Advanced explicit date-range start in YYYY-MM-DD format. Must be paired with endDate, and when both are present they take precedence over days.","schema":{"type":"string"}},{"name":"endDate","in":"query","required":false,"description":"Advanced explicit date-range end in YYYY-MM-DD format. Must be paired with startDate.","schema":{"type":"string"}},{"name":"mode","in":"query","required":false,"description":"Optional mode filter such as individual or duo.","schema":{"type":"string"}}],"responses":{"200":{"description":"Recent-form ranking payload for one custom window. Supports rolling-day and date-range queries and returns one normalized window object plus one ranking payload."}}}},"/api/v1/data/tracks/{name}/leaderboard":{"get":{"summary":"Read a same-track full-history leaderboard","parameters":[{"name":"name","in":"path","required":true,"description":"Track name.","schema":{"type":"string"}},{"name":"sortBy","in":"query","required":false,"description":"Sort metric such as pb, played, or winRate (legacy name for same-track first-place rate).","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Optional maximum rows to return. Omit it to return the full same-track leaderboard.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Track leaderboard. PB and displayed time values are only comparable within this one track."}}}},"/api/v1/data/tracks/{name}/speed-ranking":{"get":{"summary":"Read fixed same-track speed-ranking windows","parameters":[{"name":"name","in":"path","required":true,"description":"Track name.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Optional maximum rows to return inside each fixed window. Omit it to keep the full same-track result list for every returned window.","schema":{"type":"integer"}},{"name":"mode","in":"query","required":false,"description":"Optional mode filter such as individual, duo, or trio.","schema":{"type":"string"}}],"responses":{"200":{"description":"Track speed-ranking bundle with fixed windows 3d/7d/10d/14d/30d/all. finishTimeMs and finishTimeText are only comparable within this track and within the same returned window."}}}},"/api/v1/data/tracks/{name}/speed-ranking/window":{"get":{"summary":"Read one custom same-track speed-ranking window","parameters":[{"name":"name","in":"path","required":true,"description":"Track name.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Optional maximum rows to return. Omit it to keep the full qualifying same-track result list for this custom window.","schema":{"type":"integer"}},{"name":"days","in":"query","required":false,"description":"Advanced rolling-day window. Use a positive integer when you need one custom same-track slice instead of the default fixed bundle.","schema":{"type":"integer"}},{"name":"startDate","in":"query","required":false,"description":"Advanced explicit date-range start in YYYY-MM-DD format. Must be paired with endDate, and when both are present they take precedence over days.","schema":{"type":"string"}},{"name":"endDate","in":"query","required":false,"description":"Advanced explicit date-range end in YYYY-MM-DD format. Must be paired with startDate.","schema":{"type":"string"}},{"name":"mode","in":"query","required":false,"description":"Optional mode filter such as individual, duo, or trio.","schema":{"type":"string"}}],"responses":{"200":{"description":"Track speed-ranking payload for one custom window. Supports rolling-day and date-range queries and returns one normalized window object. finishTimeMs and finishTimeText are still only comparable within this track."}}}},"/api/v1/data/matches":{"get":{"summary":"List public archived match summaries","description":"Returns public historical match summaries. Duo/trio summaries include teams[] and winnerTeams[] so clients can identify teammates and spray-color teams without inferring from rank order.","parameters":[{"name":"mode","in":"query","required":false,"description":"Optional mode filter.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Title keyword filter.","schema":{"type":"string"}},{"name":"player","in":"query","required":false,"description":"Player keyword filter.","schema":{"type":"string"}},{"name":"track","in":"query","required":false,"description":"Same-track filter after whitespace-insensitive normalization.","schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"description":"Pagination offset.","schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"description":"Pagination limit.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Public match summaries"}}}},"/api/v1/data/matches/by-day":{"get":{"summary":"Browse public matches grouped by day","parameters":[{"name":"mode","in":"query","required":false,"description":"Optional mode filter.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Title keyword filter.","schema":{"type":"string"}},{"name":"player","in":"query","required":false,"description":"Player keyword filter.","schema":{"type":"string"}},{"name":"track","in":"query","required":false,"description":"Same-track filter after whitespace-insensitive normalization.","schema":{"type":"string"}}],"responses":{"200":{"description":"Public matches grouped by day"}}}},"/api/v1/data/match-history":{"get":{"summary":"Read public match-history rows using the legacy alias endpoint","parameters":[{"name":"mode","in":"query","required":false,"description":"Optional mode filter.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Title keyword filter.","schema":{"type":"string"}},{"name":"player","in":"query","required":false,"description":"Player keyword filter.","schema":{"type":"string"}},{"name":"track","in":"query","required":false,"description":"Same-track filter after whitespace-insensitive normalization.","schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"description":"Pagination offset.","schema":{"type":"integer"}},{"name":"limit","in":"query","required":false,"description":"Pagination limit.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Legacy public match-history rows"}}}},"/api/v1/data/matches/{id}":{"get":{"summary":"Read one full public match detail by id","parameters":[{"name":"id","in":"path","required":true,"description":"Match id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Public match detail. Historical rounds expose roundStats.avgTimeText and results.finishTimeText in Chinese-readable race-time format, and title phrases such as 35图 or 抢300 must be interpreted with the terminology dictionary instead of being paraphrased as stages."},"404":{"description":"The requested public match was not found."},"500":{"description":"The server failed to load the public match detail."}},"description":" 从 2026-09-14 起，明细额外带 `resultSemantics`：按结构化赛事规则算出的 `officialResult`（status/basis/ruleId/ruleVersion）、`scoreLeader`（可并列、带 complete 标记）、`seriesScore`（已确认胜局与未决局数）、`competition`（discipline/format/bestOf/winsRequired/targetPoints）、以及 4v4 的 `displaySetScore`（带第几盘与是否已结束）。逐局新增 `validity`、`timeRankComplete`、`scoreRankComplete`，逐条成绩新增 `timeRank`、`scoreRank` 与 participation / finishStatus / timeAvailability / pointsAvailability 四个可用性维度。既有的 `winner`、`rank`、`officialRank`、`score` 一个字未改；`winner` 仍是按总分排序得出的领先者，不是正式赛果，请改读 `resultSemantics.officialResult`。列表接口尚未带这些字段。 从 2026-09-14 起，选手统计里的均值、离散度与比率在「没有样本」时是 `null` 而不是 `0`：整场没有一次有效计时的选手，`avgTimeMs` / `timeStdDev` / `totalTimeMs` / `avgZScore` / `avgGapToTop` 都是 null（此前是 0，会在按平均圈速排序时排到最快）；不足两局算不出离散度时 `rankStdDev` / `consistencyScore` 是 null（此前 consistencyScore 会给 100，也就是「完全稳定」）。新增 `fieldSize` 表示这场比赛每局的出场人数；名次分布仍固定按 1-8 存，但在该赛制下不存在的名次，其比率是 null —— 两人对决里 `thirdRate` 到 `eighthRate` 与 `bottom3Rate` 都是 null，不是 0。计数类字段（roundsPlayed、totalScore、连胜等）仍然是 0，因为那是事实。"}},"/api/v1/data/overview":{"get":{"summary":"Read public aggregate overview metrics","responses":{"200":{"description":"Public aggregate overview"}}}},"/api/open/v1/live/track-pool":{"get":{"summary":"Read the current live track-pool detail","responses":{"200":{"description":"Current live track-pool detail with trackPoolName, remaining tracks, used tracks, currentSelection, and pending resolutions."},"404":{"description":"The live match exists but its track-pool context is not public."},"500":{"description":"The server failed to build the live track-pool detail."},"503":{"description":"The live state is temporarily unavailable."}}}},"/api/v1/data/current-track-pool":{"get":{"summary":"Read the current public live track-pool detail","responses":{"200":{"description":"Current public live track-pool detail with trackPoolName, remaining tracks, used tracks, currentSelection, pending resolutions, and per-item status."},"404":{"description":"The live match exists but its track-pool context is not public."},"500":{"description":"The server failed to build the current public live track-pool detail."},"503":{"description":"The live state is temporarily unavailable."}}}},"/api/v1/data/track-pool-templates":{"get":{"summary":"List public track-pool templates","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Optional fuzzy template-name query. Accepts human phrases such as G风哥图池 or X哥图池."}],"responses":{"200":{"description":"Public track-pool template library with trackPoolName, trackCount, updatedAt, and optional query by name."},"500":{"description":"The server failed to load the public track-pool template library."}}}},"/api/v1/data/track-pool-templates/{templateKey}":{"get":{"summary":"Read one public track-pool template detail","parameters":[{"name":"templateKey","in":"path","required":true,"schema":{"type":"string"},"description":"Template id or fuzzy human name such as G风哥图池. Fuzzy name lookup resolves to the best public template detail."}],"responses":{"200":{"description":"Public track-pool template detail with tracks array, rawText, lookup metadata, and fuzzy name lookup support."},"404":{"description":"The requested track-pool template was not found."},"500":{"description":"The server failed to load the public track-pool template detail."}}}},"/api/open/v1/track-pool-templates":{"get":{"summary":"List public track-pool templates","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Optional fuzzy template-name query. Accepts human phrases such as G风哥图池 or X哥图池."}],"responses":{"200":{"description":"Public track-pool template library with trackPoolName, trackCount, updatedAt, and optional query by name."},"500":{"description":"The server failed to load the public track-pool template library."}}}},"/api/open/v1/track-pool-templates/{templateKey}":{"get":{"summary":"Read one public track-pool template detail","parameters":[{"name":"templateKey","in":"path","required":true,"schema":{"type":"string"},"description":"Template id or fuzzy human name such as G风哥图池. Fuzzy name lookup resolves to the best public template detail."}],"responses":{"200":{"description":"Public track-pool template detail with tracks array, rawText, lookup metadata, and fuzzy name lookup support."},"404":{"description":"The requested track-pool template was not found."},"500":{"description":"The server failed to load the public track-pool template detail."}}}},"/api/open/v1/matches/withdrawn":{"get":{"summary":"List matches that were published and have since been withdrawn","description":"撤回清单。消费方一次调用就能把本地缓存对齐，不用挨个 id 去探 410。只列「公开过之后又撤回」的比赛（可见性策略里逐条写下 `matchId` 且带 `withdrawnAt` 的那些）；从来没打算公开的比赛不在此列 —— 没有任何消费方缓存过它们，列出 id 只会白白透露存在性。","responses":{"200":{"description":"`{ count, items: [{ matchId, withdrawnAt, note }] }`"},"500":{"description":"The server failed to load the withdrawal list."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API key"}},"schemas":{"SuccessEnvelope":{"type":"object","required":["ok","data","meta"],"properties":{"ok":{"type":"boolean","const":true},"data":{"type":"object"},"meta":{"$ref":"#/components/schemas/Meta"},"links":{"type":"object"}}},"ErrorEnvelope":{"type":"object","required":["ok","error","meta"],"properties":{"ok":{"type":"boolean","const":false},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"object"}}},"meta":{"$ref":"#/components/schemas/Meta"}}},"Meta":{"type":"object","required":["apiVersion","generatedAt"],"properties":{"apiVersion":{"type":"string"},"generatedAt":{"type":"string","format":"date-time"},"analysisScope":{"type":"string"},"source":{"$ref":"#/components/schemas/SourceMeta"},"citations":{"type":"array","items":{"$ref":"#/components/schemas/Citation"}},"pagination":{"$ref":"#/components/schemas/PaginationMeta"},"replica":{"$ref":"#/components/schemas/ReplicaStatus"},"version":{"type":["object","null"],"description":"四维版本向量。`generatedAt` 只说明响应何时生成，不说明内容是哪一代 —— 翻页途中数据被改过，靠时间戳看不出来。取不到的维度是 null，不编一个出来。","properties":{"history":{"type":["integer","null"],"description":"成绩事实的修订号"},"identity":{"type":["integer","null"],"description":"身份与生涯统计的修订号"},"rules":{"type":["string","null"],"description":"赛事与阶段规则的摘要；任一赛事/阶段 revision 变化即变"},"publication":{"type":["string","null"],"description":"公开可见性策略 + 源修订号组成的代际标识；公开撤回或策略变更会换掉它"},"consistency":{"type":"string","enum":["best_effort","verified_snapshot"],"description":"`verified_snapshot` = 客户端传了 snapshotId，服务端核对过整份响应确实来自那一代；`best_effort` = 没钉，可能横跨代际。此前这个值叫 `frozen`，那个词会被读成「服务端替我把这一代留着了」，而我们并没有——改名是为了不让一个词去许一个没有的承诺。"},"verifiedSnapshot":{"type":["object","null"],"description":"只在 consistency 为 verified_snapshot 时出现。`guarantee: single_generation_or_refused` = 要么整份响应都来自你指定的那一代，要么这次请求被拒绝；`retained: false` = 我们**没有**替你把那一代留着，它随时可能换掉，换掉后你会收到 409 snapshot_expired 而不是一份混着两代的数据。","properties":{"snapshotId":{"type":"string"},"guarantee":{"type":"string","enum":["single_generation_or_refused"]},"retained":{"type":"boolean"},"verifiedAt":{"type":"string","format":"date-time"}}},"versionDrift":{"type":"boolean","description":"只在没钉代际、且构造这份响应的过程中确实换过代时出现。不是错误，但这一份可能横跨两代。"},"snapshotId":{"type":["string","null"],"description":"当前公开语料代际的句柄；可回传到 `?snapshotId=` 把读取钉住。两个公开面（/api/open/v1 与 /api/v1/data）都认这个参数。"}}}}},"SourceMeta":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"string"},"confidence":{"type":"number"}}},"ReplicaStatus":{"type":"object","required":["mode","enabled","source","isReplica"],"properties":{"mode":{"type":"string","enum":["primary","replica"]},"enabled":{"type":"boolean"},"source":{"type":"string","enum":["json","postgresql"]},"isReplica":{"type":"boolean"},"status":{"type":"string","enum":["healthy","warming","stale","unavailable"]},"isStale":{"type":"boolean"},"checkedAt":{"type":"string","format":"date-time"},"staleAfterMs":{"type":"integer"},"heartbeatAgeMs":{"type":["integer","null"]},"revision":{"type":["string","null"]},"sourceRevision":{"type":["integer","null"]},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time"},"replicatedAt":{"type":["string","null"],"format":"date-time"},"lagMs":{"type":["integer","null"]},"missingStreams":{"type":"array","items":{"type":"string"}},"staleStreams":{"type":"array","items":{"type":"string"}},"streams":{"type":"array","items":{"$ref":"#/components/schemas/ReplicaStreamStatus"}}}},"ReplicaStreamStatus":{"type":"object","required":["stream","available","isStale"],"properties":{"stream":{"type":"string"},"available":{"type":"boolean"},"isStale":{"type":"boolean"},"revision":{"type":["string","null"]},"sourceRevision":{"type":["integer","null"]},"sourceUpdatedAt":{"type":["string","null"],"format":"date-time"},"replicatedAt":{"type":["string","null"],"format":"date-time"},"lagMs":{"type":["integer","null"]},"ageMs":{"type":["integer","null"]},"semanticHash":{"type":["string","null"]}}},"Citation":{"type":"object","properties":{"kind":{"type":"string"},"id":{"type":"string"},"label":{"type":"string"}}},"PaginationMeta":{"type":["object","null"],"properties":{"offset":{"type":"integer"},"limit":{"type":["integer","null"]},"total":{"type":"integer"}}},"ResultSemantics":{"type":"object","description":"结果语义投影。把「正式赛果」「积分领先者」「两种排序」分开，各自声明依据与完整性。契约见 docs/reviews/2026-09-14/semantic-contract.md。","properties":{"semanticVersion":{"type":"string","enum":["result-contract-2"]},"competition":{"type":"object","properties":{"discipline":{"type":"string","enum":["individual","duo","trio","team4v4"]},"format":{"type":"string","description":"head_to_head / team_series / points"},"resultUnit":{"type":"string","enum":["winner","ranking"],"description":"winner=有唯一胜者；ranking=结果是名次，没有单一冠军"},"ruleId":{"type":["string","null"]},"ruleVersion":{"type":["integer","null"]},"stageKind":{"type":["string","null"]},"bestOf":{"type":["integer","null"]},"winsRequired":{"type":["integer","null"]},"targetPoints":{"type":["integer","null"]}}},"officialResult":{"type":"object","properties":{"status":{"type":"string","enum":["unknown","provisional","confirmed","superseded"]},"winnerEntryIds":{"type":["array","null"],"items":{"type":"string"},"description":"空数组不表示未知或平局；未知一律 null"},"basis":{"type":["string","null"],"enum":["rules_engine","referee_decision","legacy_unverified",null]},"ruleId":{"type":["string","null"]},"ruleVersion":{"type":["integer","null"]},"resultRevision":{"type":["integer","null"]},"sourceRevision":{"type":["integer","null"]},"disagreesWithRulesEngine":{"type":"boolean","description":"归档里的胜者与规则复算结果不一致时出现"}}},"scoreLeader":{"type":"object","description":"某个积分口径下的领先对象，可以并列。**不叫冠军。**","properties":{"entryIds":{"type":["array","null"],"items":{"type":"string"}},"basis":{"type":"string","enum":["recorded_round_points","confirmed_set_wins"]},"complete":{"type":"boolean","description":"false 表示有人的积分不齐，这个排序不能当完整排名用"}}},"seriesScore":{"type":["object","null"],"properties":{"confirmedWins":{"type":"object","additionalProperties":{"type":"integer"}},"unresolvedRounds":{"type":"integer"},"storedWins":{"type":["object","null"],"description":"4v4：封存时写下的聚合盘数"},"storedAgreesWithRounds":{"type":"boolean"}}},"displaySetScore":{"type":["object","null"],"description":"4v4 盘内比分。只给数字无法判断该不该清零，所以带上第几盘与是否已结束。","properties":{"setNumber":{"type":"integer"},"setClosed":{"type":"boolean","description":"true = 这是那一盘的结束比分，下一盘从 0:0 开始"},"smallScore":{"type":"object","additionalProperties":{"type":"integer"}},"pointsToTakeRound":{"type":["integer","null"]}}}}},"TournamentOfficialResult":{"type":"object","description":"赛事级正式结果。冠军不属于任何一场比赛，它是从对决阶梯传导出来的。","properties":{"status":{"type":"string","enum":["unknown","provisional","confirmed","superseded"]},"basis":{"type":["string","null"],"enum":["rules_engine","referee_decision","legacy_unverified",null]},"ruleId":{"type":["string","null"]},"ruleVersion":{"type":["integer","null"]},"stageId":{"type":"string"},"unresolvedDuels":{"type":"integer"},"podium":{"type":["array","null"],"items":{"type":"object","properties":{"routeId":{"type":"string","description":"champion / runner-up / third-place"},"rank":{"type":["integer","null"]},"entryId":{"type":"string"},"entryLabel":{"type":"string"},"decidedByMatchId":{"type":["string","null"]}}}}}},"Pagination":{"type":"object","properties":{"mode":{"type":"string","enum":["offset","cursor"]},"offset":{"type":"integer"},"limit":{"type":["integer","null"]},"total":{"type":"integer"},"hasMore":{"type":"boolean"},"nextCursor":{"type":["string","null"],"description":"下一页的游标；没有下一页时为 null"},"versionDrift":{"type":"boolean","description":"true = 本轮翻页横跨了一次数据变更"}}}}}}