# Validations - the cricket data behind db-mcp.tigzig.com

Tab 1 of 2: Pipeline checks. Tab 2, the anomalies row by row, is at https://validation.tigzig.com/anomalies.md

Everything tallies, end to end. Served-data checks last run 11 Sep 2026, 23:20 IST (17:50 UTC). Refreshed twice a day, and every run is published, pass or fail.

## The chain, in the order the data travels

| Step | Verdict | When |
|---|---|---|
| 1. Cricsheet to master | all 51 checks passed | 10 Sep 2026, 10:08 IST (04:38 UTC) |
| 2. Master to served | all 21 comparisons tally | 11 Sep 2026, 23:05 IST (17:35 UTC) |
| 3. Served data, over the public API | all 22 checks passed |  |

## 1. Cricsheet to master

Last build that loaded new data: 10 Sep 2026, 10:08 IST (04:38 UTC), 31 matches added, 51 of 51 checks passed. Since then the pipeline has run 3 more times and found nothing new at the source; the latest was 11 Sep 2026, 22:08 IST (16:38 UTC).

| Result | Check | Expected | Actual |
|---|---|---|---|
| PASS | ZIP vs DB match count (T20) | 5700 | 5700 |
| PASS | ZIP vs DB match count (ODI) | 3182 | 3182 |
| PASS | ZIP vs DB match count (TEST) | 918 | 918 |
| PASS | ZIP vs DB match count (IPL) | 1243 | 1243 |
| PASS | Match counts by type+gender never regress | all segments new >= old | none |
| PASS | Delivery counts by type never regress | all types new >= old | none |
| PASS | No orphan ball_by_ball match_ids | 0 | 0 |
| PASS | Max date per type extends or stays | new max >= old max ({'T20': '2026-09-01', 'ODI': '2026-09-01', 'TEST': '2026-08-27', 'IPL': '2026-05-31'}) | {'T20': '2026-09-09', 'ODI': '2026-09-09', 'TEST': '2026-08-27', 'IPL': '2026-05-31'} |
| PASS | No NULL match_type / start_date | 0 | type=0 date=0 |
| PASS | No NULL team1/team2 | 0 | 0 |
| PASS | No teams lost | 131 old teams all present | 131 teams, missing: none |
| PASS | Winner is team1/team2/NULL | 0 | 0 |
| PASS | Toss winner is team1/team2/NULL | 0 | 0 |
| PASS | No duplicate match_ids | 0 | 0 |
| PASS | No negative runs_off_bat | 0 | 0 |
| PASS | All wicket types recognized | known types only | all known |
| PASS | Avg deliveries/match in expected band | {'TEST': (1000, 2500), 'ODI': (300, 700), 'T20': (150, 400)} | all in band |
| PASS | Innings domain valid per format | TEST 1-4; ODI/T20 innings>2 only as a short super over | 0 violations; max innings {'T20': 8, 'ODI': 4, 'TEST': 4, 'IPL': 6} |
| PASS | No innings exceeds 10 dismissals | max 10 per innings | 0 innings over 10; observed max 10 |
| PASS | No-result share per format in band | {'TEST': (12.0, 28.0), 'ODI': (2.0, 10.0), 'T20': (1.0, 8.0), 'IPL': (0.5, 8.0)} | in band: {'T20': 2.9, 'ODI': 4.9, 'TEST': 19.5, 'IPL': 2.0} |
| PASS | Duplicate ball slots explained by long overs | 0 | 0 duplicate slots not inside a 10+ delivery over |
| PASS | Multi-value fields ordered and consistent | 0 on every invariant | all consistent |
| INFO | Cricsheet duplicate player-of-match rows | reported as filed, not corrected | 1 match(es) list the same player twice: 1394759 |
| INFO | Low-delivery matches (<20, abandoned) |  | 16 |
| PASS | All old match_ids preserved | 11012 old ids all present | lost: 0 |
| PASS | Spot-check: 50 old matches identical delivery counts | 0 mismatches | 0 mismatches |
| INFO | Venue/city quality |  | venues 602->602, null city 634 |
| PASS | ICC classification distribution stable | per-class change <= 72 appearances | {"Full Member": 11361, "Associate": 6248, "null": 2486, "Associate-ODI": 1991} |
| PASS | Added match volume plausible | <= 1500 added | 31 |
| PASS | Retained matches: per-delivery values unchanged | identical sums over 11012 retained matches | no drift |
| PASS | Retained matches: metadata values unchanged | identical team/winner/toss/venue/date/gender profile | no drift |
| PASS | Schema identical (column names, order, types) | staging schema == live schema | identical |
| PASS | All expected tables present in staging | 4 tables: ['match_info', 'ball_by_ball', 'match_players', 'people'] | all present |
| PASS | Cricsheet published counts tally | our counts within 5 of cricsheet's published figures | exact tally on every format and gender |
| PASS | Source CSV matches DB values (sampled) | 0 mismatches over 150 matches / 598,024 field comparisons | verbatim match |
| PASS | No unknown cricsheet info keys | 37 consumed + 0 declined | no unknown keys |
| INFO | Non-match files in zips are skipped, not ingested | reported, not judged | 1 skipped: ['IPL:all_matches.csv'] |
| PASS | Every match has a full player registry | 0 matches with fewer than 12 named people | 0 thin/absent |
| PASS | Player identifiers are 8-char hex | 0 | 0 |
| PASS | One row per person per match | 0 duplicate names, 0 duplicate ids | names=0 ids=0 |
| PASS | Player role values recognized | parts drawn from ['match_referee', 'player', 'reserve_umpire', 'tv_umpire', 'umpire'] | all recognized |
| INFO | Registry names cricsheet does not place | reported, not judged | 1516 rows with no role |
| INFO | Named officials absent from the match registry | reported, not judged | 5 rows with no identifier |
| PASS | Ball-by-ball names resolve in the match registry | >= 99.95% of name references resolve | 99.9938% (947 of 15,383,576 unresolved) |
| PASS | People identifiers unique | 0 | 0 |
| PASS | People row count in band | 14,000 - 30,000 | 18,539 |
| PASS | Every match identifier exists in people | 0 | 0 |
| PASS | Post-deploy: live match counts == staging | {"IPL\|male": 1243, "ODI\|female": 611, "ODI\|male": 2571, "T20\|female": 2161, "T20\|male": 3539, "TEST\|female": 24, "TEST\|male": 894} | {"IPL\|male": 1243, "ODI\|female": 611, "ODI\|male": 2571, "T20\|female": 2161, "T20\|male": 3539, "TEST\|female": 24, "TEST\|male": 894} |
| PASS | Post-deploy: live delivery count == staging | 5037109 | 5037109 |
| PASS | Post-deploy: live match_players count == staging | 292532 | 292532 |
| PASS | Post-deploy: live people count == staging | 18539 | 18539 |

## 2. Master to served

Checked 11 Sep 2026, 23:05 IST (17:35 UTC) against pipeline build 20260910_043812.

| Result | Slice | Master | Served |
|---|---|---|---|
| PASS | matches, IPL men | 1,243 | 1,243 |
| PASS | matches, ODI women | 611 | 611 |
| PASS | matches, ODI men | 2,571 | 2,571 |
| PASS | matches, T20 women | 2,161 | 2,161 |
| PASS | matches, T20 men | 3,539 | 3,539 |
| PASS | matches, TEST women | 24 | 24 |
| PASS | matches, TEST men | 894 | 894 |
| PASS | matches total | 11,043 | 11,043 |
| PASS | deliveries, IPL | 295,732 | 295,732 |
| PASS | deliveries, ODI | 1,683,966 | 1,683,966 |
| PASS | deliveries, T20 | 1,289,472 | 1,289,472 |
| PASS | deliveries, TEST | 1,767,939 | 1,767,939 |
| PASS | deliveries total | 5,037,109 | 5,037,109 |
| PASS | max date, IPL | 2026-05-31 | 2026-05-31 |
| PASS | max date, ODI | 2026-09-09 | 2026-09-09 |
| PASS | max date, T20 | 2026-09-09 | 2026-09-09 |
| PASS | max date, TEST | 2026-08-27 | 2026-08-27 |
| PASS | match players total | 292,532 | 292,532 |
| PASS | people total | 18,539 | 18,539 |
| PASS | null gender rows | 0 | 0 |
| PASS | null team type rows | 0 | 0 |

## 3. Served data, over the public API

The API answers from two database engines. Most of these checks run against both
and compare them row for row. A few run against one engine only: they read every
delivery ever recorded, which one of the two cannot finish inside its own time
limit. The Engines column says which applies to each check, and the first row
compares the two engines directly on everything added recently, which is where a
half-finished refresh would show.

| Result | What it proves | Engines | Check | Remarks |
|---|---|---|---|---|
| PASS | The two engines behind the API hold identical ball data for everything added recently. | both agree · 5 rows | `engine_parity_recent` | The API answers from two independent engines and this compares them on everything added recently, format by format. History is not re-compared on every run because it does not change - a difference between the engines can only be introduced by a refresh, which is what this window covers. |
| INFO | The column list and its order have not changed, on any of the four published tables. | both agree · 104 rows | `schema_published_tables` |  |
| INFO | Table sizes, match counts and the date span they cover. | DuckDB only · 2 rows | `row_counts` |  |
| INFO | Format and gender mix, and the date span of each. | both agree · 7 rows | `matches_by_type` |  |
| PASS | No ball and no squad entry belongs to a match that does not exist. | both agree · 2 rows | `orphan_balls` |  |
| PASS | Every match has ball-by-ball data behind it. | both agree · 4 rows | `matches_without_balls` | Ball-by-ball data is now served for every published format, so this should stay at or near zero. It last meant something different: until the formats were consolidated, Test matches legitimately had no ball data and a non-zero count here was expected. |
| PASS | Every player named in a squad exists in the player master. | both agree · 1 row | `squad_integrity` | Squad role is deliberately not asserted here. It is legitimately empty for substitute fielders, who appear in a match without a batting or bowling role. |
| PASS | Every player in the player master has one unique identifier. | both agree · 1 row | `reference_table_counts` | The player master carries cross-reference keys to the major cricket sites, so a player can be matched to another source. The keys are sparse by nature and are counted rather than asserted. |
| PASS | No missing venue, bowler, striker, runs or season. | DuckDB only · 4 rows | `null_density` |  |
| PASS | Innings numbering and the ten-wicket ceiling hold in every format. | DuckDB only · 4 rows | `format_shape_validity` | Wickets are counted excluding retired hurt and retired not out, which do not end an innings. Retired out IS a dismissal under Law 25.4.3 and is counted. Test cricket has no over limit, so the deepest over reached is reported rather than asserted. |
| PASS | No T20 or IPL innings past over 19, no ODI innings past over 49. | DuckDB only · 1 row | `over_index_bounds` |  |
| PASS | No delivery scores below 0 or above 8 runs. | DuckDB only · 1 row | `negative_or_absurd_runs` |  |
| PASS | Each delivery can be pinned to one over and one ball within it, with no gaps and no duplicates. | DuckDB only · 1 row | `delivery_numbering` |  |
| PASS | Every match records the toss, and the toss winner was playing in it. | both agree · 4 rows | `toss_recorded_and_valid` | Every match records the toss and the decision, and in every one the toss winner is one of the two teams playing. |
| INFO | Every result is won by runs or by wickets, never both. | both agree · 4 rows | `result_margin_shape` | Matches with no winner are ties, abandonments and drawn Tests, so a non-zero count there is correct. A small number name a winner with no margin at all, the best known being the 2006 Oval Test, which England won by forfeit and which therefore has no margin by definition. Those rows are listed in full below. |
| PASS | Every match is exactly one of won, drawn, tied or no result, and a super-over decision sits inside tied rather than beside it. | both agree · 4 rows | `outcome_partition` |  |
| PASS | A super-over or bowl-out winner is always one of the two teams playing, and only ever appears on a tied match. | both agree · 1 row | `tiebreak_fields_sane` |  |
| INFO | Recent matches are present, so the twice-daily refresh is running. | both agree · 40 rows | `recent_matches` |  |
| INFO | Test matches are current in the match table. | both agree · 8 rows | `recent_test` |  |
| INFO | Deliveries per over stay in a plausible range. | DuckDB only · 8 rows | `balls_per_over_extremes` | Overs longer than six deliveries are wides and no-balls, both of which are re-bowled. A long tail is ordinary cricket rather than a defect; a sudden jump at the top of the range would not be. |
| INFO | Innings counts per match are 1, 2, or a super over. | DuckDB only · 15 rows | `innings_per_match` | Three or more innings in a limited-overs match is a super over, which is legitimate and rare. The deepest case on record went to three consecutive super overs in one T20. |
| INFO | How often one delivery slot carries more than one row. | DuckDB only · 4 rows | `repeated_delivery_address` | Explained, and it is not cricket. The source numbers deliveries as a running count within the over, so a ten-ball over reaches .10, which is indistinguishable from .1 once stored as a decimal. One collision per long over, exactly. This is why the data also carries the over and the delivery within it as separate whole numbers, and those are asserted separately. |

PASS is an assertion that held. INFO is a distribution published for inspection
rather than asserted - deliveries per over and innings per match are legitimately
variable.

## Previous runs of the served-data checks

| Run | Checks | Failed |
|---|---|---|
| 11 Sep 2026, 23:20 IST (17:50 UTC) | 22 | 0 |
| 11 Sep 2026, 20:19 IST (14:49 UTC) | 22 | 0 |
| 11 Sep 2026, 20:16 IST (14:46 UTC) | 22 | 0 |
| 11 Sep 2026, 20:13 IST (14:43 UTC) | 22 | 0 |
| 11 Sep 2026, 12:38 IST (07:08 UTC) | 22 | 0 |
| 11 Sep 2026, 12:37 IST (07:07 UTC) | 22 | 0 |
| 11 Sep 2026, 12:36 IST (07:06 UTC) | 22 | 0 |
| 11 Sep 2026, 12:33 IST (07:03 UTC) | 22 | 0 |
| 11 Sep 2026, 12:31 IST (07:01 UTC) | 22 | 1 |
| 24 Aug 2026, 14:39 IST (09:09 UTC) | 19 | 0 |
| 24 Aug 2026, 14:36 IST (09:06 UTC) | 17 | 0 |
| 23 Aug 2026, 18:55 IST (13:25 UTC) | 17 | 0 |
| 23 Aug 2026, 18:54 IST (13:24 UTC) | 17 | 0 |
| 23 Aug 2026, 18:41 IST (13:11 UTC) | 16 | 0 |
| 23 Aug 2026, 16:52 IST (11:22 UTC) | 16 | 0 |
| 23 Aug 2026, 15:45 IST (10:15 UTC) | 16 | 0 |
| 23 Aug 2026, 15:26 IST (09:56 UTC) | 16 | 0 |
| 23 Aug 2026, 15:24 IST (09:54 UTC) | 15 | 0 |
| 22 Aug 2026, 12:50 IST (07:20 UTC) | 15 | 0 |
| 22 Aug 2026, 12:49 IST (07:19 UTC) | 15 | 1 |
| 22 Aug 2026, 12:02 IST (06:32 UTC) | 12 | 0 |
| 21 Aug 2026, 17:00 IST (11:30 UTC) | 12 | 0 |
| 21 Aug 2026, 16:50 IST (11:20 UTC) | 8 | 0 |
| 21 Aug 2026, 16:49 IST (11:19 UTC) | 8 | 1 |
| 21 Aug 2026, 16:40 IST (11:10 UTC) | 8 | 6 |

## What is being checked, and against what

Cricsheet publishes the source data. A pipeline parses it into a master file and
checks its own work before deploying it. A refresh job loads that master into the
two engines the API serves from, and a guard confirms the served copies still match
the master. The battery at the end runs against what the API actually returns. Each
layer is blind to what the others see, which is why all three are on one page.

Anything found is published as found.

## Elsewhere

- Anomalies, row by row: https://validation.tigzig.com/anomalies.md
- The API itself: https://db-mcp.tigzig.com/v1/docs
- Everything else on this site: https://www.tigzig.com/llms.txt
