---
title: Repair the two defects sharing the defense-equals-possession signature in nfl_plays
type: task
status: completed
description: >-
  1,807 rows across 46 games carry defense_nfl_team equal to possession_nfl_team, splitting into
  1,781 home-pinned-defteam rows (2001 and one 2016 game) and 26 genuine offense/defense swaps
  (2023-2025). The two need different repairs. The JAX 2001-2015 possession defect is NOT in this
  class.
base_uri: user:task/league/repair-plays-offense-defense-swap.md
created_at: '2026-09-10T21:53:29.164Z'
entity_id: 7c9d07c6-a48e-4e5e-ac7d-58292368366b
finished_at: '2026-09-11T00:47:16.179Z'
observations:
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: High
public_read: true
relations:
  - precedes [[user:task/league/enforce-plays-team-attribution-invariants.md]]
  - relates [[user:task/league/repair-jax-home-game-possession-attribution.md]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-11T00:47:16.179Z'
visibility_analyzed_at: '2026-11-05T23:50:08.396Z'
---

## Problem

Three separate defects were filed under one name when this task was written, and only one of them is an offense/defense swap. The measurements below are from 2026-09-10 against production and supersede the original framing.

**The signature is `defense_nfl_team = possession_nfl_team` with both non-NULL.** That class is **1,807 rows across 46 games**, not the 20,000+ the first draft predicted. It splits cleanly in two:

- **1,781 rows where `offense_nfl_team = defense_nfl_team`** — both sides name the same team, so the opponent is lost rather than swapped. 1,713 rows across 22 games in 2001, plus 68 rows in one 2016 game (esbid 2016090165). Thread 7ca4a3e8 localized the mechanism: the vendor `defteam` is pinned to the HOME team on every non-kickoff play regardless of possession, which makes defense equal possession exactly when the away team has the ball. Kickoffs escape because `import-plays-nflfastr.mjs` reads `defteam` for OFFENSE there. This subsumes the older "798 plays in 2001 have offense equal to defense" observation.
- **26 rows where `offense_nfl_team <> possession_nfl_team`** — the genuine swap. 20 in 2023, 1 in 2024, 5 in 2025, across 23 games. Not confined to one play type: 16 `NOPL`, 7 `FGXP` extra points, 3 `CONV` two-point attempts. Possession is the correct side on all 26, verified against `play_description` (esbid 2025011801 reads offense DET on a Washington extra point naming T.Ott and T.Way).

**The JAX 2001-2015 home-game population is NOT in this class and does not belong to this task.** Measured directly: zero rows in those games match the signature, and `offense_nfl_team` equals `possession_nfl_team` on 100% of them. The defect there is that possession ITSELF names the wrong team and offense faithfully copies it — esbid 2015102500 files 41 Bortles-to-Hurns/Robinson plays as BUF possession. That also refutes the 2026-07-25 root-cause note on `user:repository/active/league/ABOUT.md`, which ruled out an abbreviation drift on the grounds that possession read JAX consistently; it does not. There is no correct value anywhere in the row to repair from, so it needs its own task and its own source. Do not repair it here and do not fold it into the signature.

## Why it matters

`offense_nfl_team` is the canonical offensive-team column — the plays view's OFF column, the coach and play-caller joins, and every plays-sourced team stat read it or its `possession_nfl_team` input. A row in either sub-class attributes a team's snaps to the wrong side, silently, and any opponent-keyed query over the affected games is wrong without erroring.

## What is NOT in scope

- The JAX 2001-2015 possession defect, per above.
- The 13,056 `NOPL` administrative markers carrying `offense_nfl_team` with a NULL possession. That is a coverage question, not a swap, and is recorded as its own observation.

## Plan

1. **Confirm the class before repairing.** Re-run the signature count per season and per game and require it to reproduce 1,807 / 46. Any figure in this task is inherited and should be refuted or confirmed by your own query before you design on it — the first draft of this task predicted 20,026+ and was wrong by an order of magnitude.

2. **The two sub-classes need DIFFERENT repairs, and this is the whole trap.** For the 26 swap rows, possession is correct and the repair is to set offense from possession and defense to the game's other team. For the 1,781 home-pinned rows that is WRONG — offense already equals possession there and is correct; it is `defense_nfl_team` alone that is bad, and the repair is to set it to the game's other team relative to offense. A single UPDATE keyed on the shared signature would corrupt one sub-class while fixing the other.

3. **The writer for the 1,781 is localized; confirm it and fix it.** Read the `defteam` handling in `import-plays-nflfastr.mjs` and check whether the pinning can still fire on a re-import. A repair without a writer fix gets re-minted. For the 26, the writer is still unknown: `play_changelog` carries zero rows for any of the three team columns across all 4.6M entries, so the changelog cannot attribute them.

4. **Author the repair as an adhoc file** under `db/adhoc/`, one statement per sub-class, keyed on the discriminator rather than on a game list, with post-conditions that raise. Verify a sample of each sub-class against `play_description` before applying.

5. **Gate it.** Add the signature as a check in `db/checks/` so a re-minted case fails an audit rather than waiting for someone to notice a wrong opponent. Prove the check red at the pre-repair revision before trusting its green.

## Verification

- The signature count goes to zero, and a control that injects one row of each sub-class makes the check fail.
- For the 1,781: the repaired games no longer show a defense pinned to the home team, measured as the home/away balance of `defense_nfl_team` against possession.
- Spot-check restored rows against `play_description` player identity, which is what confirmed both sub-classes in the first place.
