---
title: Reconcile the pinned per-game denominator year with the year row axis
type: task
status: completed
description: >-
  Under a year row axis the per-game denominator joins at a pinned year constant while the measure
  numerator correlates to the axis, so a raw games-played column pinned to one season renders that
  season's value on every row while the rate beside it renders blank.
base_uri: user:task/league/data-views/reconcile-pinned-denominator-year-with-row-axis.md
created_at: '2026-09-17T15:22:45.933Z'
entity_id: 1d60f727-0b46-4cbb-9def-f68ff13f51a1
finished_at: '2026-09-17T15:59:20.330Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-17T16:34:31.522Z'
visibility_analyzed_at: '2026-11-05T23:48:58.602Z'
---

## Outcome

Shipped. Under a `year` row axis, every year-grained join now correlates to the ROW's season and treats a column's own year param as a narrowing filter. Landed in league `02a527c4e` (the five period-denominator modules), `f2f255881` (its fixture), and `2f67d5bc6` (the plays source-attach copy, the zero-offset alignment, and the routing gate). The first two are deployed in `0f8f93a43`; the third was committed 2026-09-17 and pushes on the ████████ cycle.

## The defect

One column family resolved the row's season two different ways.

- The measure numerator correlated to the axis. `emit_year_match` emits `rate_aggregate_<hash>.year = player_years.year` whenever a `year_reference` exists.
- The denominator pinned a constant. A `single_year_param_set` branch fired whenever `params.year` resolved to exactly ONE year, emitting `<cte>.year = <literal>` and dropping the axis correlation entirely.

So a raw `player_games_played` column pinned to one season rendered THAT season's value on every row of every other season, while the per-game rate beside it rendered blank there. Measured on share `cc928ca2dcaefa91` (generation `79353ca6`): of 500 rows, all year 2026, `games_played_1` — pinned to 2025 — was non-null on 321, while every 2025 per-game rate cell was null on all 500.

The branch existed in SEVEN byte-identical copies, which is how it spread: five period-denominator modules, both per-game variants, and the plays source-attach.

## The decision

**The denominator moved.** A row under a year axis IS a (subject, season) pair, so every cell on it must describe that season or the grain is a lie no sort, aggregate or CSV export can see. A column's year param narrows which seasons it may describe; it does not relabel the row.

The alternative — making the numerator honor an explicit pin — was rejected on two grounds. A broadcast cell is undetectable downstream, and `emit_year_match` is shared by every year-grained source, so the change would have turned currently-correct multi-year-axis views into broadcast views. The plays source-attach already emitted correlate-plus-intersect for its multi-year case, so this makes one rule where there were two.

## What shipped

- `emit_row_axis_year_join` in `libs-server/data-views/param-utils.mjs` is the single primitive, with seven call sites.
- The pin is emitted as its own IN-list rather than left to the CTE body, because `compute_effective_scope` intersects a column's years with the view scope and a DISJOINT pin intersects to the empty set — which leaves the body unscoped and scanning every year.
- A zero `year_offset` is no longer treated as an offset. `[0,0]` routed through `emit_year_match` as `= ref + 0`, equivalent arithmetic that skipped the pin intersection. `get_per_game_cte_table_name` already counted only a non-zero offset when naming the CTE, so the join now agrees with the name.

## Verification

Two `result_equivalence` fixtures, because neither surface's fixture can see a regression on the other:

- `player-games-played-pinned-year-under-year-axis-result-equivalence.json` — denominator surface.
- `player-plays-column-pinned-year-under-year-axis-result-equivalence.json` — source-attach surface.

Each seeds a subject with rows in both seasons, pins the subject column to one, and carries a control spanning both that must agree in both worlds. Both red-proved against the unfixed builder. Scoring was per-assertion: reintroducing the branch in `apply-plays-join` reddened only the offender arm of the gate and only the plays fixture, leaving the import arm and the denominator fixture green.

`test/data-views.row-axis-year-join-routing.spec.mjs` is the wiring gate, covering what no fixture can — an eighth call site added later with a fresh hand-rolled copy. It names all seven consumers in its header and scans only the CONSEQUENT of each year-row-axis branch, sliced by AST.

Run with `yarn test:isolated test/libs-server.data-view-queries.mjs test/libs-server.data-view-queries-result-equivalence.mjs` — no `*.spec.mjs` glob reaches either runner. Full suite green at 10380 passing.

## Population, sized before changing anything

Production holds 221 saved views and 563 generation results. 54 and 80 carry a year row axis; 6 saved views (41 columns) and 3 generation results (26 columns) took the pinned branch. Only 2 saved views and 1 generation result mix seasons across their columns, which is the subset where a rendered value actually changes — including `5b9dfee4-b4d1-4e13-8211-fde433293537`, the live saved twin of the reviewed generation. The plays source-attach added 15 columns across 8 saved states.

## Left open, routed elsewhere

Data-view CTE alias drift across the fixture corpus: 144 fixtures drifting at `8ebf9c34d`, before any of this work. Not caused here — this change introduced zero newly drifting fixtures — but measured here and previously unowned. See [[user:task/league/data-views/re-bless-drifted-data-view-fixture-aliases.md]].
