---
title: Stop the stale-anchor rollover from scrubbing offseason roster moves
type: task
status: blocked
description: >-
  The year-rollover branch of generate-rosters targets week 0, which is the LIVE offseason roster,
  so its extra-row delete silently removes offseason roster moves. Reachable only while
  season-dates.mjs is un-rolled, which is why it has never bitten.
base_uri: user:task/league/stop-stale-anchor-rollover-scrubbing-offseason-moves.md
created_at: '2026-09-07T03:49:39.398Z'
entity_id: a1ad4d49-9226-48a4-8647-5c7f9d5129a1
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: High
public_read: true
relations:
  - follows [[████████████████████████-█████████-██████-████-████████]]
  - relates [[█████████████████████████████-████-███████████]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-10T20:59:18.215Z'
visibility_analyzed_at: '2026-11-05T23:50:31.157Z'
---

## Why this is Blocked

The fix changes which players survive a year rollover — offseason roster semantics — and
the auction is mid-window. That is an operator call, not an implementation detail. It is
NOT blocked on discovery: the defect is measured and reproduced, and the shape of the fix
is known. See "The decision" below.

## Context

`libs-shared/season-dates.mjs` is hand-maintained. `scripts/generate-season-dates.mjs`
says so outright — "that file is maintained by hand-copying this output into it" — and no
cron performs the roll. Between the Super Bowl and whenever someone does it, the anchor
still belongs to the season that just finished.

In that window `Season.week` is UNCLAMPED above its anchor, so it keeps counting while
`fantasy_season_week` snaps to 0. Measured at the getters against the current
`season-dates.mjs`:

| instant    | `fantasy_season_week` (reads) | `current_season.week` (writes) |
| ---------- | ----------------------------- | ------------------------------ |
| 2026-09-06 | 0                             | 0                              |
| 2027-01-20 | 20                            | 20                             |
| 2027-02-16 | 0                             | 24                             |
| 2027-07-15 | 0                             | 45                             |

Rolling the dates closes the split by itself — measured against a constructed 2027
calendar, `week` clamps to 0 below the new anchor and the two agree at every instant. So
the 52 fantasy-domain call sites reading `current_season.week` are NOT the defect to
chase; the un-rolled anchor is.

## The defect, measured

**The invariant being broken is stated in the crontab itself**, beside the schedule:

> The job writes only the FUTURE slice (nextWeek = current week + 1), never the live one,
> so running it daily cannot clobber a manager's roster.

That is why the `extra_pids` delete is safe in normal operation: its target is a slice
nobody transacts on, rebuilt nightly from the live one, and the delete only makes the copy
match its source. There is no "reset" of a live roster anywhere in the design.

The rollover is the single exception, and it is an accident of reusing one code path.
`generate-rosters` keys that branch on `is_new_season = now > end`, and sets
`nextWeek = 0`. In every other case `nextWeek` is a future slice; here week 0 IS the live
offseason roster. So the same delete, aimed at the live slice, removes anything not in the
finished season's `final_week`.

An offseason acquisition is by definition not in that source, so it is deleted.
Reproduced against a fully populated 204-row source at `final_week`:

```
PROBE source rows seeded at final_week: 204
PROBE before rerun: [ ...203 players at week 0..., {"pid":"AUST-REED-000056","week":26,"rid":217} ]
PROBE after  rerun: [ ...203 players at week 0... ]
PROBE error: undefined
```

The acquisition is gone. **No error, no signal, no slice failure.** The orphan gate
(`check_orphan_slice`) cannot catch it, because the deletion happens before the check
runs, so the gate finds a clean slice and reports success.

The probe was reverted rather than committed — a passing test asserting this behaviour
would enshrine the data loss as expected.

## The decision

On the first rollover run the target slice is empty, so `extra_pids` is empty. On every
LATER run, an extra pid on the target can only be a roster move written since the last
run. That makes the oracle precise and non-noisy, and it gives three options:

1. **Refuse.** Treat any `extra_pids` deletion during `is_new_season` as a slice failure.
   Loud, minimal, no semantic change — but it fails the job nightly until the roll happens.
2. **Stop re-running.** Perform the year rollover once and leave week 0 alone afterwards,
   matching what the post-roll offseason already does. Closes the loss; needs a marker for
   "rollover already done for year N".
3. **Roll the dates on time and guard the lag.** Treat the stale anchor as the bug and add
   a check that reports when the clock is past `end` and the schedule for the new season is
   already importable.

My recommendation is 2 plus 3 — 2 removes the loss, 3 removes the window. 1 alone converts
silent data loss into a nightly red job, which is better but still leaves the roll undone.

**Option 2 is also the one that restores the stated invariant** rather than adding a guard
beside a violation of it: the crontab says the job never writes the live slice, and after
the one-time rollover it would not.

## Do not

- Do not conform the 52 `current_season.week` call sites. Measured: they agree with
  `fantasy_season_week` at every instant of a correctly-rolled calendar, so the change is a
  no-op everywhere except the window this task closes at its root.

## Plan

- [ ] Get the operator's call between options 1, 2 and 3 above.
- [ ] Implement it, with the control shown as a pair — the injected offseason acquisition
      must survive a rerun after the fix and be shown to disappear before it.
- [ ] Add the offseason case to `test/scripts.generate.roster.spec.mjs`, which currently
      has no coverage of the stale-anchor window at all.
