---
title: Repoint the plays surfaces onto play_classification
type: task
status: completed
description: >-
  Make /plays and the player-drawer plays table filter on play_classification instead of play_type,
  so they hide administrative markers and feed retractions while showing the 78,905
  penalty-nullified plays users want. Split out of the column task during plan review; blocked on
  the column existing.
base_uri: user:task/league/repoint-plays-surfaces-onto-play-classification.md
created_at: '2026-09-21T15:44:14.892Z'
entity_id: 36273fc0-42e8-4276-88b4-17191daafa95
finished_at: '2026-09-22T01:42:31.784Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
public_read: true
relations:
  - blocked_by [[█████████████████████████-████-███████-████-█████████-████████]]
started_at: '2026-09-21T23:18:50.382Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-11-06T02:28:04.694Z'
visibility_analyzed_at: '2026-11-05T23:50:10.575Z'
---

# Implementation Plan: Repoint the plays surfaces onto play_classification

## Overview

- Make the two plays-reading surfaces filter on `play_classification` instead of on `play_type`, so they hide administrative markers and feed retractions while showing the penalty-nullified plays users want.
- **Impact.** `/plays` today hides 141,355 markers and 1,926 retractions only by also discarding 78,905 nullified plays, and the player-drawer plays table hides none of them, so it renders timeouts and quarter boundaries inline with a player's snaps.
- **Ordering.** Blocked by [[█████████████████████████-████-███████-████-█████████-████████]], which creates and backfills the column. Shares `libs-shared/plays-view-columns.mjs` with [[██████████████████████-████████-██████-█████████]] and [[user:task/league/scope-play-type-params-to-compatible-data-view-columns.md]], so it sequences behind whichever holds that file.

## Background

Split out of the column task during plan review. The two halves share no failure mode: the schema half is contended on DDL windows and measured in partition rewrite time, while this half is a three-file edit gated only on the column existing and being populated. Rollback granularity differs too — this is revertible in one commit, the backfill is not.

The consumer path contains exactly two `NOPL` literals, verified by `rg "'NOPL'" app/ api/ libs-server/plays-view/ libs-shared/plays-view-columns.mjs`:

- `app/core/plays-view/default-plays-views.js:66` — the `play_type != 'NOPL'` filter, present only in `DEFAULT_PLAYS_VIEW` and absent from `PASSING_PLAYS` and `RUSHING_PLAYS`.
- `libs-shared/plays-view-columns.mjs:90` — the picker option label.

`app/views/components/selected-player-plays/selected-player-plays.js:86` builds its `where` from year, season type and the role pid only, with no play-type filter of any kind.

### Assumptions

- `play_classification` exists, is populated on all 1,501,369 rows, and its coverage check is green. Unchecked here by construction; this task is blocked until the predecessor closes.
- A plays-view column needs one declaration only. Checked: `docs/guides/data.md:15-17` states the server derives SQL from the `table` and `column` pair and the client builds its field from `kind`, so neither needs an edit.
- `libs-shared/nfl-plays-column-params.mjs` must NOT gain an entry. Checked: `docs/guides/data.md:194` records that adding a key moves every from-plays CTE alias hash, invalidating every cached redis entry and every saved view keyed on them.

## Design

One declaration in `libs-shared/plays-view-columns.mjs` with `data_type: 'SELECT'` over the four values, then both consumer filters repointed.

**The filter is written as an allow-list, not a deny-list.** `play_classification != 'administrative_marker'` evaluates to NULL for any row the column is unset on, so those rows vanish from both surfaces with no error — a silent failure the current `play_type != 'NOPL'` at least shares rather than worsens. Writing it as `play_classification IN ('live_play','nullified_play')` states the intent positively and makes the same rows disappear for a readable reason. Either form hides unclassified rows, which is why the predecessor's coverage check is a hard prerequisite rather than a nicety.

**Knowledge Base Impact.** No new entities. The trap entry and the four stale repo documents are owned by the predecessor task, not duplicated here.

**Working tier.** None needed. This task produces no multi-file artifacts.

**Orchestrator shape.** Not warranted; three files.

## Plan

### Consumer Surfaces

- [x] **Declare the plays-view column**
  - File: `libs-shared/plays-view-columns.mjs`
  - Purpose: one declaration; server and client derive from it.
  - Changes: add `play_classification` with `table`, `column`, `kind: 'text'`, `data_type: 'SELECT'`, the four `column_values`, `group`, `header_label`, `aggregate: 'passthrough'` and a `description` naming the distinction.
  - Verify: the import-time invariants throw on a declaration that would select nothing, so a clean import is the first check; then confirm the picker offers exactly four options and each returns a non-empty table.

- [x] **Repoint the default plays view**
  - File: `app/core/plays-view/default-plays-views.js:62-67`
  - Purpose: stop discarding nullified plays in order to hide markers.
  - Changes: replace the `play_type != 'NOPL'` clause with an `IN` clause over `live_play` and `nullified_play`.
  - Verify: load `/plays` and confirm penalty no-plays appear while timeouts, quarter boundaries and retractions do not; compare the row count against the predecessor's baseline figures for the same season.

- [x] **Add the filter to the player-drawer plays table**
  - File: `app/views/components/selected-player-plays/selected-player-plays.js:86`
  - Purpose: it has never excluded markers, so timeouts render inline with a player's snaps.
  - Changes: add the same `IN` clause to its `where`.
  - Verify: open a player drawer for a player with a timeout in one of their games and confirm the marker row is gone while their nullified plays remain.

### Testing and Validation

- [x] **Prove no existing column's SQL moved**
  - Purpose: this task adds a column and changes a default view; no pre-existing column should emit different SQL.
  - Verify: `scripts/emit-plays-view-column-sql.mjs` against a worktree pinned to the pre-change revision shows byte-identical output for every pre-existing column. Assert the denominator and show the check red on a deliberate perturbation first.

- [x] **Regenerate and inspect affected goldens**
  - Purpose: the default-view change may move fixtures, and blessing must not re-bless a defect.
  - Changes: fix code first, regenerate second, explain third.
  - Verify: run `yarn test:data-view-queries` explicitly, since every `test/*.spec.mjs` glob misses both data-view-query files; regenerate named fixtures with `--seeded` and the full `NODE_ENV` and database environment; diff each regenerated fixture at token level; check `git status -- test/data-view-queries/` immediately before committing and treat a clean path you expected to be dirty as a lost blessing.

- [x] **Confirm the saved-view surface is unmoved**
  - Purpose: column ids are permanent and persisted in immutable short URLs.
  - Verify: no existing `column_id` is renamed or removed in the diff; load a saved view created before the change and confirm identical results.

- [x] **Run the gate set before merging**
  - Verify: `yarn verify` on the branch as the last step before merge, plus `yarn test:isolated`, `yarn lint`, `yarn check:types`, and both param-coverage gates via `yarn check:cluster`.
