---
title: Build out the default data view catalog for breadth and depth
type: task
status: completed
description: >-
  Build the default data-view catalog from 10 views to roughly 36 across five measure families, on a
  foundation of three named vocabularies where adding or removing a view is a one-line change. The
  map surface is split to its own task; two views needing new server columns are routed elsewhere.
base_uri: user:task/league/build-out-default-data-view-catalog.md
created_at: '2026-09-25T02:36:08.872Z'
entity_id: aa9f4eaf-405a-451a-97f3-d1dc9d333d01
finished_at: '2026-09-26T15:13:12.494Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: High
public_read: true
relations:
  - blocked_by [[█████████████████████-███-█████-█████-███████████-██████-███-████████-███████]]
  - relates [[user:task/league/add-available-opportunity-to-utilization-components.md]]
  - relates [[██████████████████████-███████████-████████]]
  - relates [[█████████████████████-███████████-███████-███████-████-███████]]
  - relates [[user:task/league/dynamic-glossary-from-data-view-fields.md]]
  - relates [[user:task/league/data-views/land-data-views-first-paint-selection.md]]
  - relates [[user:task/league/data-views/build-data-view-catalog-map.md]]
  - relates [[user:task/league/add-coach-play-caller-data-view-columns.md]]
started_at: '2026-09-26T02:02:26.808Z'
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-10-09T23:08:03.621Z'
visibility_analyzed_at: '2026-11-05T23:48:28.322Z'
---

## Overview

Build the default data-view catalog out from 10 views to roughly 36, on a foundation where adding or removing a view is a one-line change, and give it a navigable map users can open without leaving the view they are in.

Reviewed 2026-09-26 against the running code and the production database. Both UNCHECKED assumptions are settled, three verify lines a correct implementation would have FAILED are rewritten, and four things are cut: two team views that have no columns to build from, one of the seven column lines, and the `admits_lines` field. Every cut and correction is stated at the point it applies, with the measurement behind it.

The catalog today touches 12.5 percent of the column surface (77 of 615 server column definitions) and 4 real segment parameters of 254. Nothing needs new query machinery to fix that: every parameter already reaches SQL on every from-plays column. The work is naming three vocabularies, enumerating the crossings worth shipping, and making the result discoverable.

**Impact.** A reader arrives naming a question and finds it already answered, instead of arriving with a question and being handed a query builder. Concretely: the five situational views gain an unsegmented base and a rate counterpart they lack today; the team grain becomes reachable for the first time; and the 43 existing rate columns surface in a dedicated family.

**Ordering.** Blocked by [[█████████████████████-███-█████-█████-███████████-██████-███-████████-███████]] for the per-route family only — every other family ships independently, so that dependency gates one view rather than the plan. Sequences with [[█████████████████████-███████████-███████-███████-████-███████]], whose team-grain sample-size findings this inherits, and with [[user:task/league/dynamic-glossary-from-data-view-fields.md]], which owns column-level explanation this deliberately does not absorb.

## Background

Four independent view registries ship today, not one:

| registry | file | count |
| --- | --- | --- |
| system data views | `app/core/data-views/default-data-views.js` | 10 |
| players-page views | `app/core/players/default-players-views.js` | 6 |
| plays-view defaults | `app/core/plays-view/default-plays-views.js` | 3 |
| player profile tabs | `app/core/players/player-profile-tabs.mjs` | 11 |

Four names exist in two of them with divergent column lines: `passing_stats_by_play`, `rushing_stats_by_play`, `receiving_stats_by_play` and `season_projections`, at 22/17/16/14 columns against 24/16/17/14 fields. The page-level-controls-versus-column-carried-filter distinction is real and both surfaces stay; the divergence is unmaintained drift.

The navigation taxonomy already exists. `libs-shared/view-organization/auto-tag-vocabulary.mjs` is a closed 19-value set on four axes — Subject, Position, Metric domain, Time horizon — and `derive_auto_tags` already tags system views, since only the LLM tagger excludes them. Three of the five measure families already have their axis-3 value reserved (`opportunity`, `efficiency`, `play-by-play`) and axis 4's `historical` is the era bucket. The grouping, favorites and tags controller shipped.

### Assumptions

- **Every declared parameter reaches SQL on every from-plays column.** Checked: all three from-plays factories set `consumes_params_extra` to `Object.keys(nfl_plays_column_params)`, and a census of all 254 parameters found 0 whose `data_type` falls outside the four branches `apply-play-by-play-column-params-to-query.mjs` handles.
- **Game-context parameters need no new wiring.** Checked: the 12 `nfl_games_params` auto-join `nfl_games` at `apply-play-by-play-column-params-to-query.mjs:96`.
- **Adding a view costs no server work — true for every family except Team and coach.** Checked 2026-09-26 by resolving the proposed lines against the 615 definitions in `libs-server/data-views-column-definitions/index.mjs`. Two named views have NO columns to build from and are not one-line catalog entries:
  - **Playcaller Tendencies has no columns YET, and [[user:task/league/add-coach-play-caller-data-view-columns.md]] already owns building them.** Zero of the 615 definitions match coach, playcaller or play-call today. That task is Planned at High priority and adds four sided coach params plus three team-grain display columns — `team_head_coach`, `team_offense_play_caller`, `team_defense_play_caller`. Once it lands, this view is a team-grain catalog entry with a coach column and needs no new grain. Cut from this plan and sequenced after that one.
  - **Fantasy Points Allowed has no team-grain fantasy-points column.** All 12 `*fantasy_points*` definitions are player grain; no `team_*fantasy_points*` exists. Cut below.
  - The absent team-grain completion percentage is confirmed: `team_pass_completions_from_plays` and `team_pass_attempts_from_plays` both exist, `team_completion_percentage_over_expected_from_plays` exists, and the plain rate does not — so shipping the two counts separately is the right move and needs no server work.
  - The team registry holds **41** `team_`-prefixed definitions, not the 46 this plan first claimed.
- **`is_goal_to_go` is now trustworthy.** Checked 2026-09-25: flagged equals derived exactly in every season 2023-2026 after the repair, so the true goal-to-go axis is usable rather than the yard-line proxy.
- **`yard_line_100` is sound from 2016 onward.** Checked: the inversion against `yard_line_side` is roughly 700 scrimmage plays a season across 2001-2015 only, owned by a live session. Current-season views are unaffected; a multi-year range reaching pre-2016 is not.
- **THREE axes are era-bounded, not one, and the earlier reading of this was wrong.** Measured 2026-09-26 over `nfl_plays` REG rows, 2018 through 2026:

  | axis | seasons with rows | window |
  | --- | --- | --- |
  | `targeted_receiver_separation` | 2023, 2024, 2025 | 2023-2025 |
  | `is_endzone_target` | 2024, 2025 | 2024-2025 |
  | `coverage_type_charted_primary` | 2025, 2026 | 2025 onward |

  The prior assumption measured the CURRENT season, found coverage live at 78-97 percent, and generalized that to the axis. Coverage is in fact at hard zero for every season 2018 through 2024. So a man-coverage or zone-coverage view is exactly as era-bounded as the separation view, and an endzone-target view is bounded at BOTH ends today. Personnel, box defenders, motion and hash are the ones genuinely live across the range.

  This is the assumption the coverage-window mechanism exists to catch, and the first draft of this plan got it wrong — which is the argument for the mechanism rather than against it.
- **A segmented view cannot open on the current season, and the constraint is the SEGMENT rather than the grain.** The unified-defense task established the team-grain half: roughly 150 plays a team before any filter, and a 2026 pressure rate spanning 0.171 to 0.963 across teams, which is denominator artifact.

  Measured 2026-09-26 at PLAYER grain over 2026 REG, counting each play once per `target_pid` and once per `ball_carrier_pid`:

  | segment | touches | players | touches per player |
  | --- | --- | --- | --- |
  | all scrimmage | 3,762 | 376 | 10.0 |
  | play action | 537 | 249 | 2.2 |
  | goal to go | 240 | 127 | 1.9 |
  | no huddle | 237 | 139 | 1.7 |

  A player-grain goal-to-go view on the current season ranks 127 players on a mean of 1.9 touches each. That is a worse leaderboard than the team-grain case this plan already refuses to open, so anchoring by grain is the wrong rule. **Anchor by segment selectivity: any view whose segment leaves fewer than a stated number of events per ranked row opens on the last completed season, whatever its grain.** The unsegmented views stay on the current season, which is the point of adding them.

  **Resolved 2026-09-25 to a UNIFORM rule, because the per-segment form does not hold still.** The gate measured the whole catalog and found the boundary moves inside a season: deep target and man coverage clear the floor at week 3 of 2026 and would not have at week 1, and eight of thirteen segmented views sat below it. A per-segment flag derived from any one week states a per-season truth as a constant and is wrong for one season or the other from the day it is written, with nothing to report the drift — which is the exact reasoning that already cut `admits_lines`. So `resolve_opening_season` anchors EVERY segmented view on the last completed season, and unsegmented views stay current.

  **This is a user-visible change to the five frozen views and is the one decision here an operator should be aware of rather than inherit.** `REDZONE_OPPORTUNITY`, `GOAL_LINE_OPPORTUNITY`, `TWO_MINUTE_OFFENSE`, `SHORT_YARDAGE` and `LATE_DOWN_USAGE` ship today on the current season and all five read below the floor there — 2.12 to 5.61 events per ranked row at week 3. The cost of the rule is that red-zone usage shows the prior season all through the live one, which is when a reader most wants it. The alternative worth weighing is a season-progress threshold inside `resolve_opening_season`: one constant, still no per-segment judgment, and the segmented views come back to the live season from roughly week 8. Either is a one-line change and the gate grades both.
- **Column ids and parameter keys are permanent.** Source of truth: they persist into saved views and immutable short URLs. Every coined view id is a one-way door.
- **The population-floor gate CANNOT reach a populated database in CI.** Settled 2026-09-26. It is a workstation and deploy-time check, and this plan must not imply CI coverage.

  Three independent facts, all from the tree:

  - `.github/workflows/test.yml` initdbs a fresh empty cluster per run and `test/global.mjs` loads `db/schema.postgres.sql` into it. The database has the schema and **zero NFL plays**, so every view's row count is 0 and a population floor over it is either always red or vacuous.
  - The `private-tests` job is the only one that runs `yarn check:cluster`, and it runs it with **no postgres started at all**.
  - `scripts/check-cluster-gates.mjs` already names this class. Its prerequisite vocabulary is `none`, `base-ref`, `user-base-prose`, `test-container`, `private-test-database` and `production-tunnel`, and every gate needing real rows — `conflated-player-rows`, `saved-view-param-coverage`, `data-view-url-param-coverage` — declares `production-tunnel`, which is the `base db` SSH tunnel on `127.0.0.1:15432` and exists on no runner.

  So the gate registers as `requires: 'production-tunnel'`, skips loudly in CI exactly as its three siblings do, and its header says which half it covers. The runner's probe-and-skip convention already guarantees it cannot report a green over a surface it could not read.

## Design

### Three vocabularies, crossed by hand

A view is a column line, a segment, and a grain. Each vocabulary is a named constant set in one module, and the crossings that ship are enumerated rather than generated — 6 lines by 25 segments by 2 grains is 300 views, mostly cells nobody asks about. The vocabularies are the reusable part; which pairs ship is the judgment.

### Five families

| family | answers | axis-3 tag |
| --- | --- | --- |
| Volume and opportunity | how much does he get, and what was it worth | `opportunity` |
| Talent and efficiency | what does he do with it, and does he beat what it deserved | `efficiency` |
| Situational | where does the usage come from | inherited from the line |
| Team and coach | where does opportunity come from | `team-stats` / `defense-stats` |
| Market | what is he worth | `███████████████` / `trade-values` |

Expected fantasy points sits in volume and opportunity, because it measures what the usage was worth independent of what he did with it. Points over expected sits in talent and efficiency, because beating what the usage deserved is a talent signal. This split means no new axis-3 value is needed and the LLM tagger's reserved-set contract is untouched.

### A segment declares its coverage window

`{ key, params, first_season, last_season }`. The view name and description derive from the window, so a bounded axis cannot ship without its era in the title. This converts era-stamping from a discipline anyone can forget into something the factory does, and it is the mechanism that prevents the whole class of defect this plan exists to avoid: a view that lies about its population. Three segments carry a window, not one — separation, coverage and endzone target, per Assumptions.

**`admits_lines` is cut.** It was to have declared which segments can carry a rate column, on the reasoning that a rate needs a denominator its population can support. The reasoning is right and the mechanism is wrong: whether a segment supports a denominator is a fact about the DATA IN A SEASON, and it moves. Goal-to-go supports a rate over completed 2025 (2,345 plays) and does not at week 3 of 2026 (1.9 touches per ranked player). A hand-maintained per-segment boolean states a per-season truth as a constant, so it is wrong for one season or the other from the day it is written, and nothing reports the drift.

The population-floor gate already MEASURES this, per season, from the rows. Cutting the boolean removes a field from the vocabulary, removes the judgment call from every future segment, and leaves the question answered by the one thing that can actually answer it.

Segments reuse `score_diff_preset_values` at `libs-shared/nfl-plays-column-params.mjs:20` for the script segments rather than restating ranges — Leading, Trailing and Tied are already named and bounded there, and hand-coding them here would be the second enumeration this plan's own pinning rule warns about.

### Naming

Column lines are ALL_CAPS constants naming the distinguishing property in full words: `OPPORTUNITY_COLUMN_LINE`, `RECEIVING_EFFICIENCY_COLUMN_LINE`, `RUSHING_EFFICIENCY_COLUMN_LINE`, `PASSING_EFFICIENCY_COLUMN_LINE`, `TEAM_UNIT_COLUMN_LINE`, `TEAM_PACE_COLUMN_LINE`, `MARKET_VALUE_COLUMN_LINE`.

**`TEAM_DEFENSE_ALLOWED_COLUMN_LINE` is cut and `TEAM_PACE_COLUMN_LINE` takes its place.** `nfl_plays_team_column_params.team_unit` is a live `off`/`def` SELECT that the team from-plays factory already consumes, so the defensive side of every team column is the SAME line under a different param. A second constant restating the same column ids would be a copy whose two halves drift. Team defense is therefore a segment over the team line, which is exactly the shape the rest of the catalog uses, and it falls out of the vocabulary rather than being added to it.

Pace is separate for the opposite reason: its columns COUNT possessions rather than measure what happened inside them, and folding them into the team production line would yield two team views ranking the same teams in nearly the same order — which the catalog's own non-degeneracy assertion is built to fail.

Segment keys are lowercase inside `DATA_VIEW_SEGMENTS`. The SCREAMING namespace belongs to view ids, and a segment named `SHORT_YARDAGE` beside a view id `SHORT_YARDAGE` is the one-identifier-two-roles trap this repo already paid for on `play_type`, where a grep returned hits from the wrong role and produced a wrong blast-radius figure.

The five existing view ids are frozen and internally inconsistent — `REDZONE_OPPORTUNITY`, `GOAL_LINE_OPPORTUNITY`, `TWO_MINUTE_OFFENSE`, `SHORT_YARDAGE`, `LATE_DOWN_USAGE` mix two spellings of red zone and three suffix conventions. They are persistence keys riding published short URLs, so they are a documented scar rather than something to conform. New ids follow segment-then-line consistently.

### The map is split out

The catalog map — a full-screen panel over any view, `/data-views/catalog` as the addressable form, and the no-view-history first load — now lives in [[user:task/league/data-views/build-data-view-catalog-map.md]], with its design and both inherited first-paint hazards carried there in full.

It reads the catalog module and does not write it, so it is `blocked_by` this task and blocks nothing here. Splitting lets the views ship when they are done rather than waiting on UI work, and lets the map wait for [[user:task/league/data-views/land-data-views-first-paint-selection.md]] to close rather than racing it inside one resolver.

One constraint stays on this side, because it constrains the CATALOG MODULE rather than the map: the map derives everything it renders from that module. If anything hand-lists views it becomes a second registry and drifts the first time someone adds one — the failure `SYSTEM_VIEW_IDS` has today with a header claiming an import that does not exist.

### Two enumerations, pinned rather than merged

The catalog declares a view's family; `derive_auto_tags` infers its tag from column ids. Collapsing them would break tagging for every user view, which has no declaration. Leaving them unpinned lets names and navigation drift silently. Per [[user:guideline/write/write-software.md]], the answer is a conformance test asserting the declared family implies the derived tag.

### Defects the foundation fixes rather than inherits

`default-data-views.js` evaluates clock-dependent getters at module load — `is_regular_season_finished` at line 9 and `situational_base_params` at line 32. Both freeze at import and never move, which CLAUDE.md forbids explicitly. A 41-view catalog where every view carries `year` multiplies the exposure, so the catalog is built by a function called at read time.

### Knowledge Base Impact

- **Domain entry document:** [[user:text/league/data-views/data-view-service-objectives.md]] exists and gains the catalog's organizing vocabulary. No new entry document.
- **Affected ABOUT.md files:** none. No new directory in the knowledge base.
- **█████ █████:** [[███████████████████████████████-█████████-████████]] gains the three upstream-blocked 2026 columns as a population caveat. Its `yard_line_number` entry already stands and the `is_goal_to_go` entry belongs to the repair session.
- **Guideline candidates:** one — that a segmented view must declare its coverage window. It is a direct instance of the visible-seam rule in [[user:guideline/never-estimate-source-data.md]] and folds there rather than being minted separately.
- **Tag implications:** none. The four-axis vocabulary is reused unchanged and no new reserved value is needed.
- **Relations:** `blocked_by` the charting-seam task and `relates` to the available-opportunity and utilization-score tasks are wired. Adds `relates` to the unified-defense and dynamic-glossary tasks.
- **Reference reconciliation:** `docs/data-views-system.md` and `docs/glossary.md` are the two surfaces this makes stale on ship.

### Working tier

No scratch slug. The deliverable is code plus a view enumeration that fits in this body; nothing multi-file and non-canonical is produced.

### Orchestrator shape

Single task. One file family plus tests, gates and documentation — plausibly three or four sessions, which is a task rather than a sustained domain with cross-cutting decisions across many child tasks.

## Notes

The catalog is seeded at the full enumeration and cut in review. Deleting an entry is one line, and seeing the whole list is what makes the cuts obvious.

Three use cases are deliberately out of scope because they are not table-shaped, and forcing a view into their shape would be worse than leaving them: "who is my best play this week"; "is he going to play", which is availability, where the depth chart carries no availability signal and needs a `players_status` join per row; "what changed for him", which is a per-entity narrative rather than a leaderboard. Each is a page, and naming them here is so a later reader does not mistake their absence for an oversight.

**The stated reason for excluding the first one was wrong and is corrected here, because a later reader would have relied on it.** A data view DOES have an opponent slot: `nfl_plays_team_column_params.matchup_opponent_type` offers Current Week Opponent and Next Week Opponent, and `libs-server/get-data-view-results.mjs` consumes it from both column params and where clauses. The exclusion stands on the other half of the argument — the question wants one player against one opponent with a recommendation attached, which is a per-entity answer rather than a ranking — but "there is no opponent slot" is false, and anyone who took it at face value would skip a param that already works.

## Plan

### Foundation

- [x] **Named column lines** — landed `d5c9ff9d0`
  - File: `libs-shared/default-data-views/data-view-column-lines.mjs`
  - Purpose: the measure vocabulary, so a line is written once and reused across every surface that needs it.
  - Changes: exports the seven ALL_CAPS line constants plus `DATA_VIEW_COLUMN_LINES` collecting them, so the conformance test iterates the vocabulary rather than a hand-listed set. `OPPORTUNITY_COLUMN_LINE` carries the existing eight columns including expected fantasy points; the three efficiency lines are rates only plus `player_points_over_expected_from_plays`.
  - Verified: all 57 ids across the seven lines resolve in `libs-server/data-views-column-definitions/index.mjs`. The check is a permanent spec at `test/app.default-data-view-column-lines.spec.mjs`, not a script. Control run — misspelling one id in the rushing line fails it naming that line and that id; the unmodified tree passes; the two readings differ.
  - **This discharges the UNCHECKED column assumption for every line except the two Team views cut above**, which is where the surprise actually was.

- [x] **Named segments with coverage windows** — landed `82f41d4c9`
  - File: `libs-shared/default-data-views/data-view-segments.mjs`
  - Purpose: the situation vocabulary, with the era declaration that makes a bounded axis self-describing.
  - Changes: 25 segments, each carrying `params` and optional `first_season` / `last_season`. No `admits_lines`. All three bounded axes declared per Assumptions. Script segments read their ranges off `nfl_plays_column_params.score_difference.preset_values` rather than restating them, and `bounded_segment_keys` is derived rather than listed.
  - Verified: six assertions, all green, each proved to report. Four injected faults — a misspelled param key, a base param inside a segment, coverage losing its window, a bad enum value — each fail it against 6 passing unmodified, and every reading differs from the control. The param-resolution assertion is scoped to situational keys, since `year` and `seas_type` are not among the registry's 254.
  - **Three traps found while resolving the params, each recorded beside its segment:**
    - `offense_personnel` is an OBJECT_PRESET over dedicated count columns, not a match on the personnel string. Structured `rb=1, te=1` matches 20,458 plays on 2025; the string `'1 RB, 1 TE, 3 WR'` matches 19,755, silently dropping every six-lineman variant — 3.4 percent, with nothing to indicate it. The preset compares for EQUALITY and ORs objects, so "two or more tight ends" is an explicit list rather than a range.
    - `targeted_receiver_separation` is a CATEGORY, not a distance in yards. Its permitted set is OPEN, TIGHT_COVERAGE, ONE_STEP_OPEN, WIDE_OPEN, CLOSING_COVERAGE, so a numeric range would filter nothing.
    - `box_defenders` reads 0 on 7,226 REG plays in 2025, and those are non-scrimmage rows where a box count is meaningless — only 2 of the 7,226 are PASS or RUSH, so it is a correct reading rather than a null sentinel. The scrimmage default already excludes them; the light-box floor is 4 so the segment stays correct if the play-type set ever widens.

- [x] **The factory** — landed `1cb00804c`
  - File: `libs-shared/default-data-views/build-default-data-view.mjs`
  - Purpose: turn a line, a segment and a grain into a view, deriving the name and description from the segment's coverage window.
  - Changes: `build_default_data_view` plus `resolve_opening_season`, which refuses to open a back-bounded segment past its window however current the clock is — so no catalog entry has to remember it. Reads `current_season` inside the function. Refuses an unknown grain and an empty column line rather than falling back.
  - Verified: 10 assertions green, four injected faults each red — a shared params object, a window appended to every name, a resolver ignoring `last_season`, and a silent grain fallback. The shared-object fault still passes the byte-identical assertion and is caught only by the aliasing one, which is why both exist.

- [x] **The catalog module** — landed `b91600b5c`
  - File: `libs-shared/default-data-views/default-data-view-catalog.mjs`
  - Purpose: the enumerated crossings as data, exported as a function so nothing clock-dependent freezes at import.
  - Changes: `build_default_data_view_catalog()` returning 33 views across the five families, each entry naming its line, segment, grain, family, question text and promoted flag. `build_system_view_ids()` derives the id set from the same source.
  - Verified: 11 assertions green, four injected faults each red — **including the module-level frozen form, written out and watched go red**, which is what the plan claimed could not pass. The bounded SET is pinned rather than a count, so the original assertion naming separation alone now fails.
  - **Four views are era-stamped, not one:** man coverage and zone coverage (2025 onward), tight coverage (2023-2025), end zone targets (2024-2025). Tight coverage opens on 2025 rather than the current season.

- [x] **Rewire the app registry onto the catalog**
  - File: `app/core/data-views/default-data-views.js`
  - Purpose: retire the frozen getters and make the app a consumer rather than a second source.
  - Changes: the module body is now a call into `build_default_data_view_catalog()`. `situational_base_params`, `situational_opportunity_columns` and `build_situational_view` are gone, and the module-level `is_regular_season_finished` became `get_default_data_view_view_id()`, converting five of its six call sites from import-time to read-time. The situational-views comment block is kept.
  - **The catalog was missing the five frozen legacy ids, so the rewire had to carry them or drop five shipped views.** `SEASON_FANTASY_POINTS`, `SEASON_PROJECTIONS` and the three `*_STATS_BY_PLAY` are now catalog entries carrying a read-time `build` rather than a line, because none fits the factory: the first mixes per-column params by design, the second switches its whole line on `is_fantasy_offseason`, and the last three carry no params at all. Catalog is 38 views.
  - Verified: every one of the ten shipped views is present and byte-identical column-for-column, except the five situational ones moving to the last completed season, which is the anchoring decision recorded in Assumptions. Full suite green.

- [x] **Derive the system id registry from the catalog**
  - File: `libs-shared/view-organization/system-data-view-ids.mjs`
  - Purpose: make registration impossible to forget, retiring the drift class rather than documenting it.
  - Changes: `system_view_ids_set` derives from `build_system_view_ids()`. The header is corrected. `SYSTEM_VIEW_IDS` and `system_view_ids_array` are deleted rather than rewired — neither had a consumer, and only the set was ever read.
  - Verified: the set is asserted equal to the catalog id set in BOTH directions, since a derivation returning everything passes a subset check and a stale superset literal passes a superset check. Control — restoring the hand-written ten-id literal fails it against 13 passing clean.

- [x] **Retire the duplicate column lines on the players page** — closed 2026-09-26: the two surfaces stay independent, operator decision
  - File: `app/core/players/default-players-views.js`
  - **There is no shared line to import, so this item cannot be done as written.** The two registries are not two copies of one list in one vocabulary. They are different identifier spaces backed by different pipelines: a data view asks for `player_pass_completion_percentage_from_plays` through `libs-server/data-views-column-definitions/`, while the players page asks for `stats.pc_pct`, computed by `libs-shared/calculate-stats-from-plays.mjs` and served through `libs-server/get-players.mjs`. Nothing maps one to the other, and none of the 15 `_from_plays` names in `app/core/player-fields.js` is a data-view column id.
  - So the 22/17/16/14 against 24/16/17/14 divergence is not unmaintained drift between copies. It is two systems that happen to name four views alike, and closing it means either a translation table of roughly 60 identifiers — a THIRD registry, which is one more rather than one fewer — or retiring one pipeline, which is its own project.
  - The four shared lines now exist in `data-view-column-lines.mjs` regardless, which removes the duplication on the data-views side. What remains open is a product question rather than a refactor: whether a view named "Passing Stats by Play" should show the same measures on both surfaces.

### Catalog content

The gate comes BEFORE the views it grades. A per-view population
measurement is the instrument that decides which crossings are honest
enough to ship -- discovering that after enumerating thirty views means
enumerate, measure, then cut, where one pass would do.

- [x] **The population-floor gate** — landed `24b810ad0`, green on all 38 views
  - File: `db/gates/check-data-view-population-floor.mjs`
  - Landed with the two assertions as specified, registered `requires: 'production-tunnel'`. The denominator is a COLUMN appended to each view's own generated query carrying the view's params — `player_opportunities_from_plays` at player grain, `team_play_count_from_plays` at team grain — rather than a figure derived beside the query. A view whose sort column is not a from-plays column is reported `no-event-denominator` and graded on the row floor alone, since a market view ranks on a projected salary and has no events behind a row.
  - **The floors are measured, not chosen.** Over the catalog against production: every view clears 5.94 events per ranked row on completed 2025, and eight fall between 2.12 and 4.33 three weeks into 2026. `EVENTS_FLOOR` is 5, the value separating those two populations; `ROW_FLOOR` is 30, below the 32 rows a team view must return, so it fires on a collapse rather than on a narrow segment.
  - Verified: all five negative controls fire. Hard zero — tight coverage forced onto 2026 ranks 0 and fails, against 324 ranked at 22.03 on 2025. Thin denominator — the full form fails goal-to-go on 2026 and passes it on 2025 (exit 1 against 0), while `--row-count-only` passes BOTH, which is the control proving the second assertion earns its place.
  - **The `is_goal_to_go` pre-repair re-run was NOT possible and this is the honest half-claim.** That repair backfilled every season 2019-2026 and the pre-repair state is no longer reachable in the database. What was verified is the structural equivalent: a filter matching no rows in the season the view opens on, which is exactly the shape the flag had, and the gate reports it as 0 ranked.
  - Landed GREEN rather than red, per the rule in `docs/guides/gates.md` that a new gate's finding count is measured and dispositioned before its manifest row goes in. All eight findings were one class — segmented views on a partial current season — repaired by the anchoring rule in the same commit.

- [x] **Fold the non-degeneracy assertion into the same gate** — landed `5ce8fec88`
  - File: `db/gates/check-data-view-population-floor.mjs`
  - **It found a real defect on its first run, in views this plan had just landed.** All three efficiency views returned the SAME top 20 players in the SAME order — a back, a receiver and a quarterback interleaved — because every efficiency line opens with `player_points_over_expected_from_plays` and the factory defaults the sort to the first column of the line. Three views, one leaderboard, differing only in the columns to the right of the sort. Each now carries an explicit `sort_column_id` naming its own rate, which also does the position filtering for free: a quarterback has no receiving yards per target and sorts out of the receiving view with no position param.
  - **The rule is SEQUENCE EQUALITY of the top 20, not an overlap percentage,** and the difference matters. Views over one season legitimately share most of their top — the best players are the best players — so a percentage threshold fires on honest pairs and has to be tuned upward until it catches nothing. Two leaderboards landing on the same subjects in the same ORDER is not coincidence: one of the two things distinguishing them, the column line or the segment param, never reached SQL.
  - Verified: four controls on this assertion alone, because it can be wrong in three directions and only the first looks like a failure — identical order is reported; the same subjects in a DIFFERENT order are not; identical tops at different grains are not compared, since they share no identity space; and two EMPTY views are the floor's finding rather than this one's. Nine controls total on the gate, all firing.
  - **The cheap half is also pinned in the spec**, because the gate needs a production tunnel and CI has none: two views at the same grain with the same params and the same sort column are the same leaderboard, and that needs no rows to detect. Control fires naming both views and the shared column, 13 passing against 14 clean.
  - The team pair this was also written for came back clean on its own terms: `team_unit` does reach SQL, and the offense and defense profiles return the same 32 teams in genuinely different orders.

- [x] **Volume and opportunity family** — landed `750c56524`, all four shipped
  - **Landed 2026-09-26 by widening the view vocabulary rather than cutting the views.** The factory now carries `where` (a SUBJECT filter — Backfield Workload filters `player_position IN ('RB')`, 95 backs at 19.13 events each) and `row_axes` (Opportunity Trend by Week, on `['year', 'week']`). The gate forwards both, counts the whole result set instead of a 2000-row page, and sums rows back to their subject, so the events floor means the same thing on every axis; a live control proves a week split ranks the same 2025 subjects on more rows.
  - **Why `['year', 'week']` and not `['week']`:** the query builder's week-only axis groups and joins on the week number alone — measured, it sums 2024 and 2025 into one row per week (Barkley week 1 = 42 = 24 + 18) and crashed when sorted on fantasy points. Fixed in league `c6c692e46`, which normalizes a week axis to year and week.
  - File: `libs-shared/default-data-views/default-data-view-catalog.mjs`
  - Purpose: the unsegmented base the five situational views lack, plus the distribution and trend questions.
  - Shipped: **Season Opportunity** and **Team Target Distribution** (`5a80aebf4`), the second being the same line and population under a different sort — who a team throws to rather than who produced. Both clear the gate and are non-degenerate against everything else.
  - Route Participation stays gated behind [[█████████████████████-███-█████-█████-███████████-██████-███-████████-███████]].

- [x] **Talent and efficiency family** — landed `5ce8fec88`
  - File: `libs-shared/default-data-views/default-data-view-catalog.mjs`
  - Purpose: surface the 43 rate columns and the points-over-expected measure, which no default view carries today.
  - Changes: Receiving Efficiency, Rushing Efficiency, Passing Efficiency. **Three views, not six — the per-drive crossings are cut.** Their stated purpose was "to exercise the unused depth axis", which is building views to exercise a feature rather than to answer a reader's question. The output period stays available on every column; it just does not get three default views of its own.
  - **The three shipped as ONE leaderboard under three names and the non-degeneracy assertion caught it**, because every efficiency line opens with points over expected and the factory defaults the sort to the first column. Each now sorts on its own rate. See that item for the measurement.

- [x] **Situational family** — landed `5ce8fec88`
  - File: `libs-shared/default-data-views/default-data-view-catalog.mjs`
  - Purpose: run the factory across the segment vocabulary instead of once on one axis.
  - Changes: the five existing views keep their frozen ids and stay volume-only. Add goal-to-go on the true flag, early down, leading, trailing, neutral script, play action, pre-snap motion, man coverage, zone coverage, heavy box, light box, eleven personnel, heavy personnel, deep target, short target, under pressure, no huddle, and by quarter. Era-stamp the three bounded axes per Assumptions: separation 2023-2025, endzone target 2024-2025, both coverage segments 2025 onward.
  - Verify: the population-floor gate passes for every new segment on the season its view opens on; **exactly the three bounded segments carry a window in their name, and each name states the window Assumptions measured.** Seed the fault both ways — drop the window from the coverage view and confirm it fails, and add a window to an unbounded view and confirm that fails too.
  - **The original verify asserted separation was the ONLY windowed view, which a correct implementation now fails.** Coverage and endzone target are bounded as well, measured 2026-09-26. An assertion written from a wrong premise does not merely miss a defect, it forbids the fix.

- [x] **Team and coach family** — landed `5ce8fec88`, the three views that were in scope
  - Verified: all three return 32 team rows on completed 2025 and clear both floors. The `team_unit` reach is confirmed rather than assumed — the offense and defense profiles return the same 32 teams in genuinely different orders, which the non-degeneracy assertion checks and a 32-row count could not distinguish. Pinned in the spec as well, by a dedicated assertion.
  - File: `libs-shared/default-data-views/default-data-view-catalog.mjs`
  - Purpose: reach the grain no default view uses, against 41 team column definitions.
  - Changes: **three views, not five.** Team Offense Profile, Team Pace, and Team Defense Profile — the last being the same `TEAM_UNIT_COLUMN_LINE` under `team_unit: ['def']` rather than a line of its own. All anchored on the last completed season per the sample-size assumption. Ship `team_pass_completions_from_plays` and `team_pass_attempts_from_plays` separately, since the team-grain completion rate does not exist.
  - **Two views are cut here and are the whole of this plan's server cost, so they are cut rather than smuggled in.** Neither is a one-line catalog entry, and both were named on the assumption that a column existed:
    - **Playcaller Tendencies** — zero coach or play-call columns among the 615 definitions today. [[user:task/league/add-coach-play-caller-data-view-columns.md]] (Planned, High) builds them, including three team-grain display columns. Sequence this view after that task; it is then a team-grain entry needing no new grain.
    - **Fantasy Points Allowed by position** — no team-grain fantasy-points column exists. `team_unit: ['def']` reaches yards and touchdowns allowed but not fantasy points, so this needs a new server column.
    Both are worth having. Neither belongs in a plan whose premise is that adding a view is a one-line change, and folding them in here would quietly convert this from a catalog task into a column-definition task.
  - Verify: each of the three returns 32 team rows for a completed season, and the same view on the current season is shown to fail the population floor, which is why the anchor exists. Assert the defense view's rows DIFFER from the offense view's on the same season — identical numbers mean `team_unit` never reached SQL, which a 32-row count cannot distinguish.

- [x] **Market family** — landed `be65b6ff6`
  - Rushing Yards, Receiving Yards and Receptions Prop Hit Rates: this week's line, price, implied probability, last-ten-games hit rate and edge, sorted on edge. `measure_params` carries the market into every column without segmenting the view, so it prices the current week. Current-season window rejected: at week 3 the top of an edge sort is every 3-for-3 player. Passing yards (at most 32 lines, under the row floor on a bye week) and anytime touchdown (lineless, zero rows) are cut. The gate now ranks a no-denominator view on rows whose sort value is present, so Market Value reads 548 rather than 380 players-with-snaps.
  - File: `libs-shared/default-data-views/default-data-view-catalog.mjs`
  - Purpose: the value questions, reusing existing market and trade-value columns.
  - Changes: Market Value and Betting Market Hit Rates. `SEASON_PROJECTIONS` joins this family with its id unchanged.
  - Verify: each returns rows with non-null market values for the current season, and the hit-rate view's denominator is stated in its description.

- [x] **Coordinate the defensive views with the unified-defense task** — boundary recorded on both tasks 2026-09-26
  - Purpose: the team-defense crossing overlaps an In Progress task with no live session; its findings are inherited rather than rediscovered.
  - Changes: reconcile Team Defense Profile and Fantasy Points Allowed against that task's personnel, coverage-shell and pass-rush design, and record on it which half this plan takes.
  - Verify: an observation on [[█████████████████████-███████████-███████-███████-████-███████]] names the boundary, and no column line here duplicates one it defines.

### Testing and validation

- [x] **Generalize the situational spec to the whole catalog** — already catalog-driven; verified 2026-09-26
  - All four seeded faults fire alone against the existing specs: an unresolvable segment key, a divergent column param, a `yard_line_number` reach, and a bounded segment missing its window. Clean run 70 passing.
  - File: `test/app.situational-default-data-views.spec.mjs`, renamed to `test/app.default-data-view-catalog.spec.mjs`
  - Purpose: the existing assertions grade five hand-listed views against a hand-listed expectation; they must grade every view the catalog produces.
  - Changes: drive every assertion from the catalog rather than the inline `situational_views` literal. Keep the `yard_line_number` prohibition and the identical-parameters check. Add: every segment's keys resolve, and every segment with `last_season` has its window in the view name.
  - Verify: seed four faults — an unresolvable parameter key, a column whose parameters diverge from its siblings, a `yard_line_number` reach, and a bounded segment missing its era — and confirm each fires alone. Sweep the test tree by file path as well as by symbol, since a spec can consume a source file as text rather than importing it.

- [x] **The declared-family conformance test** — landed `58f093f6a`, by fixing the tagger
  - Opportunity against efficiency is now a vote with rates counted as efficiency, so every factory-built view derives its family's tag. The spec pins family to allowed tags, exempts the two frozen mixed-line views by literal and checks each still derives outside its family. Fails on the old tagger naming five views, and on a mislabelled family naming the entry. Tags are render-time, so user views pick up the correction with no migration.
  - File: `test/app.default-data-view-catalog.spec.mjs`
  - Purpose: pin the relation between the declared family and the derived tag, rather than merging two enumerations that legitimately differ.
  - **"Efficiency family implies the `efficiency` tag" is false today, measured 2026-09-25 by running `derive_auto_tags` over every line in `DATA_VIEW_COLUMN_LINES`.** This is the fourth verify line in this plan that a correct implementation would fail:

    | line | derived axis-3 tag |
    | --- | --- |
    | `RECEIVING_EFFICIENCY_COLUMN_LINE` | `opportunity` |
    | `RUSHING_EFFICIENCY_COLUMN_LINE` | `play-by-play` |
    | `PASSING_EFFICIENCY_COLUMN_LINE` | `play-by-play` |
    | `TEAM_UNIT_COLUMN_LINE` | `efficiency` |
    | `MARKET_VALUE_COLUMN_LINE` | `███████████████` |
    | `SEASON_PROJECTIONS_COLUMN_LINE` | `projections` |

    The tagger's `derive_metric_domain` reads column-id patterns, and a rate column named `*_from_plays` reads as play-by-play to it. So the only line deriving `efficiency` is the TEAM one, and `SEASON_PROJECTIONS` derives `projections` rather than either market value. Writing the assertion as stated would fail the catalog as built and forbid the correction.
  - Verify: mislabel one entry's family and confirm the test fails naming that entry; confirm it passes on the unmodified catalog; and whichever option is taken, assert the mapping is non-trivial — a test that pins every family to whatever it currently derives cannot fail.

### Knowledge base updates

- [x] **Record the 2026-empty axes as a population caveat** — landed `0ec0df2555`
  - Entity: [[███████████████████████████████-█████████-████████]]
  - Written from a RE-MEASUREMENT rather than from this task's Assumptions table, per the rule that an inherited figure is a premise until it is checked. Two of the three needed correcting, and both corrections matter more than the entry they were meant to support:
  - **`is_endzone_target` is non-null from 2023 but ALL FALSE in 2023** — 19,427 non-null and 0 true, against 1,202 true in 2024 and 1,139 in 2025. The Assumptions window of 2024-2025 was right about TRUE rows and the reason was wrong: this is not a gap, it is a populated column contributing nothing, so a null check passes and a 2023-2025 rate divides three seasons of denominator into two of numerator. Worse than an empty column, because the usual defence does not fire.
  - **The sheet's existing claim that coverage charting is "nothing at all before 2023" is FALSE**, and it was load-bearing. `coverage_type` sits at 88-94 percent of dropbacks across 2018-2022 on the same `COVER_*` vocabulary, verified by reading distinct values per season rather than by counting nulls. Its stated figures — 99 / 93.4 / 87.8 percent for 2025 / 2024 / 2023 — reproduce at no grain today; a backfill most likely landed after they were written. Corrected in place with the full nine-season table and the grain stated.
  - **There are TWO coverage columns and the catalog uses the narrower, newer one.** `coverage_type_charted_primary` is empty before 2025 and is a strict subset of `coverage_type` on 2025 (67.9 percent of dropbacks against 99.9). So the man- and zone-coverage views are era-bounded to 2025 onward because of the COLUMN CHOICE, not because coverage is unavailable — an eight-season shell trend is available on `coverage_type` today. See the open decision below.
  - Verify: `base entity get` renders the addition; separation containment checked against dropbacks per season; the all-play-type and PASS-only grains are quoted separately because that is exactly where the two get confused.

- [x] **The coverage segments filter the CANONICAL now** — landed `3de0bfd56`, eight seasons recovered
  - File: `libs-shared/default-data-views/data-view-segments.mjs`
  - **The question "should we build a canonical coverage type spanning multiple sources" is already answered: one exists and is deployed.** `coverage_type` is a derived canonical over `coverage_type_tracking`, then `coverage_type_charted_primary`, then `coverage_type_charted_secondary`, declared in `libs-server/derive-play-canonical-columns.mjs` and written only by enrichment. Owned by [[██████████████████████-████████-██████-█████████]] and [[███████████████████████-█████████-████-███████-████-██████████]], both **Completed**, with the precedence reasoning in [[█████████████████████-█████████-██████-█████████████]]. `is_blitz`, `pass_rushers`, `box_defenders` and `coverage_class` follow the same pattern. Searching before proposing is what stopped this from becoming a duplicate build.
  - **The defect was that this catalog queried a SLOT.** Both segments filtered `coverage_type_charted_primary` against a hand-written shell list, which is why they carried `first_season: 2025` — a fact about when one supplier started, era-stamped as a fact about the sport. They filter `coverage_class` now: MAN / ZONE / SITUATIONAL / MISC, the canonical that answers the question directly.
  - Measured 2026-09-26 on REG `play_type = 'PASS'`: `coverage_class` populated on 17,136 of 18,952 dropbacks in 2018 and 2,238 of 2,243 in 2026, live every season between. The gate returns 452 and 519 ranked players on 2018 and 449 and 546 on 2022, where the old filter returned nothing at all.
  - **The shell list was also WRONG, and unfixably** — the class is not a function of the shell. On 2025 `COVER_2_MAN` appears in both classes and `COVER_1` is ZONE on 72 rows. Against the old lists: man over-counted by 132 and missed 42, zone over-counted by 88 and missed 177. A better list cannot reach it, because the canonical carries a judgment the shell name does not.
  - Verified: both bounded-set assertions fired on the change — the segments spec and the catalog spec each pin the windowed set by literal, so a segment losing its window fails a test rather than shipping quietly. Suite 13,055 passing; gate green on all 39 views with the `coverage_class` predicate confirmed in the statement by the operator-anchored assertion.
  - **The remaining headroom is not more sources.** 2016 and 2017 are zero across all three slots, 38,096 dropbacks. Before recording that as permanent, apply this sheet's own rule and identify who wrote the 2018+ rows — a vendor window that looks closed is not proof the data never arrived. Out of scope here and not opened.

- [x] **Extend the service objectives document with the catalog vocabulary** — Default views section added 2026-09-26
  - Entity: [[user:text/league/data-views/data-view-service-objectives.md]]
  - Changes: the five families, the three vocabularies, and the rule that a segment declares its coverage window.
  - Verify: a reader can determine from that document alone which family a proposed new view belongs to.

- [x] **Fold the coverage-window rule into the source-data guideline** — folded into the visible-gap rule 2026-09-26
  - Entity: [[user:guideline/never-estimate-source-data.md]]
  - Changes: add that a segmented view must declare its coverage window and carry it in its name, as an instance of the existing visible-seam rule.
  - Verify: the addition reads as an instance of that guideline's stated rule rather than a new one, and no separate guideline is minted.

- [x] **Reconcile the stale reference documentation** — landed `9f35bfae1`
  - `docs/data-views-system.md` gained the catalog section; `docs/glossary.md` carries no data-view terms and is unchanged. The retired-name grep returns nothing.
  - Files: `docs/data-views-system.md`, `docs/glossary.md`
  - Changes: document the catalog module, the three vocabularies, the map surface, and the new view names. Grep both for the retired `situational_opportunity_columns` and `build_situational_view` names before committing.
  - Verify: `grep -rn "build_situational_view\|situational_opportunity_columns" docs/ app/ libs-shared/` returns nothing outside the historical comment that explains the scar.

- [x] **Wire the remaining relations** — both edges present; all eight targets resolve
  - Entity: this task
  - Changes: add `relates` edges to the unified-defense and dynamic-glossary tasks.
  - Verify: `base entity get --json` shows all five relations and every target resolves.
