---
title: Repair the run_gap Sportradar overwrite
type: task
status: completed
description: >-
  Sportradar filled nflfastR-owned run_gap through a mis-numbered running lane, collapsing agreement
  with the play description to 17 percent on 2025 and 10 percent on 2026. Not a protection breach:
  the protected set permits filling an empty column. Writer deleted, 13,631 rows repaired to 100
  percent, and a second-source check registered.
base_uri: user:task/league/repair-run-gap-sportradar-overwrite.md
created_at: '2026-09-23T20:49:04.019Z'
entity_id: 0bc3c8c6-d78f-411b-ad8c-4eb8c7c3cc26
finished_at: '2026-09-23T22:48:11.061Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: High
public_read: true
relations:
  - relates [[██████████████████████-█████████-██████-██████████-███████]]
  - follows [[user:guideline/software/design-data-checks.md]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-23T22:52:36.255Z'
visibility_analyzed_at: '2026-11-05T23:50:09.110Z'
---

## Context

`nfl_plays.run_gap` is declared nflfastR-owned and is being overwritten by
Sportradar on the live season. Measured 2026-09-23 against `league_production`,
comparing the stored enum against a naive parse of the row's own
`play_description` (`left end`, `left tackle`, `left guard`, `up the middle`,
`right guard`, `right tackle`, `right end`):

| Season | Rows | Exact agreement | Coarse left/middle/right |
| --- | --- | --- | --- |
| 2019 REG | 12,887 | 12,887 (100%) | 12,887 |
| 2022 REG | 14,236 | 14,226 (99.93%) | 14,226 |
| 2024 REG | 13,334 | 13,275 (99.6%) | 13,290 |
| 2025 REG | 14,045 | **2,401 (17.1%)** | 5,650 (40.2%) |
| 2026 REG | 1,536 | **154 (10.0%)** | 480 (31.3%) |

Coarse agreement below three-way chance is the discriminator: these are wrong
values, not finer-grained ones. The confusion is near-random rather than a
systematic shift — stored `LEFT_TACKLE` parses as `MIDDLE` 1,517 times, stored
`RIGHT_TACKLE` as `LEFT_END` 1,246 times.

The charting columns corroborate independently. On 2025 regular season the
largest single cell pairs `run_gap_outcome_side = LEFT` with
`run_gap = RIGHT_TACKLE` (816 rows), and `run_gap_intent`'s gap letters spread
evenly across all nine `run_gap` values.

## Cause

`libs-server/sportradar/sportradar-protected-fields.mjs:167` lists
`'run_gap', // nflfastr` — the column is declared nflfastR-owned. But
`scripts/import-plays-sportradar.mjs:416` writes
`mapped.run_gap = transform_run_gap(play.running_lane)` regardless, mapping the
vendor's 0-8 lane through `libs-server/sportradar/sportradar-transforms.mjs`.
The write leaks past its own protection list.

The corruption is ongoing: 2026 is the live season and its agreement is the
worst of any season measured.

## Why this is its own task

Found while enumerating the preseason column population gaps
([[██████████████████████-█████████-██████-██████████-███████]]), which needs
`run_gap` derivable from the play description and cannot validate that
derivation against a corrupted era. Every other candidate owner is Completed, so
this had no home.

## Plan

- [x] **Stop the leak**
  - File: `scripts/import-plays-sportradar.mjs:416`
  - Purpose: the write contradicts the protection list that names the same
    column. Establish whether the protection list is not consulted on this path
    at all, or is consulted and this field bypasses it — the fix differs.
  - Verify: re-run the importer on one 2026 game and confirm `run_gap` is
    unchanged on rows where it already held a value.

- [x] **Decide whether the lane transform is salvageable or wrong**
  - Purpose: a 0-8 lane mapped to a 9-value enum should not produce
    near-random disagreement, which suggests the mapping itself is wrong
    (likely an offset or a handedness convention) rather than merely
    unauthorised. Worth knowing before the column is written by anyone.
  - Verify: compare `transform_run_gap` output against the play description on
    2024 rows, where Sportradar and nflfastR both wrote and agreement was still
    99.6 percent, to isolate whether the transform or the write order changed.

- [x] **Repair 2025 and 2026 from the play description**
  - Purpose: restore the column to its pre-2025 accuracy. The description is the
    validated source — 100 percent on 2019, 99.93 percent on 2022.
  - Verify: post-repair agreement against `play_description` at or above 99.9
    percent on both seasons, and `LEFT_MIDDLE` / `RIGHT_MIDDLE` left untouched
    since no description phrase produces them.

- [x] **Register a check that would have caught this**
  - Purpose: the column sat corrupted through a whole season with every existing
    oracle green, because none of them compares a column against a second
    in-row source.
  - Verify: the check exists, gates, and passes — an invariant, not a one-shot
    measurement.

## Outcome

Done 2026-09-23. Commits: `75d6621ec` (leak), `5a699dfb7` with private `ffcbc20`
(repair and check).

| Season | Before | After |
| --- | --- | --- |
| 2025 REG | 17.1% | 100.000% |
| 2025 POST | 12.6% | 100.000% |
| 2026 REG | 10.0% | 100.000% |
| 2024 REG | 99.6% | 99.932% |

**The Cause section above is wrong and is left standing as written.** It says
the write "leaks past its own protection list". It does not. The importer DOES
pass `SPORTRADAR_PROTECTED_FIELDS`, and that set forbids OVERWRITING a value
while expressly permitting the FILL of an empty one — so the live import reached
every row before nflfastR did and the protection never had occasion to engage.
Same shape as `2026-09-23-set-kick-event-flags-the-mapper-manufactured-false.sql`
in the same mapper in the same week. Reading this as a protection breach points
the fix at the wrong file.

**Two plan verifies could not be run as written, and both premises were wrong.**

- Step one asked to re-run the importer and confirm `run_gap` unchanged where it
  held a value. That test would have passed against the unfixed code: the defect
  was never an overwrite. The write is deleted outright instead, and the
  evidence is that `75d6621ec` is an ancestor of the deployed `359fd7b4a`.
- Step three asked that `LEFT_MIDDLE` / `RIGHT_MIDDLE` be left untouched "since
  no description phrase produces them". They were not left untouched — they are
  the corruption's fingerprint. nflfastR's `format_run_gap` cannot emit either,
  so all 3,739 in the window were Sportradar fills, and all 3,739 are resolved.
  Their 12 pre-2025 occurrences are what dated the leak to 2024 week 2.

**Scope larger than planned in two directions.** 2025 POST (635 rows, 87%
corrupt) and 2024 REG week 18 were both in and neither was named in the plan;
2024 week 18 is the onset, holding 55 rows against roughly 800 run plays because
nflfastR never filled it.

**The transform was also wrong**, independently of authority: `running_lane` is
standard football hole numbering (0/1/2 centre, then odd left and even right,
increasing outward), not a left-to-right 0-8 sweep. Correcting it recovers only
75.9% on 2025, because the vendor charts where a run ARRIVED and nflfastR
records where it was AIMED — so the mapping was deleted, not fixed.

## Left open, deliberately

- **61 rows outside the window hold a `run_gap` on a non-run play** — field
  goals, completed passes — spread across 2002, 2021, 2022, 2023 and 2024. They
  predate this writer and nflfastR owns them. The registered check reports them
  as `no_gap_in_description` rather than pooling them into the agreement rate,
  so they are visible rather than parked.
- **2024 REG week 18 is still short a full population**: 55 rows carry a
  `run_gap` against roughly 800 run plays. This repair corrected the values that
  were there; it did not fill the gap. That belongs to
  [[██████████████████████-█████████-██████-██████████-███████]].
