---
title: Fix the calculate_week Postseason Off-By-One
type: task
status: completed
description: >-
  RESOLVED: the postseason arithmetic was already corrected by the nfl_postseason_week conditional
  clamp, re-measured against 1140 stored 2022-2025 games with a firing control. What was genuinely
  missing was the spec, which asserted only the Super Bowl -- the one round the regression did not
  move. All four rounds are now pinned against the observed playoff calendar.
base_uri: user:task/league/fix-calculate-week-postseason-off-by-one.md
created_at: '2026-08-23T18:49:01.527Z'
entity_id: bddbee43-99e7-4927-bfea-203b55b4b45c
finished_at: '2026-09-12T22:52:40.069Z'
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - follows [[██████████████████████-███-████-████-████████-███████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-12T22:53:22.237Z'
visibility_analyzed_at: '2026-11-05T23:49:19.515Z'
---

# Fix the calculate_week Postseason Off-By-One

## Resolution (2026-09-12)

**The arithmetic defect no longer exists.** It was fixed ahead of this task by the rename that produced `nfl_postseason_week`, which took a CONDITIONAL clamp rather than the unconditional subtraction this task describes:

```js
if (round <= 3) return round
return Math.min(round - this.nfl_super_bowl_bye_weeks, 4)
```

`calculate_nfl_week` delegates to that method, so both branches the plan named were corrected together. The three test files were reconciled at the same time — `test/fixtures/postseason.mjs` now encodes the playoff calendar as the literals `{1: 1, 2: 2, 3: 3, 4: 5}` weekends past the regular season, specifically so a fixture cannot pin a broken getter.

**Re-measured, not read.** Replayed `calculate_nfl_week` against all 1140 stored 2022-2025 REG and POST games:

| Form | Agree | Disagree | POST disagreements |
| --- | --- | --- | --- |
| Current code | 1140 | 0 | 0 |
| Regressed control | 1092 | 48 | 48 of 52 |

The control fires, so the green is not vacuous. The 4 POST rows the regression does NOT move are the Super Bowls — the cap absorbs them, which is exactly why the one round the suite asserted was the one round that stayed correct.

**What was actually missing was the gate**, the plan's fourth item. Closed by league `a1177d231`, which asserts all four rounds against the observed playoff calendar rather than against the algebraic inverse of the method under test. Verified to fail on the regressed form and on nothing else in the file.

`nfl_super_bowl_bye_weeks` survives, used by the clamp's Super Bowl branch — so the plan's question about deleting it resolves as "keep".

No deploy was required: nothing shipped but a spec.

## Background

`libs-shared/season.mjs`'s `calculate_week` resolves a date to `(seas_type, week)`. Its POST branch reads:

```js
week_number = Math.min(diff - this.nflFinalWeek - this.superBowlByeWeeks, 4)
```

The `- this.superBowlByeWeeks` term was added by league `6d2bf23e5` (2026-02-08), titled "correct postseason week calculation to account for Pro Bowl bye week". It is a regression.

The `Math.min(diff - nflFinalWeek, 4)` form it replaced was already correct for all four rounds. Continuous offsets run wildcard 19, divisional 20, championship 21, Pro Bowl bye 22, Super Bowl 23; subtracting only `nflFinalWeek` gives 1, 2, 3, and 5 — and the existing cap turns that 5 into 4. The cap absorbed the bye. Subtracting the bye as well shifts every round down by one: wildcard resolves to POST week 0.

The commit was almost certainly motivated by `nfl_season_week`, whose POST branch has no cap and so read 5 for the Super Bowl. That getter was changed in the same commit and needs the same care — a cap rather than an unconditional subtraction.

## Evidence

Replayed `calculate_week` against every stored 2026 and 2025 game in `nfl_games`:

- All 12 stored 2025 POST games disagree with their stored `(season_type, week)`.
- All 322 PRE and REG games of the same season agree, so the disagreement is specific to the POST branch and the replay is not vacuous.

2025's postseason prop markets linked correctly at the time because they were imported BEFORE `6d2bf23e5` landed. The defect has never yet run against a live postseason.

## Why it matters

Every odds importer resolves `esbid` through `find_nfl_game`, which looks up `nfl_games` by `(week, seas_type, teams)`. A shifted week misses, the importer writes `esbid: null`, and the market becomes invisible to every consumer that joins on `esbid` — with no error anywhere.

This is the exact failure mode that unlinked all 4,632 2026 rows in `prop_markets_index` via a mis-set `regular_season_start`, fixed in `a64e388e5`. That one was caught only because someone went looking. This one fires in January 2027 and would silently suppress every playoff market.

## The complication

Three test files encode POST week 1 at continuous offset `nflFinalWeek + superBowlByeWeeks + 1` rather than `nflFinalWeek + 1`, so they agree with the regression and will fail against the fix:

- `test/fixtures/postseason.mjs`
- `test/resolve-single-nfl-week-id.spec.mjs`
- `test/nfl-week-offset-params.spec.mjs`

Reconciling them is the bulk of the work, and it has to be decided rather than mechanically flipped — `test/fixtures/postseason.mjs` drives `run_under_season_type('POST', ...)`, which the week-encoding guideline requires of every week-dependent code path, so its offset convention reaches well beyond these three files.

## Plan

- [x] **Establish the ground truth for POST continuous offsets** — confirmed across 2023-2025 stored `nfl_games`: rounds 1-4 hold 6, 4, 2 and 1 games. `nfl_super_bowl_bye_weeks` is kept.
  - Confirm against stored `nfl_games` rows which continuous offset each postseason round occupies, for at least two seasons.
  - Decide whether `superBowlByeWeeks` should survive at all, or whether the cap alone expresses the rule. If it does not survive, remove it from `libs-shared/season-dates.mjs` and the `Season` constructor rather than leaving an unused field.

- [x] **Fix both branches** — already done ahead of this task by the `nfl_postseason_week` conditional clamp, which `calculate_nfl_week` delegates to.
  - `calculate_week`'s POST branch and the `nfl_season_week` getter, which `6d2bf23e5` changed together and which must stay consistent.
  - The Super Bowl must still resolve to 4, which is what motivated the original commit.

- [x] **Reconcile the three test files** — already done; the fixture now carries observed-calendar literals.
  - Decide the offset convention once, then apply it. Check every consumer of `test/fixtures/postseason.mjs` before changing its shape.

- [x] **Add a gate that fails on the regression** — league `a1177d231`. This was the only item still genuinely open.
  - Assert each postseason round's resolved week against an independently-derived expectation, not against another expression of the same constant. Verify it fails on the current code before trusting it.

- [x] **Full suite, lint, prettier** — `yarn test:isolated` 8581 passing, eslint and prettier clean.
  - `yarn test:local` — never `yarn test:db:up && yarn test`.

- [x] **Deploy** — not required; nothing shipped but a spec.
  - Human-gated. Must land before the 2027 postseason; there is no urgency beyond that.
