---
title: Repair the missing per-play scores on the 2024 Hall of Fame game
type: task
status: completed
description: >-
  All 129 plays of esbid 2024080151 carry NULL home_score, away_score and possession_score while the
  schedule holds a FINAL 21-17, so the published 2024 preseason CSV cannot reconstruct a running
  score. One game database-wide, and repairable — the surviving scoring plays reconstruct 21-17
  exactly, which gives the repair an independent oracle.
base_uri: user:task/league/repair-missing-play-scores-2024-hall-of-fame-game.md
created_at: '2026-09-22T06:04:00.335Z'
entity_id: 487fd8f3-d17f-41a7-b28d-69bced4cacc0
finished_at: '2026-11-04T16:42:15.883Z'
observations:
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Low
public_read: true
relations:
  - relates [[user:task/league/republish-dataset-after-play-enrichment-backfill.md]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-11-04T16:42:15.883Z'
visibility_analyzed_at: '2026-11-05T23:50:07.671Z'
---

## What is wrong

Every one of the 129 published plays in the 2024 Hall of Fame game, esbid `2024080151` (CHI 21, HOU 17, week 0), carries `NULL` in `home_score`, `away_score` and `possession_score`, while the `nfl_games` row holds a `FINAL` 21-17. So the published `nfl/plays/2024/PRE.csv` cannot reconstruct a running score for that game at all.

`check-published-payload-oracles.mjs` reports it as `score_reconstruction` / `games_unpopulated_with_known_final`. The CSV is a faithful copy — the gap is in `nfl_plays_year_2024`, not in the export.

## It is one game, database-wide

```sql
select g.season_year, g.season_type, count(*)
from nfl_games g
where g.home_score is not null
  and exists (select 1 from nfl_plays p where p.esbid = g.esbid and p.is_deleted is not true)
  and not exists (select 1 from nfl_plays p where p.esbid = g.esbid
                  and p.is_deleted is not true and p.home_score is not null)
group by 1, 2
```

Returns exactly one row: 2024/PRE, 1 game. This is an isolated defect, not the visible end of a class, and that containment query is the control to re-run before assuming otherwise.

## It is repairable from data already held

The scoring stream survived even though the running score did not. The game has 11 `is_scoring_play` rows and 6 `score_type` values, and 518 rows in `nfl_play_stats` that all join cleanly to the plays.

Reconstructing from the scoring plays lands on the schedule final exactly:

| team | scoring plays | points |
| --- | --- | --- |
| HOU (away) | TD at 375, FG at 968, TD at 1695, with XP plays at 396 and 1728 | 14 + 3 = **17** |
| CHI (home) | TD at 1203, TD at 1870, TD at 2250, with XP plays at 1229, 1896, 2276 | 18 + 3 = **21** |

That agreement is what makes this gradeable: the repair has an independent oracle in the `nfl_games` final, so a reconstruction that does not land on 21-17 is wrong by construction.

## Plan

- [ ] Identify the WRITER before designing the repair. `home_score` on plays is not written by any module under `libs-server/play-enrichment/` — grep it there and the result is empty, validated against a control. The candidates are `scripts/import-plays-nflfastr.mjs` and `scripts/import-plays-sportradar.mjs`. Find out which one covers preseason week 0 and why it skipped this game; the answer decides whether this is a re-import, a recompute, or a source gap.
- [ ] Ask why only THIS game. Every other preseason game in the database has per-play scores, so whatever normally supplies them ran everywhere else. A single missing game usually means a single failed run, and the `jobs` ledger for that window is the discriminator.
- [ ] Repair, grading on the 21-17 reconstruction rather than on non-null-ness. A run that populates the columns with wrong values passes a population check and fails this one.
- [ ] Confirm `check-published-payload-oracles.mjs --only nfl/plays/2024` reaches PASS on `score_reconstruction`.

The published copy only changes on a republish, and every republish is an operator gate. [[user:task/league/republish-dataset-after-play-enrichment-backfill.md]] is Completed — it shipped `e2c58cc` on 2026-09-22 carrying this defect by operator decision — so clearing the public copy now needs a FUTURE republish, which this task should not wait for. Repair the database, confirm the oracle, and let the next regeneration carry it.

## History

This partition has now been accepted into a regeneration ledger TWICE, and the second time was a decision rather than a repeat:

- **2026-09-22, first publish.** Accepted under a ledger signed `claude-session-close-player-position-split` — an agent, not the operator.
- **2026-09-22, second publish (`e2c58cc`).** Put to the operator explicitly as "accept again, or hold the republish for the repair", and the operator ruled accept; the republish does not wait on this. Recorded as the `external_check` on the `nfl/plays/2024/PRE.csv` ledger record, with the reviewer field naming the session that read the evidence.

One thing changed in the machinery between those two acceptances and it matters to whoever picks this up. The finding was previously INVISIBLE in the ledger a reviewer reads: `build-regeneration-ledger.mjs` summarized `score_reconstruction` without `games_unpopulated_with_known_final`, the array that actually drives `ok: false` here, so the record rendered as a failure with `mismatch_count: 0`, `games_unscored_in_db: 0` and `games_unpopulated_in_csv: 0` and no finding attached. Fixed in league `4539ee354`; the record now shows esbid `2024080151` and the 21-17 final inline. Do not expect the old shape.

Also note this partition does NOT self-surface through `oracle_changed_since_review` — at the second publish it sat in the 74 flags whose only difference was a mechanical summary-key change, so it has to be examined deliberately.

## █████ █████ that apply here

- [[███████████████████████████████-█████████-████-████████]] — the scoring `stat_id` must be unioned with the base one or a sum drops every touchdown, which is exactly the arithmetic this repair rests on.
- [[███████████████████████████████-█████████-████████]] — several boolean play columns are written only on TRUE rows, so `is_scoring_play` counts presence rather than a true/false split.
