---
title: Decide whether to reconstruct the lost week-1 2026 lineup starter rows
type: task
status: completed
description: >-
  Week 1 of 2026 lineup starters was reconstructed and the starter-identity oracle is green.
  project_lineups_for_past_season now writes starters and accepts a bounded current-season repair;
  contribution weeks remain unrepaired under a separate task.
base_uri: user:task/league/decide-week-1-2026-lineup-starter-reconstruction.md
created_at: '2026-09-15T06:41:44.909Z'
entity_id: da5d9cba-9596-4238-93ef-c3df94498bc5
finished_at: '2026-09-15T16:36:38.778Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
public_read: true
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-15T16:36:38.778Z'
visibility_analyzed_at: '2026-11-05T23:49:06.652Z'
---

The leak is fixed and committed (league `043f2b1f3`), and the rows it destroyed have now been reconstructed (league `7b86cb3cc`). Week 1 of 2026 is whole again and the starter-identity oracle is green — see Outcome. The sections below are kept in the order they were learned, because the premise this task opened on turned out to be wrong and the correction is the useful part.

## What happened

`scripts/project-lineups.mjs` deleted `league_team_lineup_starters` and `league_team_lineup_contribution_weeks` season-wide before each insert. Both are week-grain, and `getPlayers` filters to `week >= fantasy_active_week`, so an in-season run only ever produces the current week forward — the delete removed weeks 1..N-1 and the insert put back only N..18.

Nothing reported it. Each hourly run logged a full set of saved rows while the season's history drained behind it. By week 2 of 2026 every week-1 starter row was gone: 20 team-weeks across lids 1 and 119, 10 teams each. `███████████████████` was untouched, because it never had the delete, so weeks 1-17 of lineups survive with `optimal_total > 0` and no starters beneath them. That mismatch is what `check_lineup_starter_identity_oracle` in `scripts/process-projections.mjs` fires on, and it first fired 2026-09-15 05:30Z.

The fix scopes both deletes to the weeks the run actually writes. Verified against production in both directions: a seeded week-1 row survives the patched run and is deleted by a baseline copy carrying the season-wide form.

## Why no existing path could regenerate it — SUPERSEDED, it was reconstructed

Both supported paths refused it as originally written:

- The live `project_lineups` path cannot write a week below `fantasy_active_week`, which is now 2.
- `project_lineups_for_past_season` throws on `year >= current_season.year` by design, so it would not touch 2026.

That read as permanent, and it was wrong twice over. The year guard is not the whole story — `project_lineups_for_past_season` never wrote the lost tables at all, which is what made the original option 1 unworkable. And the guard itself was a design choice rather than a law: a bounded repair allowance for weeks below `fantasy_active_week` gives a lost historical week a writer without creating a second writer for live weeks.

Both are addressed in league `7b86cb3cc`. See Findings for the measurement and Outcome for what was written.

## Findings (2026-09-15, measured)

Steps 1 and 2 are done, and one of them overturned the premise under option 1.

**The inputs survive, so reconstruction is open.** `rosters_players` for 2026 week 1 holds 339 rows for lid 1 and 293 for lid 119; `projections_index` holds 5,053 rows for 2026 week 1.

**The blast radius is the entire league population, which is two leagues.** Exactly 2 leagues exist, both hosted and unarchived, and both carry the hole — `███████████████████` starts at week 1 with 170 rows each while `league_team_lineup_starters` starts at week 2. There is no third league to check.

**The leak fix is holding.** Weeks 2 through 17 sit at exactly 174 starters and 632 contribution-week rows each, with no further erosion since `043f2b1f3`.

**`project_lineups_for_past_season` does not write starters at all.** This kills option 1 as originally written. That function writes only `███████████████████` — the one table that never lost anything, and whose week 1 is already intact. Relaxing its year guard would rewrite healthy data and leave the actual gap untouched. The function's own doc comment says it reconstructs `███████████████████`, and its only write is a single `onConflict.merge()` into that table.

**But the starter identities are recoverable, and the function already computes them.** It calls `optimizeLineup` per team per week and keeps only `.total`, discarding `lineup.starter_pids` — the exact field the live path at `scripts/project-lineups.mjs:91` uses to build starter rows.

A read-only probe mirroring that logic for 2026 rebuilt the lost week:

| league | week | rebuilt | stored | match |
| ------ | ---- | ------- | ------ | ----- |
| lid 1 | 1 | 90 | 0 (lost) | — |
| lid 1 | 2 | 90 | 90 | 88 (97.8%) |
| lid 119 | 1 | 84 | 0 (lost) | — |
| lid 119 | 2 | 84 | 84 | 84 (100%) |

Week 2 is the control: reconstruction reproduces a week we still have, so the week-1 output is trustworthy rather than merely plausible. The rebuilt week-1 total of 174 matches the 174 every intact week carries.

**Two caveats, both honest limits rather than blockers.** Reconstruction is near-exact, not byte-identical — 2 of 90 lid-1 week-2 starters differ, most likely an optimizer tie or a roster snapshot taken at a different moment than the original run. And the probe covered `league_team_lineup_starters` only; `league_team_lineup_contribution_weeks` lost week 1 too (632 rows per week) and is built from a different loop that needs `getRoster`, so its recoverability is unmeasured.

## Outcome: reconstructed, 2026-09-15 16:34Z

**Week 1 is restored and the starter-identity oracle is green.** 90 starter rows for lid 1 and 84 for lid 119 — 174 total, matching every intact week of the season.

Landed in league `7b86cb3cc`. `project_lineups_for_past_season` now keeps `lineup.starter_pids` and writes `league_team_lineup_starters` with a week-scoped delete, and a `repair_current_season` option reaches the current season for weeks strictly below `fantasy_active_week`. Three guards were verified to refuse before anything was written: the current year without `--repair`, `--repair` without an explicit week list, and `--repair` aimed at a live week.

**The strongest evidence is incidental.** The repair recomputed `███████████████████` for week 1 as a side effect, and all 20 pre-existing rows came back **byte-identical** — same `optimal_total`, same `baseline_total`. The reconstruction independently derived the values the original live run wrote in week 1, which means it read the same roster snapshot and the same projections. The starter identities fall out of that same optimization, so they are trustworthy rather than merely plausible.

The oracle was not touched. It went green on the data being repaired, which is the outcome the "do not silently rescope" warning existed to protect.

**Still outstanding:** `league_team_lineup_contribution_weeks` lost week 1 as well and is NOT repaired. It is built from a per-player contribution loop rather than from the optimizer result, so it needs separate work. The starter-identity oracle does not read it, and nothing currently reports its gap.

**One incident worth recording.** While testing the guards, `--year 2026 --dry` fell through to the live path — which ignores `--dry` — and ran a real production write. No damage: week 1 was untouched, weeks 2-17 held at 174 starters each, and the week-scoped delete from `043f2b1f3` demonstrably held under a real run. `league_team_lineup_contribution_weeks` moved 632 to 631 per week, which is the live path recomputing off the current roster exactly as the hourly cron does. The same commit now refuses `--year <current>` without `--repair`.

## Plan

1. ~~Check the inputs still exist.~~ Done — both present.
2. ~~Confirm the blast radius.~~ Done — lids 1 and 119, the whole population.
3. ~~Take the operator decision among the three options.~~ **Reconstruct**, chosen on the operator's instruction to proceed. It was the only option that restores the data rather than managing its absence, and the write was purely additive — week 1 held zero starter rows, so there was nothing to destroy.
4. ~~Apply the chosen option and record the outcome.~~ Done — see above.
5. ~~Decide whether `league_team_lineup_contribution_weeks` week 1 is worth reconstructing too.~~ Moved to its own task: [[user:task/league/reconstruct-week-1-2026-lineup-contribution-weeks.md]]. Nothing reports it and no oracle reads it, so it is invisible rather than urgent.

### The three options, for the record

- **Reconstruct week 1.** CHOSEN. Measured viable beforehand, confirmed exact afterwards.
- **Accept the gap and scope the oracle** to weeks at or after the earliest week the writer can produce. Rejected — it stops catching a genuine future week-1 break.
- **Accept the gap and leave the oracle red.** Rejected — a permanently red check trains everyone to ignore it.
