---
title: Move the regular season boundary onto the season row
type: task
status: planned
description: >-
  The fantasy regular season's final week lives in the global NFL-calendar constant while
  wildcard_round and championship_round are already per league-season; add
  seasons.regular_season_final_week with a correct backfill, convert the league-scoped consumers,
  and leave the constant to the generator and the printer.
base_uri: user:task/league/move-regular-season-boundary-onto-season-row.md
created_at: '2026-08-30T16:39:45.853Z'
entity_id: e543af23-1156-4424-8664-918c90aa5628
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - >-
    relates
    [[██████████████-███████-█████████████-████████████████████████████████████████████-██-████████-█████████]]
  - follows [[███████████████████████████████-█████████████]]
  - relates [[████████████████████████████-██████-████████-██████████]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-10-24T00:35:27.867Z'
visibility_analyzed_at: '2026-11-05T23:49:44.251Z'
---

## Overview

Move the fantasy regular season's final week from the global NFL-calendar constant onto the `seasons` row, so a league-season's calendar is configuration rather than a literal that every consumer reads from one shared file.

**Impact.** No user-visible change on the current league, whose boundary is 14 either way. What changes is correctness for any league-season whose boundary is not 14: today `libs-shared/season-dates.mjs:45` is a flat literal covering only the current season, so a standings or schedule computation for 2023 uses today's value. This is the defect the column fixes, and it is worth stating plainly because the migration otherwise looks like pure churn.

**Ordering.** Split out of `[[user:task/league/correct-the-season-forecast-model.md]]` on 2026-08-30 after review found it carried eight findings and none of that task's user-visible benefit. Independent of it: the forecast still reads the constant (`simulate-season-forecast.mjs:528-529`), and this task changes that read. No ordering constraint in either direction.

## Background

`seasons` already carries `wildcard_round` and `championship_round` per league-season, but the regular season's final week lives in the global constants as `regular_season_final_week: 14`. The split is arbitrary — the same calendar, half configuration and half literal.

### Observed consumer inventory

Re-run 2026-10-23 against branch `standings-from-scored-matchups` (league `77f47b95b`): `grep -rn "fantasy_regular_season_final_week" --include=*.mjs --include=*.js . | grep -v node_modules`. The constant was renamed since this plan was written. `fantasy_regular_season_final_week` is this task's subject; `nfl_regular_season_final_week` is the NFL calendar and stays global. Re-run it at pickup; the classification is what matters.

**League-scoped, year in scope — these convert, reading that year's `seasons` row:**

- `scripts/process-matchups.mjs` (`resolve_regular_season_weeks`) builds the regular-season `weeks` list that the standings kernel and lineup analytics take as input. It already uses a mirror's `wildcard_round - 1`. The kernel itself (`libs-shared/calculate-standings.mjs`) no longer reads any boundary, so this one list is the only standings read.
- `libs-server/simulation/simulate-season-forecast.mjs:528-529` reads the constant before `load_context` runs. `load_simulation_context` (`libs-server/simulation/simulation-helpers.mjs`) already returns `playoff_format` and `has_median_match` off the season row, and the boundary joins them there.
- `scripts/process-projections.mjs:121`.

**Frontend, league-scoped — these convert:**

- `app/views/components/dashboard-team-summary/dashboard-team-summary.js:24`
- `app/views/pages/standings/standings.js:249`
- `app/views/pages/league-home/league-home.js:126` (new since 2026-08-30)

**Genuinely global — these keep the constant:**

- `libs-shared/season-dates.mjs:47` (the literal)
- `libs-shared/season.mjs:73,99` (the `Season` constructor)
- `scripts/generate-season-dates.mjs:38,119` (the generator)
- `scripts/current-season-info.mjs:63` (the printer)

**Not simple converts:**

- `scripts/update-stats-and-process-matchups.mjs:157,160` validates the week argument before any league loads.
- `libs-server/generate-fantasy-league-schedule.mjs:136` has only `teams` in scope.
- `jobs/finalize-week.mjs:285,304` is prose only.
- `libs-server/external-fantasy-leagues/sync/matchup-sync.mjs:15` is prose claiming `wildcard_round` is NULL on every mirror. That is no longer true: config sync now writes it from the vendor, and the comment is stale either way.

**Test readers:**

- `test/generate-fantasy-league-schedule.spec.mjs:47,134`
- `test/playoff-format.spec.mjs:957`
- `test/simulation.season-forecast-played-weeks.spec.mjs:65`
- `test/libs-shared.season-dates-contract.spec.mjs:31` (contract on the constant itself; keeps it)

### Mirror seasons need a first week as well as a final week

Found 2026-10-23 by the Sleeper record oracle on [[██████████████████████-████████-██████-██████-█████████-███████-█████████]]. Lid 129's 2024 season has Sleeper `settings.start_week = 7` and `playoff_week_start = 15`. Sleeper counts weeks 7-14 (16 games with the median match), while we count every scored week from 1, and week 6 carries vendor points. So a mirror's regular season is `[start_week, playoff_week_start - 1]`, and both ends are vendor-declared.

- The Sleeper adapter reads `playoff_week_start` and not `start_week`.
- The final week is already implied for a mirror by its vendor-written `wildcard_round`.
- The first week has no column at all.

Proposed at pickup: add `seasons.regular_season_first_week` beside the final week (`NOT NULL DEFAULT 1`, CHECK first <= final), written by config sync from `start_week` (ESPN: check the equivalent field). `process-matchups` then builds `weeks` as `[first, final]`. Confirm or reject the column against the ESPN payload before the DDL.

### Assumptions

- **The access form is `current_season.regular_season_final_week` in every code reader except the generator.** Checked: the grep above; `scripts/generate-season-dates.mjs` uses `result.` and is the only exception.
- **`seasons.wildcard_round` is nullable with no default.** Checked: `db/schema.postgres.sql:27253` declares `wildcard_round smallint`. This is why the CHECK below cannot be the whole guard.
- **Exactly one existing `seasons` row would violate a naive `regular_season_final_week < wildcard_round` CHECK.** Re-measured 2026-10-23: the 8 hosted rows all carry `wildcard_round`, and only `lid=1, season_year=2020` has 14 (the rest 15). Of the 51 mirror rows, 23 carry the vendor's `wildcard_round` 15 and 28 are NULL until their bracket sync runs. The 115 NULL 2023 imported rows the earlier count saw no longer exist.
- **A null boundary silently produces all-zero standings rather than throwing.** Re-checked 2026-10-24: the read now sits in `resolve_regular_season_weeks` (`scripts/process-matchups.mjs`), where `Math.min(Math.max(week - 1, 0), undefined)` is `NaN`, so the `weeks` list is empty and the kernel counts nothing. This is the failure the NOT NULL column exists to make impossible.
- **League 1 / 2027 is NOT a usable control for that failure.** It had 10 `teams` rows but no `matchups` and no `rosters` on 2026-08-30, so its standings are all-zero regardless of the boundary. Any absent-season-row test needs a purpose-built fixture with matchup data.
- **`getLeague` LEFT JOINs `seasons` and is called WITHOUT a year in the forecast path.** Checked: `simulation-helpers.mjs:52` calls `getLeague({ lid: league_id })`, and `libs-server/get-league.mjs:111` defaults `year = current_season.year`. So `league.regular_season_final_week` is always the CURRENT year's value — which is why year-scoped consumers must read the season row instead. The kernel's absent-season-row fallback (`resolve_season_rules` in `calculate-standings.mjs`) is a BEHAVIOR fallback for a null `playoff_team_count`, not a default value for a numeric week; it does not generalize to this field.
- **No file under `db/fixtures/` supplies a `seasons` row.** Checked: `config/config-test.json:18-21` points the seed directory at `db/fixtures/test`, which contains only `player.mjs` and `player.sql`; `db/fixtures/league.mjs:80` reaches the season row by calling `createLeague`. `libs-server/create-league.mjs:55` is the single birth site for `seasons` rows.
- **Settability mid-season is a hazard, not a checked property.** UNCHECKED. Mitigated by reading the field per run rather than caching it.
- **The forecast does NOT go through the LEFT JOIN and does NOT need a fallback.** Checked 2026-10-23: the default `load_forecast_context` calls `load_simulation_context`, which queries the `seasons` row directly for the requested year and THROWS `Season not found` when it is absent. It already refuses to run without a season row.
- **`load_simulation_context` already returns the league and already reads format off the season row.** Checked 2026-10-23: it returns `{ league, scoring_format_id, has_median_match, playoff_format }`, and its comment states the reason — "Read off the season row rather than the league join, so a forecast for a past year uses that year's playoff format." The boundary must follow that idiom, not the league join.

## Design

The season row becomes the source of truth for the boundary in every league-scoped consumer. The constant remains for the genuinely global readers, which are the generator and the printer — neither of which has a league.

**Two source shapes, not one — this is the correction that matters.** An earlier draft of this plan said every consumer reads `league.regular_season_final_week`. That is wrong for the forecast and dangerous generally, because `league` comes from `getLeague`, whose season join is not year-scoped to the year being computed. `load_simulation_context` already documents exactly this hazard for the playoff format and solves it by reading the season row for the requested year. The boundary is the same kind of value and takes the same route:

- **Year-scoped consumers** — anything computing a specific `year` — read the boundary from that year's `seasons` row. For the forecast that means adding `regular_season_final_week` to what `load_simulation_context` returns, beside `playoff_format`, and reading it AFTER the context load rather than before it. The read sits at `simulate-season-forecast.mjs:528-529`, above the `load_context` call, so the line moves as part of the change.
- **Current-season consumers** — the frontend panels and the per-league scripts operating on `current_season` — may read it off the league record, since current year is the only year they address.

**The column is `NOT NULL DEFAULT 14`, and there are no read-site fallbacks.** An earlier draft shipped a nullable column, a null-tolerant CHECK, and a `??` fallback at every read — three overlapping guards that still left a hole, because `libs-server/create-league.mjs:55` inserts the `seasons` row for every new league and would have written NULL forever. One end state instead:

- `NOT NULL DEFAULT 14` on the column, so a row cannot exist without a boundary.
- `create-league.mjs` supplies it explicitly rather than relying on the default.
- CHECK is `regular_season_final_week >= 1 AND (wildcard_round IS NULL OR regular_season_final_week < wildcard_round)`. The `>= 1` matters: a boundary of 0 means a zero-week regular season, `for (week = 1; week <= 0)` never runs, and the result is the same silent all-zero standings this plan exists to prevent. An earlier draft justified `??` over `||` as "so a legitimately configured 0 is not swallowed" — 0 is not legitimate and the CHECK now forbids it.
- No `??` fallbacks at read sites. With the column NOT NULL and `create-league` patched, a null cannot occur; adding a fallback would only hide a constraint violation.

**Read the boundary from the row that matches the year being computed.** `getLeague` is called WITHOUT a year in `load_simulation_context` and defaults to `current_season.year`, so `league.regular_season_final_week` is the CURRENT year's value regardless of the year being computed. For a historical forecast that is exactly the cross-year defect this task exists to remove. Two shapes follow:

- **Year-scoped consumers** read the boundary from the `seasons` row for the year they are computing. `load_simulation_context` already does this for the playoff format and says why in a comment; the boundary joins it there. `process-matchups.mjs` restates past seasons, so it is year-scoped too: its `getLeague({ lid, year })` joins that year's row.
- **Current-season consumers** — the frontend panels — may read it off the league record.

**Two files leave the convert list.** The multi-league orchestrator and the schedule generator are handled as their own items with their own shapes, rather than being swept into a single "read it from the league" instruction that is false for both.

**No ratchet in this task.** The field stays dual-sourced by design — the constant is still correct for the global readers. A lint forbidding new `current_season.regular_season_final_week` reads outside the four global files would be the end-state guard, and `local/no-season-member-reconstruction` is the precedent, but it is a separate concern from the migration and is recorded as a follow-up rather than bundled here.

### Naming decisions

- **`regular_season_final_week`** for the new `seasons` column: matches the constant it replaces, every existing call site, and the `regular_season_wins` / `regular_season_finish` column family. Amendment XL's term "Qualifying Season" is more precise, but adopting it here would leave `regular_season_*` on every neighbouring column; renaming that family is a separate undertaking.
- **`seasons_regular_season_final_week_below_wildcard_round`** for the CHECK, following the existing `seasons_bye_count_within_playoff_field` and `seasons_head_to_head_berth_count_within_field` pattern of naming the relationship rather than the columns alone.

### Knowledge Base Impact

- No new entity. The boundary's meaning is already carried by Amendment XL and by the forecast task's modelling-contract document.
- Repo docs affected: `docs/guides/schema.md` (new column), `docs/guides/scripts.md` (if the generator's role changes).

## Notes

- DDL, the schema export, and the code reading the new column ship in ONE commit.
- Use `yarn test:isolated`, never `yarn test:db:up && yarn test`.
- The seed fixtures must carry the new column or every spec going through a real `getLeague` gets a null boundary. The `getLeague`-backed cases in `test/leagues.head-to-head-berth-count.spec.mjs` are the shape to check against.

## Plan

### Core Implementation

- [ ] **Add the column with a correct backfill and a real CHECK**
  - Files: `db/adhoc/2026-08-30-add-seasons-regular-season-final-week.sql` (new), `db/schema.postgres.sql`
  - Purpose: complete the per-league calendar on the season row.
  - Changes: add the column, backfill, then set `NOT NULL DEFAULT 14` — in that order, since a NOT NULL add on a populated table needs the values first. Backfill as `wildcard_round - 1` where `wildcard_round is not null`, and 14 where it is null. Add CHECK `seasons_regular_season_final_week_valid`: `regular_season_final_week >= 1 AND (wildcard_round IS NULL OR regular_season_final_week < wildcard_round)`. Then `yarn export:schema`.
  - Note: a naive backfill of the constant 14 aborts the `ADD CONSTRAINT`, because `lid=1, season_year=2020` has `wildcard_round=14`. That row's true boundary is 13, since its `championship_round` is `[15,16]` — so the constant is also the wrong VALUE for it, not merely a constraint violation.
  - Note for the DDL comment: the 115 rows with a null `wildcard_round` are all `is_hosted = false` imported leagues from 2023 with no matchups and no seasonlogs. Nothing in the database supports 14 for any of them and imported leagues commonly end at 13. State in the migration comment that non-hosted rows carry the DEFAULT rather than a measured value, so a later reader does not mistake the backfill for history.
  - Verify: re-run the population query from Assumptions BEFORE applying, since the counts may have moved; `yarn db:exec` succeeds; `select count(*) from seasons where regular_season_final_week is null` returns 0; `select count(*) from seasons where wildcard_round is not null and regular_season_final_week >= wildcard_round` returns 0. Then, inside a transaction, confirm the CHECK rejects `regular_season_final_week = wildcard_round` AND rejects `regular_season_final_week = 0`, and ROLLBACK. Finally confirm `max(week)` in `matchups` per league-season equals the backfilled value for all 7 hosted rows — measured 2026-08-30 as 13 for 2020 and 14 for 2021 through 2026, matching `wildcard_round - 1` exactly. DDL, schema export and dependent code ship in ONE commit.

- [ ] **Source the boundary from the requested year's season row in the forecast**
  - Files: `libs-server/simulation/simulation-helpers.mjs`, `libs-server/simulation/simulate-season-forecast.mjs`
  - Purpose: the forecast computes a specific `year`, so it must read that year's boundary, not the league join's. `load_simulation_context` already documents this exact hazard for the playoff format and solves it by reading the season row.
  - Changes: return `regular_season_final_week` from `load_simulation_context` beside `playoff_format` and `has_median_match`, sourced from the `season` row it already queries; in `simulate-season-forecast.mjs`, move the read at `:528-529` to below the `load_context` call and take it from that return. No fallback — `load_simulation_context` already throws `Season not found` when the row is absent.
  - Verify: a forecast for a past year whose season row carries a boundary different from the current constant uses the row's value; confirm the pre-change code uses the constant for that same call. `yarn test:isolated test/scripts.process-projections-forecast-propagation.spec.mjs` still green, since that spec deletes the season row and relies on the existing throw.

- [ ] **Read the boundary from the season row in process-matchups and process-projections**
  - Files: `scripts/process-matchups.mjs`, `scripts/process-projections.mjs`
  - Purpose: both read the year's row through `getLeague({ lid, year })`. `process-projections.mjs` already reads `league.wildcard_round`, which is the idiom to copy.
  - Changes:
    - `resolve_regular_season_weeks` in `process-matchups.mjs` builds the regular-season `weeks` list. Since league `f592b3e15` it already ends a mirror's season at `wildcard_round - 1` and keeps the constant for hosted leagues. Converting it means reading `league.regular_season_final_week` for both, plus `league.regular_season_first_week` if the mirror first-week column above is adopted. This one list feeds both the standings kernel and the lineup analytics; `calculate-standings.mjs` reads no boundary.
    - `process-projections.mjs:121` reads `league.regular_season_final_week`.
    - No `??` fallback: the column is NOT NULL, so a null is a constraint violation that should surface.
  - Note: a null boundary in `process-projections.mjs:121` falls through to "No forecast to run" and returns quietly instead of throwing, which is why the NOT NULL column matters more than a read-site guard.
  - Verify:
    - `yarn test:isolated` green, including `test/scripts.process-projections-forecast-propagation.spec.mjs` and `test/scripts.process-matchups-vendor-scored-standings.spec.mjs`.
    - A fixture mirror season whose boundary differs from 14 counts exactly its own weeks, and goes RED against the constant.

- [ ] **Handle the multi-league orchestrator on its own terms**
  - File: `scripts/update-stats-and-process-matchups.mjs`
  - Purpose: the week validation at `:157-160` runs before any league loads, inside a loop over all hosted leagues, so there is no single boundary to read.
  - Changes: keep the constant for the pre-loop argument validation, and if a per-league bound is wanted, apply it inside the per-league loop where the league is in scope. Do not convert the pre-loop check.
  - Verify: state in the commit which of the two shapes was chosen and why; `yarn test:isolated` green.

- [ ] **Thread the boundary into the schedule generator**
  - Files: `libs-server/generate-fantasy-league-schedule.mjs`, `libs-server/generate-schedule.mjs`
  - Purpose: `generate_fantasy_league_schedule(teams)` has no league and no lid, so it cannot read config without a signature change.
  - Changes: add a `num_weeks` parameter (or a second argument carrying the boundary) rather than reaching for a global inside a pure function; load the league in `generate-schedule.mjs`, which already holds `lid`, and pass it down.
  - Verify: `yarn test:isolated test/generate-fantasy-league-schedule.spec.mjs` AND `yarn test:isolated test/playoff-format.spec.mjs`, both green. The second is not optional — it calls `generate_fantasy_league_schedule` several times and reads the constant at `:957`, so a required new parameter breaks every call. Enumerate the call sites with `grep -rn "generate_fantasy_league_schedule(" --include=*.mjs . | grep -v node_modules` (the trailing paren excludes the import, export and definition lines, which a bare symbol grep would wrongly include in the count).

- [ ] **Correct the stale comment references**
  - Files: `jobs/finalize-week.mjs`, `libs-server/external-fantasy-leagues/sync/matchup-sync.mjs`
  - Purpose: `finalize-week.mjs:285,304` describe `process_matchups` capping at the boundary, which becomes per-league. `matchup-sync.mjs:15` says `wildcard_round` is NULL on every mirror, which config sync has since made false.
  - Changes: reword all three comments. There is no code read in either file.
  - Verify: `(no verify — mechanical step)`

### Configuration & Setup

- [ ] **Accept and validate the new field over the API**
  - Files: `api/routes/leagues/league-settings.mjs`, `api/routes/leagues/index.mjs`
  - Purpose: the field must be settable where the other format fields are.
  - Changes: add to both allowlists at `league-settings.mjs:239-279` (which spans `season_fields` and `integer_fields`); add cross-field validation beside the `bye_count` / `head_to_head_berth_count` block at `index.mjs:230-297`, rejecting a value at or above a non-null `wildcard_round` and rejecting anything below 1.
  - RESOLVED 2026-08-30 (operator): accept the field with structural validation only — no constitutional ceiling. The constitution fixes the Qualifying Season at fourteen weeks (`constitution.md:730`), but that fixity is load-bearing for exactly one rule, confirming a Provisional Manager after "at least seven (7) weeks of the Qualifying Season" (`constitution.md:552`), and **nothing in the codebase implements it**: a repo-wide grep for `provisional` returns only unrelated NFL player-id and Sportradar matches. Provisional-manager confirmation is an off-system commissioner determination, so no code path can be corrupted by a non-14 boundary. Do not add a constitutional guard for a rule the code does not carry.
  - Verify: new spec cases following `test/leagues.head-to-head-berth-count.spec.mjs` — a valid PUT persists; a value at or above `wildcard_round` returns an error naming the field; a value of 0 is rejected; a value set while `wildcard_round` is null is accepted. Confirm each case goes RED before the validation is added.

- [ ] **Carry the field to the frontend**
  - Files: `app/core/leagues/league.js`, `app/core/selectors.js`, `app/views/components/dashboard-team-summary/dashboard-team-summary.js`, `app/views/pages/standings/standings.js`, `app/views/pages/league-home/league-home.js`
  - Purpose: all three frontend readers sit inside `/leagues/:lid/`.
  - Changes: add the field beside `wildcard_round` on the League record at `league.js:134-135`; supply it via a selector; replace the three `current_season.fantasy_regular_season_final_week` reads with the league value.
  - Note: `app/core/seasons/season.js:4-5` carries `wildcard_round` / `championship_round` on a per-(lid, year) Season record as well. Both consumers here are current-year-only, so the League record suffices — decide deliberately whether the Season record should also carry it rather than defaulting.
  - Verify: `yarn dev:live` per `[[███████████████████████████████-█████████]]`, confirming the odds panels appear and disappear at the configured boundary rather than at the constant. Change the season row's value and confirm the boundary moves.

- [ ] **Supply the column when a league is created**
  - File: `libs-server/create-league.mjs`
  - Purpose: this is the single place `seasons` rows are born, at `:55`. No file under `db/fixtures/` supplies a season row — `db/fixtures/test/` holds only `player.mjs` and `player.sql`, and `db/fixtures/league.mjs:80` reaches the season row by calling `createLeague`, the production path. Leave this unpatched and every newly created league gets the column DEFAULT rather than a deliberate value, in tests and in production alike.
  - Changes: add `regular_season_final_week` to the `seasons` insert, sourced from the league parameters with the constant as the parameter default.
  - Verify: create a league through the normal path and assert its `seasons` row carries the boundary; `yarn test:isolated` green, especially `test/leagues.head-to-head-berth-count.spec.mjs`, which reaches the season row through `db/fixtures/league.mjs` and `createLeague`.

### Testing & Validation

- [ ] **Repoint the three test readers**
  - Files: `test/generate-fantasy-league-schedule.spec.mjs`, `test/playoff-format.spec.mjs`, `test/simulation.season-forecast-played-weeks.spec.mjs`
  - Purpose: `generate-fantasy-league-schedule.spec.mjs:47,134`, `playoff-format.spec.mjs:957` and `simulation.season-forecast-played-weeks.spec.mjs:65` read the constant while the code under test will read league config, so they pass whether or not the migration works. `calculate-standings.spec.mjs` needs nothing: the kernel takes its weeks as input.
  - Changes: assert against the configured value, not the constant.
  - Verify: set a fixture boundary that differs from 14 and confirm each spec follows the configured value; confirm each assertion goes RED when the fixture value and the constant disagree and the code still reads the constant.

- [ ] **Boundary-is-configuration regression**
  - File: `test/playoff-format.spec.mjs` or a new spec
  - Purpose: the whole point of the migration is that two leagues can disagree. Nothing today proves that.
  - Changes: two league-seasons with different `regular_season_final_week`, asserting process-matchups standings and schedule length follow each league's own value.
  - Verify: confirm it goes RED against the pre-migration code, where both leagues would return the constant.

- [ ] **Full suite and gates**
  - Verify: `yarn test:isolated` green; `yarn lint`; `yarn check:types`; `npx prettier --check` on the touched files only, since `yarn prettier` rewrites the whole tree.

### Knowledge Base Updates

- [ ] **Reconcile repo documentation**
  - Files: `docs/guides/schema.md`, `docs/guides/scripts.md`
  - Purpose: a new column with a CHECK, and a changed relationship between the generator and the season row.
  - Verify: read each and edit only what the change actually makes stale; state in the commit which were checked and needed nothing.

- [ ] **Record the dual-source follow-up**
  - Purpose: the constant and the column coexist after this task, with nothing stopping a new consumer reaching for the global.
  - Changes: record an observation on this task naming `local/no-season-member-reconstruction` as the precedent for a lint that would forbid new `current_season.regular_season_final_week` reads outside the four global files.
  - Verify: `base entity get` shows the observation.
