---
title: Build weekly matchup default data views
type: task
status: completed
description: >-
  Adds a matchup family of default data views scoped to the current week's slate — team
  offense-versus-defense and game lines, plus tendencies, scheme, trenches, play type and
  per-position player drill-downs — built on a factory param-overlay mechanism and new in-season
  team rate columns.
base_uri: user:task/league/data-views/build-weekly-matchup-default-data-views.md
created_at: '2026-09-26T22:06:57.736Z'
entity_id: c9929744-0047-4905-92d7-28339bfafa8b
finished_at: '2026-09-27T00:38:56.820Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: High
public_read: true
relations:
  - follows [[user:task/league/data-views/support-matchup-opponent-type-on-team-identity-views.md]]
  - relates [[█████████████████████-███████████-███████-███████-████-███████]]
  - relates [[user:task/league/data-views/build-data-view-catalog-map.md]]
  - relates [[user:task/league/add-weekly-matchup-rating-per-player.md]]
  - relates [[user:task/league/build-out-default-data-view-catalog.md]]
started_at: '2026-09-26T22:17:37.476Z'
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-27T00:38:56.820Z'
visibility_analyzed_at: '2026-11-06T00:17:44.680Z'
---

# Implementation Plan: Build Weekly Matchup Default Data Views

## Overview

- Add a `matchup` family of default data views to the league catalog, each scoped to the current week's NFL slate. Every row reads own side against opponent side.
- Top level: a team offense-versus-defense view and a game-lines view.
- Drill-downs: tendencies, scheme, trenches, play type, and one player view per position (QB, RB, WR, TE) against what the opponent allows to that position.
- Impact: a reader opening data views during the season gets this week's matchups without hand-running SQL. The protection sweep, shell-mix exposure, personnel response and positional finder queries under `text/nfl/query/matchup/` become views anyone can open.
- Ordering: follows the completed opponent-type-on-team-views task, which made `matchup_opponent_type` work at team grain. Takes over the in-season fantasy-points-allowed column from the unified defense task. Lands a new family on the catalog map task. Unblocks the per-player weekly matchup rating draft.

## Background

- The catalog lives in `libs-shared/default-data-views/default-data-view-catalog.mjs`. Views are built by `build_default_data_view` in `build-default-data-view.mjs`. That factory copies ONE params object into every column, a test pins the rule byte-for-byte, and the only exemption is the hand-built `MIXED_PARAM_VIEW_IDS = ['SEASON_FANTASY_POINTS']`.
- Row grains are `player` and `team` only; there is no game grain. `row_axes` refine a grain to year or week identities.
- `matchup_opponent_type` (`current_week_opponent_total`, `next_week_opponent_total`) joins each subject to this week's opponent through an INNER JOIN on a clock-driven opponent CTE (`libs-server/data-views/week-opponent-cte-tables.mjs`, `libs-server/get-data-view-results.mjs` around lines 2072-2119). The rows are therefore limited to teams on this week's slate. The week comes from the clock, not from the view's `year` param.
- `team_unit` (`off` / `def`, `libs-shared/nfl-plays-team-column-params.mjs:4`) turns every team column built from plays into "offense does" or "defense allows". Team Defense Profile is the Team Unit line under `team_unit: def`. The column-lines file forbids a separate defense-allowed line.
- In-season gaps. These are all published after the season, so they have no current-year rows during the season: ESPN line win rates, DVOA line metrics, team seasonlogs (blitz, pressure, fantasy points allowed by position), and the grades supplier's team grades. The tendency, scheme and pressure rates exist only as play filters, not as team rate columns. Game-line columns (spread, total, moneyline, implied team total) declare player grain only.
- Gates: the population floor gate (`db/gates/check-data-view-population-floor.mjs`) needs production access and never runs in CI. It checks `ROW_FLOOR = 30`, `EVENTS_FLOOR = 5` and non-degeneracy on the top 20. A bye week leaves 26-30 slate teams, and the offseason leaves zero. The catalog spec requires a family, a question, and a family-to-auto-tag match, and it rejects duplicate grain/params/sort combinations. The map spec places every view once.
- Stale docs: `docs/data-views-architecture.md` lines 73 and 233 still call matchup rules player-only. No golden covers team columns built from plays with `matchup_opponent_type` at team grain.
- Hand-run analyses to port: `slate-protection-matchup-sweep.sql`, `slate-shell-mix-exposure.sql`, `defense-personnel-response.sql`, `matchup-alignment-finder.sql` and `team-seasonlog-matchup-finder.sql`, all under `text/nfl/query/matchup/`. `player-occurrences-vs-specific-defense.sql` has an unresolved grain and is not ported.

## Design

### Factory: column groups with param overlays

- `build_default_data_view` takes `column_groups: [{ column_line, param_overlay }]` in place of the flat `column_line`. Base params (`year`, `seas_type`, segment params) apply to every group, and each group's `param_overlay` is merged over them.
- Named overlays in the catalog module:
  - `OPPONENT_DEFENSE_PARAM_OVERLAY` = `{ team_unit: ['def'], matchup_opponent_type: ['current_week_opponent_total'] }`
  - `OPPONENT_OFFENSE_PARAM_OVERLAY` = `{ matchup_opponent_type: ['current_week_opponent_total'] }`, for defense-perspective groups
- An overlay may omit a base key, so one mechanism covers the Season Fantasy Points divergence (seasonlogs columns take no `seas_type`).
- The spec invariant becomes "identical base params, and every divergence is a declared overlay". Season Fantasy Points moves onto overlays, and `MIXED_PARAM_VIEW_IDS` and its exemption tests are deleted.

### Matchup season and slate semantics

- Matchup views open on `current_season.year` and never on the last completed season. The opponent is clock-driven, so any other year would pair the wrong season's stats with this week's opponent.
- Offseason: the opponent CTE is empty. The view renders an explicit empty state saying there is no NFL slate this week. The state is reachable, since zero rows is the observed offseason result.
- The population floor gate gets a slate-aware rule for the matchup family:
  - team grain: the row count equals the number of teams on this week's schedule
  - player grain: every slate team is represented
  - outside the NFL regular season: assert empty
- The events floor stays. Every view carries a visible play-count column so thin early-season samples read as thin.

### Family

- Add `matchup` to `DATA_VIEW_FAMILIES`, `DATA_VIEW_FAMILY_DETAILS` (question: "How does this week's opponent line up against him or them?") and `FAMILY_METRIC_DOMAINS`.
- `app/core/data-views/derive-auto-tags.js` derives a `matchup` tag whenever any column carries `matchup_opponent_type`. User-saved matchup views get tagged too.

### New columns

- Team grain, built from plays, in the existing numerator/denominator measure form in `libs-server/data-views-column-definitions/team-stats-from-plays-column-definitions.mjs`, with field definitions in `app/core/data-views-fields/team-stats-from-plays-table-fields.js` and index entries in `libs-shared/data-view-fields-index.mjs`. Each column works on both sides through `team_unit`, and each gets its `player_team_*` twin from the existing generator.
- Efficiency: EPA per play. First confirm whether the per-team-play output period (`period-denominator/per-team-play-wrap.mjs`) already renders `team_expected_points_added_from_plays` per play. Add `team_expected_points_added_per_play_from_plays` only if it does not.
- Tendencies:
  - `team_pass_rate_from_plays`
  - `team_play_action_rate_from_plays`
  - `team_motion_rate_from_plays`
  - `team_no_huddle_rate_from_plays`
  - `team_shotgun_rate_from_plays`
  - early-down pass rate is `team_pass_rate_from_plays` under the existing `early_down` segment params
- Scheme:
  - `team_man_coverage_rate_from_plays`
  - `team_zone_coverage_rate_from_plays`
  - `team_blitz_rate_from_plays`
  - `team_heavy_box_rate_from_plays`
  - `team_average_box_defenders_from_plays`
- Trenches:
  - `team_pressure_rate_from_plays`
  - `team_sack_rate_from_plays`
  - `team_stuffed_rate_from_plays`
  - `team_rush_yards_before_contact_per_attempt_from_plays`
- Play type: `team_screen_rate_from_plays` and `team_run_play_option_rate_from_plays`. Run gap, target depth and success on each play type use existing filters under a group overlay.
- Fantasy points allowed by position: `team_fantasy_points_from_plays` with a new param `credited_player_position` (QB/RB/WR/TE), meaning the position of the player credited with the play's fantasy points. Under `team_unit: def` it is fantasy points allowed to that position. `player_position` is not used as the name because it is already a prefix column and field name.
- Points: `team_points_per_game_from_games`, a team-grain source over finalized `nfl_games` that accepts `team_unit`. The `home_points` field reads 0 on unplayed weeks, so unplayed games are filtered out.
- Game lines: the existing `team_game_prop_line_from_betting_markets` and `team_game_implied_team_total_from_betting_markets` gain a team-grain source declaration.
- Every new column id and param key is permanent. Charting- and tracking-sourced inputs (`coverage_class`, charted blitz and box) are checked for current-season rows before shipping. A bounded input carries its window in the view name, per the bounded-source rule.

### Views

| View id | Grain | Own side, then opponent side |
|---|---|---|
| `WEEK_MATCHUP_OFFENSE_VS_DEFENSE` | team | EPA per play, success rate, explosive rate, pass rate over expected, plays per game, then the same allowed |
| `WEEK_MATCHUP_GAME_LINES` | team | spread, total, implied team total, moneyline, points per game, then opponent points allowed and pace |
| `WEEK_MATCHUP_TENDENCIES` | team | pass rate, early-down pass rate, play action, motion, no huddle, shotgun, then opponent success allowed against each |
| `WEEK_MATCHUP_SCHEME` | team | EPA against man, zone and blitz, then opponent man rate, zone rate, blitz rate, heavy box rate, average box |
| `WEEK_MATCHUP_TRENCHES` | team | pressure rate allowed, sack rate allowed, stuffed rate, yards before contact, run success, then opponent pressure, sack and stuff rates and run success allowed |
| `WEEK_MATCHUP_PLAY_TYPE` | team | rate and success on inside runs, outside runs, deep, short, screen and RPO plays, then opponent success allowed on each |
| `WEEK_MATCHUP_QUARTERBACKS` | player | dropbacks, pressure rate, EPA, then fantasy points allowed to QBs and opponent pressure rate |
| `WEEK_MATCHUP_RUNNING_BACKS` | player | rush share, target share, opportunity share, expected fantasy points share, then fantasy points allowed to RBs and run success allowed |
| `WEEK_MATCHUP_WIDE_RECEIVERS` | player | target share, air yards share, deep target rate, then fantasy points allowed to WRs, deep success allowed, man and zone rate |
| `WEEK_MATCHUP_TIGHT_ENDS` | player | target share, routes, snaps, then fantasy points allowed to TEs |

- Player views are split by position because a column's `credited_player_position` is fixed per column while a row's position varies.
- Each view has a distinct sort column, which satisfies the catalog's non-degeneracy check.

### Integration points

- Tests: `test/app.default-data-view-catalog.spec.mjs` and `test/app.data-view-catalog-map.spec.mjs`, plus goldens under `test/data-view-queries/`.
- Opponent attach path, unchanged apart from new golden coverage: `get-data-view-results.mjs`, `week-opponent-cte-tables.mjs` and `source-attach/rules/matchup-opponent.mjs`.
- The catalog file is shared with the per-view review task (`█████████████████████████████`). Every commit uses explicit pathspecs.

### Knowledge Base Impact

- League docs, which are published: fix `docs/data-views-architecture.md` lines 73 and 233, document column groups and overlays in `docs/data-views-system.md`, and add the matchup season rule to `docs/guides/data.md`.
- `text/league/data-views/data-view-service-objectives.md`: add the matchup family to the family table and the slate-aware floor to the population section.
- Relations on this task: `relates` the unified defense task, the catalog map task, and the per-player weekly matchup rating draft; `follows` the opponent-type-on-team-views task.
- The unified defense task gets a `[handoff]` observation that the in-season fantasy-points-allowed column moved here. No live session owns it.
- Tags: `league-xo-football` and `software-task`. No new tag and no ABOUT.md change. There is no league feature inventory.
- Working tier: none. Population-floor readings are recorded as task observations.
- Shape: one bounded multi-session task, not an orchestrator. It sits in the league-xo-football orchestrator's queue through its tag.

## Notes

- Out of scope:
  - a computed edge column (own minus allowed against league average), which needs derived cross-column columns the data-view system lacks
  - playcaller tendencies, which stay blocked on the coach-columns task
- Early-season samples are about three games per defense at week 3. The design shows them as they are, current season only with a visible play count, rather than blending in prior seasons.
- A traded player keys on `current_nfl_team`, so he gets his current team's opponent. That is the desired behavior here.
- Every golden and gate change follows the verification rule: make it fail on a known-broken case, run the control as a pair, and require the readings to differ.
- Deploys are human-gated: `yarn deploy:all` runs only on the operator's go.

## Plan

### Setup and Preparation

- [x] **Measure current-season coverage of each new column's inputs**
  - Purpose: decide which views ship unwindowed, and prove the in-season premise before building on it.
  - Query 2026 regular-season `nfl_plays` for non-null rates of `coverage_class`, the blitz and box slots, play action, motion, no huddle, shotgun formation, pressure, yards after contact, screen and RPO flags. Check the jobs ledger for importer health over the same window.
  - Record the result as one `[finding]` observation per bounded input.
- [x] **Confirm how EPA per play renders**
  - Check whether the per-team-play output period renders `team_expected_points_added_from_plays` as a per-play value at team grain, and decide whether the new column is needed.

### Core Implementation: infrastructure

- [x] **Column groups and param overlays in the factory**
  - Change `build_default_data_view` to take `column_groups`, including key omission in an overlay, and convert every existing catalog entry to a single group with no overlay.
  - Update the params invariant in `test/app.default-data-view-catalog.spec.mjs` and add a negative control: a view with an undeclared divergence must fail.
- [x] **Retire the mixed-param exemption**
  - Rebuild Season Fantasy Points on overlays, then delete `MIXED_PARAM_VIEW_IDS` and its paired tests.
  - Verify the rebuilt view's rendered table state is identical to the old one's.
- [x] **Matchup family and auto tag**
  - Add the family, its details and its metric domains. Derive the `matchup` tag from `matchup_opponent_type` in `derive-auto-tags.js`, with a spec covering a user view.
- [x] **Matchup season rule and offseason empty state**
  - Matchup-family views resolve `year` to `current_season.year`. Add the empty state for a slate with no teams, and demonstrate it against an offseason clock.
- [x] **Slate-aware floor in the population floor gate**
  - Compute the expected slate from `nfl_games` for the current week. Enforce equality at team grain, representation at player grain, and empty outside the regular season.
  - Add negative controls: a view missing one slate team fails, and a bye-week slate of 28 teams passes.

### Core Implementation: columns

- [x] **Tendency rate columns**
  - Pass, play-action, motion, no-huddle and shotgun rates. Each gets a golden, and a control showing the value changes when `team_unit` flips.
- [x] **Scheme rate columns**
  - Man coverage, zone coverage, blitz and heavy box rates, and average box defenders, with goldens and controls. Name the charting window for any bounded input.
- [x] **Trench columns**
  - Pressure, sack and stuffed rates, and rush yards before contact per attempt, with goldens and controls.
- [x] **Play-type columns**
  - Screen and RPO rates, with goldens and controls.
- [x] **EPA per play**, only if the setup check found it missing.
- [x] **Fantasy points by credited position**
  - Add `team_fantasy_points_from_plays` and the `credited_player_position` param.
  - Cross-check 2025 defense-side totals by position against `league_nfl_team_seasonlogs_points` with `AGAINST_TOTAL`, and state the tolerance and its basis.
- [x] **Points per game from games**
  - Add `team_points_per_game_from_games` accepting `team_unit`, and exclude unplayed games. The golden includes an unplayed week to prove it is excluded.
- [x] **Team-grain game lines**
  - Declare a team-grain source for the game prop line and implied team total columns, with a team-grain golden.
- [x] **Golden for team columns from plays under the opponent param at team grain**
  - Closes the coverage gap. Assert 32 reciprocal rows on a full-slate week.

### Core Implementation: views

- [x] **Top-level views**: `WEEK_MATCHUP_OFFENSE_VS_DEFENSE` and `WEEK_MATCHUP_GAME_LINES`
- [x] **Team drill-downs**: `WEEK_MATCHUP_TENDENCIES`, `WEEK_MATCHUP_SCHEME`, `WEEK_MATCHUP_TRENCHES` and `WEEK_MATCHUP_PLAY_TYPE`
- [x] **Player drill-downs**: `WEEK_MATCHUP_QUARTERBACKS`, `WEEK_MATCHUP_RUNNING_BACKS`, `WEEK_MATCHUP_WIDE_RECEIVERS` and `WEEK_MATCHUP_TIGHT_ENDS`
  - Each view gets a family, a question, a distinct sort column, a play-count column, and a place on the catalog map (coordinate with the catalog map task's owner).

### Knowledge Base Updates

- [x] Fix the player-only claims in `docs/data-views-architecture.md` at lines 73 and 233.
- [x] Document column groups and overlays in the factory and catalog header comments, which `docs/data-views-system.md` defers to; no matchup season rule exists to document (see the scope-change observation).
- [x] Add the matchup family and slate-aware floor to `text/league/data-views/data-view-service-objectives.md`.
- [x] Add a `[handoff]` observation on `██████████████████████████████████████████████████████████████` saying the fantasy-points-allowed column moved here.

### Validation and Review

- [x] `yarn test:isolated`. Confirm the summary line, and the catalog, map and new golden specs by name.
- [x] `yarn lint` and `yarn check:types`.
- [x] Run `db/gates/check-data-view-population-floor.mjs` against production for every matchup view. Record the readings as observations.
- [x] Visual check of each view with `yarn dev:live` on the current week.
- [x] Hand to the operator for `yarn deploy:all`.
