---
title: Fix the missing projected_points_total alias that wipes a year of league-format projections
type: task
status: completed
description: >-
  process-projections-for-league-format.mjs did not alias projected_points_total as total, so every
  player took the -999 sentinel on a writer that deletes by (league_format_id, year) before
  inserting. Fixed, with an output oracle that aborts before the destructive write and a spec proven
  red at the pre-fix revision. Blast radius investigated: no year was ever wiped. Unblocks the
  projection rest-of-season backfill.
base_uri: user:task/league/fix-projection-league-format-total-alias.md
created_at: '2026-08-05T19:23:55.392Z'
entity_id: b98c5e81-bab2-43fb-8870-e13aa33a3bb0
finished_at: '2026-08-05T20:08:48.151Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: High
public_read: true
relations:
  - blocks [[███████████████████████████-████-██-██████-███████████]]
started_at: '2026-08-05T19:24:56.380Z'
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-08-05T20:47:27.535Z'
visibility_analyzed_at: '2026-11-05T23:49:27.722Z'
---

## Problem

`scripts/process-projections-for-league-format.mjs:158-166` selects from `scoring_format_player_projection_points` without aliasing `projected_points_total as total`, while `get_player_week_total` reads `.total`. Every player therefore takes the `-999` sentinel.

The writer deletes by `(league_format_id, year)` before inserting, so a run wipes a year of values and exits 0. The failure is silent — no output oracle distinguishes a wiped year from a successful run.

Introduced by league `72346e579` (shorthand schema conformance). The hourly cron is reported safe; the danger is the manual/backfill invocation path.

Blocks [[███████████████████████████-████-██-██████-███████████]] — its plan invokes this script for the historical backfill (task body lines 305 and 510).

## Plan

- [x] Fix the alias at `scripts/process-projections-for-league-format.mjs:158-166` so `projected_points_total` reaches `get_player_week_total` as `.total`. Done in worktree `/Users/trashman/user-base/repository/active/league-worktrees/fix-projection-league-format-total-alias` (branch `fix/projection-league-format-total-alias`): the query now carries `.select('pid', 'week', 'projected_points_total as total')`, matching `libs-server/get-players.mjs:368`. Confirmed `.total` is the only key any consumer reads off `points[week]` (`libs-shared/calculate-values.mjs:37`, `libs-shared/calculate-projection-values.mjs:118,122`).
- [x] Establish blast radius: **nothing was wiped.** Four pairs are fully sentinel — `b8586bea-56a5-4be9-9d9b-b1fc576a1d95` for 2021, 2022, 2023, 2024 — and all four are the legitimate shape, not damage: their scoring format (`2ad04012-0abc-465e-81ce-72cc6710f211`) has zero rows in `scoring_format_player_projection_points` for those years, so a correct run produces the sentinel too. The decisive query is the one that joins values to source: no `(league_format_id, year)` pair anywhere is fully sentinel while holding source rows, which is the wipe signature. The ~48% sentinel rate on healthy pairs is the normal baseline (unprojected and non-fantasy-position players in the pool), not partial damage.
- [x] Repair any wiped years: **not applicable and not possible.** The precondition in this item — "after confirming the source projections are intact" — fails for the only fully-sentinel pairs. Re-running the corrected script against `b8586bea` 2021-2024 would recompute the same sentinel, because there is nothing to compute from. No repair was attempted and no production write was made.
- [x] Add an output oracle so a wipe cannot exit 0 again. Asserted in `process_league_format_year` **before** the delete-then-reinsert, so a bad run aborts with stored values intact. It fires when source point rows were read but every computed weekly value is the sentinel, and stays quiet when the year legitimately has no source rows. Two supporting fixes were required for it to work at all: `main()` ended in a bare `process.exit()` that reported 0 even after catching a throw (now carries the outcome), and the count must exclude the `ros`/`ros_net` aggregates, which skip the sentinel and land at 0 — counting them left the oracle silent on exactly the wipe it exists to catch.
- [x] Verify the hourly cron path is genuinely unaffected. Verified by mechanism, not assumption: the cron (`server/crontab-main/league-imports.cron:54`) runs `process-projections.mjs`, whose own `process_league_format` loads player rows through `getPlayers()` (`libs-server/get-players.mjs:367-368`) — the loader that already aliases `projected_points_total as total`. It never reaches the defective query. Corroborated in production: all 22 formats in `league_format_player_projection_values_history` (the hourly path's output, since 2026-07-29) sit at the healthy ~48% baseline.

### Coverage

`test/scripts.process-projections-for-league-format.spec.mjs` — 4 tests, executing the real write path against Postgres rather than inspecting the payload. Proven honest: **3 of 4 fail at the pre-fix revision** in a clean worktree at HEAD, and all 4 pass in the branch. The suite carries a negative control (points rows present but null totals) so "no throw" cannot be confused with "the check cannot fire".

### Verification

Re-run after rebasing onto master (14 sibling commits had landed; none touched these files, so the rebase was clean):

- Full suite: **3262 passing, 0 failing**.
- Gate 1 and gate 2 of `db/adhoc/check-renamed-column-consumers.mjs`: both OK, 0 unadjudicated.
- `eslint`, prettier, `check-test-spec-collection` (226 collected), and the schema-conformance ratchet: all clean.

Merged to master as `42699c774` (fast-forward, single commit) and pushed by `cli/sync/sync-all.sh`. Worktree and branch removed.

### Also fixed in the same file

The `all`-without-`year` branch of the exported `process_projections_for_league_format` queried `projections_index.distinct('year')`; that table has only `season_year`, so the branch would throw Postgres 42703. Unreachable from the CLI (`main()` handles that case itself with the correct column) but live for any direct caller of the default export — which is the backfill entry point the blocked task uses.

## Out of scope

Sweeping league `72346e579` for other missing-alias sites of the same class. That belongs to the schema-redesign loop's shorthand cluster and is routed there separately.
