{
"id": "game_draw_rate",
"version": "1.0.0",
"grain": "one provider/game_id",
"inputs": [
"fact_game"
],
"filters": "rated Standard games, excluding source-marked bots, completed results only",
"numerator": "count of eligible games with result 1/2-1/2",
"denominator": "count of eligible games with result in 1-0, 0-1, 1/2-1/2",
"formula": "numerator / denominator",
"null_rules": "unknown results excluded and counted; empty denominator yields null",
"units": "fraction",
"result_schema": {
"numerator": "integer",
"denominator": "integer",
"value": "number|null",
"unknown_results": "integer",
"marked_bots": "integer"
},
"caveats": [
"Historical foundation corpus is not evidence about contemporary players.",
"Excluding marked bots does not establish all other players are human.",
"No opening, time-pressure, or causal claims are supported by this metric."
]
}
{
"id": "opening_usage",
"version": "1.0.0",
"grain": "one provider/game_id in the observed bounded prefix",
"inputs": [
"fact_game.source_opening",
"fact_game.result",
"fact_game.marked_bot"
],
"classification": "source opening family is the trimmed text before the first colon in the provider tag; no local position classifier",
"numerator": "eligible games in selected source opening family",
"denominator": "eligible games with a nonempty source opening family",
"eligible": "rated Standard accepted games with completed result, excluding source-marked bots",
"null_rules": "missing/empty source opening excluded from denominator and reported separately",
"units": "fraction",
"caveats": [
"The 100,000-game prefix is a convenience sample, not an August 2026 population sample.",
"Opening tags are provider supplied; opening choice is confounded."
]
}
{
"id": "opening_player_score",
"version": "1.0.0",
"grain": "one player-game at selected color, opening family, exact time control, and rating band",
"inputs": [
"fact_game",
"fact_player_game"
],
"filters": "completed rated Standard, no source-marked bot, known source opening, known focal rating; half-open rating band",
"win_rate": "wins / (wins + draws + losses)",
"score_rate": "(wins + 0.5 * draws) / (wins + draws + losses)",
"null_rules": "unknown result and missing focal rating excluded and counted in coverage",
"units": "fraction",
"low_support_threshold_games": 100,
"caveats": [
"Descriptive association in the observed prefix only.",
"Do not infer a causal advantage from opening choice.",
"Focal-player games are dependent across players and opponents; M4 uncertainty method must account for this."
]
}
{
"id": "opening_adjusted_score",
"version": "1.0.0",
"grain": "one focal player-game in a selected source Opening family",
"filters": "completed non-marked-bot games; exact color, half-open player rating band, exact base+increment, two named source opening families; recorded opponent rating required for adjustment",
"score": "wins plus 0.5 times draws divided by eligible player-games",
"strata": "player rating minus recorded opponent rating: below -100, -100 to 99, and 100 or more",
"common_weights": "pooled eligible player-game counts over strata represented in both openings, normalized to sum to one",
"adjusted_result": "within-stratum score rates for each opening averaged using identical common weights",
"interval": "400 seeded focal-player cluster bootstrap repetitions within each opening; percentile 2.5 and 97.5 bounds among valid resamples",
"low_support": "fewer than 100 eligible player-games is a project warning",
"null_rules": "missing opponent rating is excluded from adjustment but remains in raw score; a missing common stratum yields no adjusted rate",
"units": "score fraction",
"caveats": [
"Observed ordered prefix only; no month-wide inference.",
"Association, not opening-choice causation.",
"Bootstrap does not model opponent dependence or archive-prefix selection."
]
}
{
"version": "1.1.0",
"name": "observed_white_opening_positions",
"source_grain": "checked source snapshot \u00d7 provider \u00d7 game ID \u00d7 White decision ply",
"analysis_grain": "visit snapshot \u00d7 cohort \u00d7 optional recorded opening family \u00d7 canonical position",
"position_key_version": "standard-fen-first-four-legal-ep-1.0",
"position_key": "Piece placement, side to move, castling rights, and en passant only if legal; first four normalized FEN fields. SHA-256 truncated to 24 hex characters; retained key and collision check.",
"history": "Full FEN, full UCI prefix and repeated-position flag remain in local visit shards. Draw counters and repetition history are not part of the recognition key.",
"default_window": {
"learner_color": "white",
"min_ply": 6,
"max_ply": 20,
"step": 2
},
"game_count": "A game contributes once per canonical position within the window; the earliest visit supplies its route and continuation.",
"frequency": "Distinct games reaching the position / all eligible games in that cohort and, when selected, recorded opening family. Not a personal encounter estimate.",
"opening_family": "Recorded game opening tag before its first colon. ECO is retained separately. Labels do not define or merge board positions.",
"recurring": "At least two distinct games reach the exact position.",
"acyclic_route_count": "Distinct complete UCI arrival prefixes with no repeated canonical position anywhere in that prefix. Looped prefixes are counted in game frequency but not in this route count.",
"transposing": "At least two distinct acyclic arrival prefixes reach the exact position.",
"ranking": "Descending distinct-game count, with position ID as deterministic tie breaker; up to 20 recurring and 20 transposing positions per view.",
"route_share": "Games with this earliest arrival prefix / position games; up to three non-looped examples are published, so displayed shares need not sum to one.",
"continuation_share": "Games choosing this legal next White move at the earliest visit / position games, including games without a next move in the denominator. Report next_move_games separately. Up to five moves are published.",
"top_set_coverage": "Union of distinct games encountering any first-N frequency-ranked position / eligible view games, N=1..20. Overlapping position frequencies must not be added.",
"endpoint_compression": "At a fixed ply, 1 \u2212 distinct canonical endpoint positions / distinct UCI prefixes. Exclude any prefix with a repeated canonical position. Descriptive convergence, not a measured learning benefit.",
"publication": "Separate Elite and broader public cohorts. Publish all opening-family inventory counts, plus all-opening rankings and up to twelve largest known families with at least 1000 eligible games. Local hashes bind retained inputs, extraction plan, parser implementation and artifacts.",
"limits": "Elite is a curated November 2025 month; public sources are nine ordered first-day archive prefixes. Frequency is not move quality, strategic importance, or causal evidence. No engine scoring, personal weighting, drills, or live provider requests.",
"alternative_route_share": "1 - dominant acyclic route games / acyclic position games; acyclic position games = position games - repetition-prefix games. Null for zero acyclic games. Route cards retain all position games as denominator.",
"coverage_curve": "For each N=1..20 in the frequency ranking, union distinct games reaching any first-N position / eligible view games. additional_games is the difference from N-1. This is not a greedy coverage ranking.",
"opening_label_breadth": "Count all distinct known recorded families and all distinct known ECO codes separately; unknown-family games reported separately. Five displayed family/ECO pairs are examples, not exhaustive breadth. Labels can reflect later play.",
"route_remainder": "Other acyclic route games + repetition-prefix games + top-three acyclic route games = position games.",
"public_lenses": {
"d4-g3": {
"label": "1.d4 with g3",
"moves": [
"d2d4",
"g2g3"
]
},
"d4-g3-catalan": {
"label": "Catalan-style fianchetto",
"moves": [
"d2d4",
"c2c4",
"g2g3",
"f1g2",
"e7e6",
"d7d5"
]
},
"d4-g3-kings-indian": {
"label": "King's Indian fianchetto",
"moves": [
"d2d4",
"c2c4",
"g2g3",
"f1g2",
"g8f6",
"g7g6",
"f8g7",
"d7d6"
]
}
},
"lens_policy": "First White move d4; all required UCI moves present by max_ply. Curated move-sequence facets, not exhaustive taxonomy. Denominator is matching games; include eligible positions before and after g3. No personal histories.",
"scatter_scope": "Published ranked positions in the selected view only, deduplicated by position ID; not a corpus-wide distribution."
}
{
"id": "local_user_opening_analysis",
"version": "1.0.0",
"input": "Plain UTF-8 multi-game PGN exported from Chess.com, Lichess or another source. Mainline legal replay from the initial Standard position; variations/comments/NAGs do not contribute visits.",
"limits": {
"bytes": 10485760,
"games": 5000,
"plies": 1000,
"variationDepth": 32,
"files": 20,
"worker_seconds": 60
},
"privacy": "Files and normalized games remain in the browser worker for the session. No account connection, API downloads, model calls, server uploads or persistent storage. Clearing/reloading terminates the worker. Report download is explicit and contains summaries rather than raw PGNs.",
"eligibility": "Completed known results and legal Standard mainlines with named players. Reject variants, setup/FEN games, unfinished or conflicting results, duplicate headers and illegal mainlines. Do not impose reference-corpus rating floors. Missing ratings and casual games are supported; unknown metadata stays unknown.",
"identity": "Provider + native game ID from recognized Site/Link URLs. Chess.com live/daily namespaces stay separate; Lichess uses the first eight game-ID characters. Conflicting provider/native-ID headers are rejected. Generic fallback is SHA256 of normalized player names, date and UCI mainline; source-file SHA256 is separate.",
"deduplication": "Semantic fingerprint of normalized players, UCI moves and completed result excludes comments, variations, NAGs and other optional metadata. Same identity with equal fingerprint counts once; any differing fingerprint withholds all copies of that identity as conflicts. Provider identities never collapse across platforms. First copy supplies optional metadata.",
"reconciliation": "Framed records = accepted unique games + duplicate copies + conflicting copies + rejected records. Failed framing imports no partial file and its unframed records are outside that count. Invalid UTF-8, byte/file/game/ply/variation-depth caps, cancellation or timeout retain no partial session.",
"player_selection": "Explicit selected player per provider; NFC lowercase matching within that provider. A matching name on another provider is not automatically the same person. White first; matched Black games and unmatched-player records are reported separately.",
"personal_denominator": "All selected valid completed Standard White games after explicit date/time-control/rated filters, including short games. Date-filtered undated games are excluded visibly. An optional minimum-20-ply comparison filter has its own exclusion count.",
"position_identity": "First four canonical FEN fields with legal en passant; SHA256 truncated to 24 hex characters, matching the checked public reference. Preserve full FEN separately. Draw counters/history are not part of the recognition key.",
"window": {
"learner_color": "white",
"min_ply": 6,
"max_ply": 20,
"step": 2
},
"position_metrics": "One earliest eligible visit per game/position. Frequency = position games / selected White games. Acyclic route count excludes any repeated-position prefix. Alternative-route share = 1 - largest acyclic route game count / (position games - repetition-prefix games), null for zero acyclic games.",
"reference_scope": "Selected first-N published study positions, N=1..20, for one cohort/view/ranking. Coverage is the union of selected White games reaching those boards. No match means not in this selected set, not unknown in the full corpus or out of theory.",
"route_comparison": "First differing move while the selected published route examples still define a next move, within the first 20 plies. An example ending stops comparison; it is not a departure. A later canonical White decision matching a selected board counts as re-entry. This is not a move-quality or mistake metric.",
"metadata": "Date is a valid UTCDate or Date header, otherwise null; do not infer UTC time. Rated status uses explicit Rated true/false or Rated/Casual/Unrated Event prefixes; Chess.com branding alone does not establish rated status. TimeControl is retained without guessed speed/rating comparability.",
"limits_of_inference": "Personal samples and public reference cohorts differ. Coverage is a descriptive count of the selected reference set. No engine evaluation, learning-gain claim, causal result comparison, username downloads or private-game authentication. Generic fingerprints cannot resolve every missing-ID ambiguity."
}
{
"id": "clock_pressure_error_proxy",
"version": "1.0.0",
"grain": "one selected game x ply, stratified by clock-before bucket",
"sampling": "game ID SHA256 first eight bytes modulo 20 equals zero; deterministic 5% selection from the bounded prefix, independent of annotations",
"eligible": "non-first move by a player in a zero-increment rated Standard game with valid prior same-player clock and a completed result, excluding source-marked bots",
"buckets_seconds": [
"[0,10)",
"[10,30)",
"[30,60)",
"[60,infinity)"
],
"evaluable": "eligible move with source-provided before and after centipawn evaluations; mate transitions excluded and counted separately",
"deterioration": "max(0, mover-perspective cp_before minus cp_after); source cp scores are White-perspective, invert both for Black",
"numerator": "evaluable eligible moves with deterioration >= 200 centipawns",
"denominator": "evaluable eligible moves in bucket",
"coverage_denominator": "all eligible moves in bucket",
"null_rules": "first same-player move, missing/invalid clock, missing evaluations and mate transitions never become zero error",
"units": "fraction",
"caveats": [
"Exploratory proxy, not an official Lichess blunder label.",
"Existing evaluation coverage is selected and source quality varies.",
"The prefix and move sample do not represent all August games; no causal time-pressure claim."
]
}
{
"id": "evaluation_coverage",
"version": "1.0.0",
"grain": "one selected game x ply, by clock-before bucket",
"inputs": [
"fact_move"
],
"filters": "same eligible move and deterministic game sample as clock_pressure_error_proxy",
"numerator": "eligible moves with comparable before and after source centipawn evaluations",
"denominator": "all eligible moves in bucket",
"null_rules": "missing source evals and mate transitions are non-evaluable, never zero deterioration",
"units": "fraction",
"caveats": [
"Source evaluation availability can depend on which games were analyzed.",
"Coverage is conditional on the observed prefix and deterministic move-game sample."
]
}
This worked illustration starts with the authored demo:
3 proxy errors among 6 evaluable moves. Change the number of moves with missing evaluations;
they change coverage, while the proxy rate stays tied to evaluable moves.
Proxy rate 3 / 6 = 50.0%
Evaluation coverage 6 / 8 = 75.0%
Other slider values are hypothetical examples,
not new observations or model results.
contracts/game_draw_rate.json is the versioned definition and result schema.
src/chess_analytics/metrics/draw_rate.sql is the reviewed SQL implementation.
Game draw rate 1.0.0: completed eligible drawn games / completed eligible games,
one count per provider/game ID. Eligible = rated Standard, excluding marked bots.
Unknown results are excluded and reported separately; an empty denominator is null.
Marked bots and unknown results are separate non-overlapping exclusion counts
(bot status takes precedence). Missing rating does not exclude a draw-rate record.
Result includes exact numerator/denominator, fraction, metric/contract versions,
dataset snapshot ID, source hash and caveats. The tiny fixture's manually specified
expected result is 4/20. A separate header-tally script verifies the real corpus
without using the parser, analytical SQL or DuckDB; it does not verify move legality.
Only compare its denominator directly when ingestion reports zero quarantine/conflicts.
This is a descriptive foundation-data check. No uncertainty/population claim,
opening comparison, time-pressure analysis or model benchmark is attached to it.
M3 metrics use the checked partial analytical snapshot and are descriptive only:
Opening usage: source Opening tag family
count divided by eligible completed, rated Standard, non-marked-bot games
with a nonempty source opening family. Unknown
results do not enter the denominator. Missing/unknown source tags are shown,
never silently inferred.
Opening player score: one player-game
in a specified color, rating range and exact time control. Reports wins,
draws, losses, win rate and half-point score rate with explicit denominator.
Clock pressure error proxy:
deterministic game-ID-hash selection within the source prefix; zero-increment
games only. Prior same-player clock determines the bucket. Among eligible
moves with comparable source centipawn evaluations, the numerator counts
=200 cp mover-perspective deterioration. Missing/mate evaluation counts and
coverage accompany the rate.
These definitions are versioned, and M3's independent raw-PGN check
confirms selected arithmetic. Source annotations are not independently
engine-verified. No monthly, population, or causal interpretation is warranted.
M4 adds opening adjusted score:
common pooled weights across three focal-minus-opponent rating-difference
strata, with a seeded focal-player cluster bootstrap. The M4 dashboard and
analyst tool use the same implementation. Its interval describes dependence
within the observed prefix, not uncertainty for all August games.
Governed semantic layer
ADR 0085 gives each core
metric one dbt 1.12 semantic definition. query_metric uses it only when
the current mart matches the analytical snapshot's source and the mart's
model hash matches the project. Any other case, including a failed semantic
query, keeps the reviewed SQL. The result keys stay the ones the evidence
IDs already hash. compare_openings calls query_metric, so it uses the
same path.
Metric
Model
Rule
eligible_games
games view
Sum of rated Standard completed games, excluding source-marked bots
game_draw_rate
games
drawn_games / eligible_games. That ratio is the returned value
opening_usage
games
Games in one family / games with a known family. No ratio metric; the analyst divides two known_opening_games queries
opening_player_score
player_games view
opening_player_score and win_rate are the returned rates. The rating filter, time control, and low_support stay in Python
The family expression is nullif(trim(split_part(source_opening, ':', 1)), '').
player_games copies opening family and time control from the game onto each
player row. game_key is the foreign entity.
Outside this layer
Why it stays out
clock_pressure_error_proxy, evaluation_coverage
They read fact_move, which dbt does not build
opening_adjusted_score
Seeded cluster bootstrap
Zero denominator
Stays null in the analyst
Offline check, after python -m chess_analytics.marts transform:
The compiled draw-rate statement divides sum(drawn_game) by nullif(sum(eligible_game), 0) on games and does not constrain utc_date. The replay plan evals/replay_plans/governed_draw_rate.json asks the analyst the same question.
Distinct games reaching a canonical board at an eligible White decision
Eligible games in selected cohort, recorded family, or public lens
Acyclic routes
Distinct complete earliest-arrival UCI prefixes without repetition
Count, not a percentage or measure of move quality
Alternative-route share
Acyclic position games outside the largest route
Position games minus repetition-prefix games; null if zero
Study-set coverage
Union of games hitting at least one first-N ranked board
Eligible view games; a game counts once across the set
Marginal coverage
Newly covered games when the next ranked board is added
Report count as well as total coverage
Endpoint compression
1 minus distinct endpoints / distinct acyclic routes at one ply
Same nonrepeating visits at that ply; descriptive convergence
Recorded-label breadth
Complete distinct known families and known ECO codes, counted separately
Recorded game tags can reflect later play; five examples are not exhaustive
The two cohort denominators stay separate. Lens frequencies use only matching games;
family filters use recorded-family game counts. Repetition arrivals contribute to
position frequency but not acyclic routes. Route-card shares use all position games,
while alternative-route share uses acyclic games. Draw counters and repetition
history are outside the canonical recognition key.
In the Elite report, the leading Sicilian board has 8,465 games and eight routes,
but 99.29% of acyclic arrivals use one route. The QGD example has 6,431 games and
28 routes, with only 33.28% on its leading route. Frequency and route count alone
would hide that difference. Their alternative-route shares are 0.71% and 66.72%.
Charts and rankings describe selected public games. They do not establish learning
improvement, personal encounter rates, engine quality, or causal outcome differences.
Local imported-game metrics
The import contract defines a separate personal
population. Selected valid completed Standard White games include short games by
default; matched Black games, unmatched players and explicit filter exclusions
are reported separately. No public rating floor is silently applied.
Metric
Definition
Interpretation
Personal board frequency
Earliest eligible board visits / selected White games
Plies 6–20; short games remain in the denominator
Study-set coverage
Union of White games reaching the first N selected published boards / selected White games
N=1..20; selected set only, not full-corpus novelty
Personal alternative-route share
1 − leading acyclic route games / acyclic position games
Repetition-prefix arrivals excluded; null for no acyclic arrivals
First departure and re-entry
First differing move while selected examples define a continuation; later selected canonical-board hit
Stops at example endpoints; not move quality or a mistake count
Counts reconcile imports, duplicates, conflicts, rejections and failed files.
Unknown date/time/rated metadata stays unknown. Personal results do not change
public reports. See the local game guide.
The explicit storage types are in contracts/tables.json.
fact_game: one provider/game ID. Source ordinal is one-based within the
uncompressed export. Original headers are JSON text. Result is 1-0, 0-1,
1/2-1/2, or unknown (*). Standard/rated eligibility is checked during ingestion;
source-marked bots are retained for visible metric exclusion. Fingerprint covers
the trimmed source record, including annotations. Move count is validated by
legal replay. M3 persists a separate sampled move table.
fact_player_game: exactly two rows per accepted game (white, black). Rating
and opponent rating are nullable values recorded at that game, not current
ratings. Lichess uses Glicko-2 despite PGN “Elo” names. Score is 0, 0.5, 1, or null
for unknown result. Missing/anonymous player keys stay null, never one shared ID.
UTC date / played_at: date and nullable TIMESTAMPTZ with precision marker.
Source timestamp uses UTC; missing UTC time is not imputed. Source Date headers
remain in raw metadata but are not assumed to be UTC.
Time control: original text retained, base/increment parsed only for simple
integer base+increment. No guessed speed categories.
Source opening/ECO: provider tags only. Unknown is null. No derived label.
Manifest: one ingestion attempt with source/implementation/config/lock hashes,
versions, limits, ordinal counts, rejection reasons, date coverage, missingness,
elapsed time, artifact hashes, and status/error. Complete input SHA256 does not
mean every input was accepted; reconciliation records exclusions/quarantine.
Quarantine JSONL: source ordinal, non-overlapping disposition and primary
reason, full original PGN. Raw archive remains the source for accepted records.
Reconciliation: seen = accepted + duplicate + excluded + quarantine + conflict.
For conflicts, the whole candidate is withheld, even though first occurrences are
present in staging. Failed framing/byte-limit runs may have only a lower-bound
scanned byte count (last yielded record); no failed run claims full reconciliation.
Source manifests/run records live in JSON during M1; warehouse dimensions, dbt
lineage and recovery records are added in M2.
M3 adds fact_move with the explicit types in
m3_tables.json: one selected provider/game ID and
ply, side to move, SAN/UCI, after-move clock and source evaluation, previous
same-player clock, and derived before/after centipawn values. Missing annotation
values stay null; mate scores are separate from centipawns. A selected zero-ply
game has no fact_move row but remains in the sample manifest. The M3 source
manifest and move manifest explicitly mark the archive partial and retain
source/implementation/contract hashes. Only the observed prefix, not the full
publisher archive, has a verified compressed-byte hash.
Opening-position publication
Local visit shards retain source/provider/game identity, cohort, decision ply,
full FEN, canonical position key and SHA-derived ID, complete UCI prefix, a
repeated-position-prefix flag, next move, and recorded family/ECO. Per-position
aggregation uses a game's earliest eligible visit. The compact tracked report
publishes bounded rankings and examples; it is not the full visit database.
Each view records its game denominator, distinct/recurring/transposing board
counts, positions and frequency rankings. Each ranking includes a cumulative
union-coverage curve with covered games, share and additional games for N=1..20.
Position details retain acyclic games, leading-route games, alternative-route
share, route remainder, complete known-family/ECO breadth and unknown-family
counts. Public lenses have versioned move-sequence rules and matching-game
denominators; white_g3_played_games distinguishes arrivals before and after g3.
The report, configuration, contract, source and implementation hashes are checked
by the site builder. Local full FEN preserves draw counters; the recognition key
uses pieces, turn, castling and legal en passant only.
Session-only imported games
Imported PGN records are held in a browser worker, separate from the immutable
public corpus. Grain: one completed legal Standard game per provider/native ID
or provider/fallback semantic identity. Fields include normalized players, date,
result, optional rated/time-control metadata, full legal UCI mainline and White
visits at plies 6–20. Each visit retains full FEN, canonical first-four-field key,
24-character SHA-derived ID, route, repetition-prefix flag and next move.
File SHA256 identifies bytes separately from semantic game identity. Equivalent
annotation copies count once; differing semantic fingerprints withhold all copies
of that identity. Explicit player selection is scoped to the provider. An exported
summary contains selected aliases, filters, source hashes and derived metrics and
routes, rather than raw PGNs/comments. Records are not persisted or published.
See the contract and guide.