---
title: Project individual defensive players
type: task
status: completed
description: >-
  Build weekly and season projections for DL, LB and DB players so a league that starts a defender
  can be priced. Needs projection-table columns, consensus, player pool, points generation and a
  simulation fit for the defender groups.
base_uri: user:task/league/project-individual-defensive-players.md
created_at: '2026-09-27T21:47:27.965Z'
entity_id: 89dd399c-1897-4909-a4bb-ed6916f3d104
finished_at: '2026-09-28T18:33:28.519Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[████████████████████████-██████████-█████████-██████████]]
started_at: '2026-09-28T17:10:13.250Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-28T18:33:28.519Z'
visibility_analyzed_at: '2026-11-05T23:49:50.827Z'
---

## Why

Operator ruling 2026-09-27: price an individual defensive player by building real projections, not by a zero rule. Mirror 123 cannot produce odds without it, and mirror 122 cannot be scored against its vendor without it. The roster half of [[████████████████████████-██████████-█████████-██████████]] seats the defender; this prices him.

## What is missing, measured 2026-09-27

Five layers, and the vendor is not the main blocker.

- **Projection tables.** `projections_index`, `season_projections_index`, `rest_of_season_projections` and both history tables carry no `individual_defensive_*` column. `libs-server/record-projection-history.mjs` has a hardcoded `STAT_COLUMNS`.
- **Consensus.** `process_average_projections` (`scripts/process-projections.mjs`) builds from `all_projected_fantasy_stats`, which deliberately excludes `individual_defensive_fantasy_stats`. `has_any_projected_value` in `libs-shared/weight-projections.mjs` would drop a defender-only row as an all-zero placeholder.
- **Player pool.** `libs-server/get-projections.mjs:41` and `get-season-projections.mjs:29` filter to `fantasy_positions`, so defenders never reach the consensus.
- **Points generation.** `scripts/calculate-points.mjs:138` filters to `fantasy_positions`, so no defender has a `scoring_format_player_gamelogs` row for actual scoring either. `fantasy_positions` must NOT simply grow — it is the hosted player universe; see the roster task.
- **Simulation.** `libs-shared/simulation/fit-player-distribution.mjs` throws `no outcome ratio quantiles for position` for DL/LB/DB, and `scripts/calculate-projection-outcome-ratio-quantiles.mjs` never fits them — its snap filter checks offense and special-teams snaps only. Correlation constants have no defender keys and fall back to independence.

`scoring_format_player_projection_points` would pick new columns up without change: `calculate-points.mjs:48` iterates the whole registry.

## Source candidates, not yet chosen

- **Sleeper.** The shortest path. `scripts/import-sleeper-adp-and-projections.mjs:150` requests no defender positions, and `format_projection` drops `idp_*`. The adapter's `idp_* -> individual_defensive_*` map already exists. It is season-only and runs June to August, so weekly needs a new endpoint.
- **ESPN.** Weekly in season, with individual stat ids pinned in `adapters/espn.mjs` (95, 99, 106, 107, 108, 109; no tackles-for-loss id). Those pins were measured on actual stat blocks, not projection blocks.
- **In-house model** from the `nfl_plays`-derived defender gamelogs. None exists.

Whether either vendor's projection payload actually carries defender values is unverified — measure it before choosing.

## Decisions, 2026-09-28

- **Source: Sleeper (rotowire), weekly and season.** Measured, see observations. ESPN is out.
- **Sleeper weekly is imported for DL, LB and DB only.** Adding offense would change every hosted league's consensus, which is a separate call.
- **Actual-points generation for defenders belongs here.** The simulation fit needs defender actual points, so it is on the path.
- **Two-way players are not changed by this.** Eligibility stays keyed by position group, so Travis Hunter is priced and seated as a WR. Seating him in `IDP_FLEX` stays recorded on the parent task.
- **Pool constant.** Filters move from `fantasy_positions` to `lineup_position_groups`. `fantasy_positions` does not grow.

## Plan

Code map with file:line is in this session's research; the load-bearing sites are named below. Repo paths are relative to `repository/active/league`.

- [x] **Schema, authored in a worktree.** Add the seven `individual_defensive_*` columns to `projections_index`, `projections_history`, `season_projections_index`, `season_projections_history` and `rest_of_season_projections`. Widen the `projection_outcome_ratio_quantiles_pos_vocabulary` CHECK to DL, LB and DB. All are nullable and outside every conflict key, so they are safe to apply before deploy.
- [x] **Stat lists.** Append the defender stats to `all_projected_fantasy_stats` (`libs-shared/constants/stats-constants.mjs`), not to `all_fantasy_stats`. Derive `STAT_COLUMNS` in `libs-server/record-projection-history.mjs` rather than extending the hand list. Check the knock-ons: `resample-production-residuals.mjs`, `reconstruct-projection-consensus.mjs`, `load-weekly-realized-panel.mjs`.
- [x] **Importer.** Give `get_sleeper_projections` a `week` and defender positions, and map the seven `idp_*` keys from the adapter's existing map (never `idp_tkl`). Write weekly rows through `save_projections` under `SLEEPER`, plus a season run. Schedule it on `league` in season.
- [x] **Pool and points.** Move `get-projections.mjs:41`, `get-season-projections.mjs:29` and `calculate-points.mjs:138,195,217` to `lineup_position_groups`. Projected points per format need no change.
- [x] **History.** Backfill defender actuals for 2022-2025 with `generate-individual-defensive-gamelogs.mjs`, backfill Sleeper weekly defender projections for the same seasons, and run consensus and points over them.
- [x] **Simulation fit.** Add DL, LB and DB to `PROJECTION_BUCKET_COUNT_BY_POSITION`, and add defensive snaps to the took-the-field test in `calculate-projection-outcome-ratio-quantiles.mjs`. Fix its every-position rule so a format that does not score defenders keeps its offensive fit instead of being refused whole. Correlations stay independent for defenders.
- [x] **Pricing gate, deliberately.** Make `lineup_position_groups` the scoreable set in `libs-server/league-starter-scoreability.mjs`, and rewrite its docstring and message.
- [x] **Verify.** Mirror 123 produces championship odds summing to 1.0000. Mirror 122 stops being refused at pricing and is refused at playoff format instead. Mirror 121 and both hosted leagues stay byte-identical.

### Assumptions

- Sleeper's historical weekly projections were published before each game, not revised after it. Verify on one 2025 week before fitting on them: projections that track actuals too closely would make the simulation overconfident.
- `nfl_plays` participant slots are populated for 2022-2025 the way they are for 2026. Verify with a dry run of the gamelog generator per season before backfilling.
- One source means the defender consensus is Sleeper's number. The consensus path must accept a single-source row without special-casing it.
