---
title: Surface projected points added net
type: task
status: completed
description: >-
  Expose the projected rest-of-season net value that the pipeline already computes and persists but
  surfaces nowhere, and correct two fields that label a signed value as floored. No DDL — storage
  layout and column naming belong to ██████████████████████████████████.
aliases:
  - ██████████████████████████-██████-█████-████████-███-███-█████████████
base_uri: user:task/league/surface-projected-points-added-net.md
created_at: '2026-08-03T18:12:18.065Z'
entity_id: b478f96b-6b66-4f8b-bba2-7dfafd419d6a
finished_at: '2026-08-05T01:04:00.077Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[user:task/github/mistakia/league/157-implement-wins-above-replacement.md]]
  - relates [[user:task/league/add-projected-lifetime-value-metric.md]]
  - follows [[████████████████████████████-██████-█████-███████████]]
  - relates [[███████████████████████████-████-██-██████-███████████]]
  - relates [[███████████████████████-█████-█████████-████████]]
  - relates [[████████████████████████████████-█████-████████████]]
  - relates [[user:task/league/expose-careerlog-net-first-n-season-fields.md]]
  - relates [[user:task/league/extend-distributional-draw-pass-beyond-week-zero.md]]
  - relates [[████████████████████-█████-███████████-████████-██████-████████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-26T05:00:41.704Z'
visibility_analyzed_at: '2026-11-06T00:18:07.345Z'
---

# Implementation Plan: Surface Projected Points Added Net

## Overview

Surface the projected rest-of-season net value that the pipeline already computes and persists but exposes nowhere, and correct the two places where a signed value is labeled as though it were floored.

Expected outcomes:

- Rest-of-season net reachable as a data-view field and visible on the four SPA surfaces that show points added today.
- The weekly projected field stops implying it is a floored quantity when it is not.
- The positive-versus-net naming argument contributed to the task that owns the storage, rather than executed as a competing rename.
- No DDL, no schema apply slot, no rename sweep.

**Impact.** A manager can already see what a player earned in his good weeks. He cannot see anywhere, on any forward-looking surface, what the bad weeks cost a lineup that started him — even though the number exists in the database. In a managed redraft league the top players start every week regardless of matchup, so that cost is real and currently invisible. This makes it filterable and sortable in data views and visible on the player and roster pages.

**Ordering.** This task carries no schema change, so it is independent of and unblocked by `██████████████████████████████████████████████████████`, which owns the storage layout for these values. Its data-view field repoints to whatever tables that cutover produces. It contributes to the WAR umbrella issue #157. It follows `█████████████████████████████████` (shipped 2026-05-16, realized side).

**This plan was rewritten on 2026-08-03 after a plan review.** The original scope — a rename cluster across six tables, a new season-grain net estimator, four rank columns, and a `week`-column widening — was withdrawn. Background records why, because the discarded reasoning is the load-bearing part.

## Background

### The two variants answer different questions

The floor in `E[max(X − b, 0)]` is an option's value — the value of benching a player who would score below replacement. Best ball exercises that option perfectly ex-post, so the floored form is correct there. A player who starts every week never exercises it, so the option is worth zero and the floored form overstates him by exactly that option value. Managed redraft sits between, closer to net the better the player.

Positive is therefore the perfect-optionality bound and net the no-optionality bound; they bracket reality rather than competing. The honest middle is start-probability weighting, which issue #157 already proposes and which is later, separate work.

### What already exists (verified 2026-08-03)

The weekly projected value is **already net**. `libs-shared/calculate-values.mjs:37-39` writes `player_week_points - baseline` with no floor, so `league_format_player_projection_values.pts_added` at weeks 1-18 holds signed values including negatives.

The rest-of-season net **already exists and is already persisted**. `libs-shared/calculate-player-values-rest-of-season.mjs:26` sums every remaining signed week (skipping only the `-999` "did not play" sentinel) and `scripts/process-projections.mjs:346` writes it as a row with `week = 'ros_net'`.

`libs-server/tag-board/build-tag-board.mjs` already consumes the negative range of the weekly value.

### What is actually missing

- No data-view field exposes `ros_net`. `libs-shared/data-view-fields-index.mjs:411` exposes only `player_rest_of_season_projected_points_added`, which reads the floored sum.
- No SPA surface renders it.
- `player_week_projected_points_added` (`data-view-fields-index.mjs:409`) reads the signed weekly column under a label implying a floored quantity.
- `app/views/components/selected-player-gamelogs/selected-player-gamelogs.js:13-14` fetches `points_added_net` and renders only `points_added_earned` at `:104-105`.

### Why the original scope was withdrawn

**A season-grain net does not measure weekly downside.** `libs-shared/calculate-distributional-baselines.mjs:206-213` draws one whole-season total per player. Removing the floor guard leaves `E[X − b] = E[X] − E[b]` by linearity of expectation, which within a position is the projection mean minus a shared constant — rank-identical to the projection itself, and unable to distinguish a steady at-replacement player from one alternating +40 and −20 weeks. That is precisely the case this work exists to penalize. The estimator was cut.

**The storage rename belongs to another task.** `██████████████████████████████████████████████████████` is In Progress and hoists `'0'` / `'ros'` / `'ros_net'` out of the `week` column into dedicated period tables, narrowing `week` to `smallint NOT NULL`. The original plan widened that same column to keep period strings in it — the reverse design. Its 2026-07-30 operator ruling also requires the sentinels to migrate rather than be deleted, because `libs-server/get-players.mjs` assigns by dynamic week key and `app/views/components/trade-player/trade-player.js` renders in-season, so a delete without a replacement payload key blanks a surface with no SQL error.

**The rename failed surgical scope.** Nothing in "surface a projected net value" requires the `earned` to `positive` sweep across gamelogs, seasonlogs and careerlogs, and that sweep is what dragged in the serialized production-DDL apply slot, the saved-view migration rule, the golden regeneration battery, and three-checkout deploy verification.

### The naming correction to contribute upstream

The redesign task's step 5 renames the weekly signed column to `pts_added_earned`. Its own line 357 justifies omitting a per-week net column on the grounds that "`pts_added_earned` and `pts_added_net` therefore carry identical values" — which is true only because the column is already net. It therefore plans to label a signed column `earned`.

The argument to contribute: `earned` correlates perfectly with "realized" while actually meaning "positive-only," so it carries two axes in one token; `positive` states the rule literally. `gross` is worse than both, because in ordinary usage `net` means "after filtering" while here net is the variant that includes everything, so `gross`/`net` gives the wrong reading internal consistency. `absolute` is wrong outright — it reads as `abs()`, which flips negatives rather than dropping them.

## Design

### Approach

Two data-view fields and four SPA surfaces. No schema change, no compute change, no new estimator.

The rest-of-season net is read from `league_format_player_projection_values` at `week = 'ros_net'` today. When the redesign cutover lands, the same field repoints to the rest-of-season period table's net column. Because the field is registered once and read through the column-definition layer, that repoint is a one-line change in a single file rather than a consumer sweep — which is why this task does not need to wait.

### Field naming

Two fields, following the existing projected-field grammar rather than inventing one:

- `player_rest_of_season_projected_points_added_net` — the signed cumulative remaining-season value.
- `player_week_projected_points_added` keeps its id and gains a description stating it is signed and can go negative. Renaming it would force a saved-view migration for no semantic gain, since the value it returns is already correct.

The `positive` and `net` tokens are not introduced into storage by this task. They are contributed to the redesign task, which owns the columns.

### Surfacing

Net appears beside the existing value on `selected-player`, `player-roster`, `player-roster-total`, and the team page, and `selected-player-gamelogs` renders the `points_added_net` it already fetches.

Pricing surfaces are untouched — tag board, auction nomination, RFA bid floors, trade analyzer, `market_salary`, `market_salary_adj` and `salary_adj_pts_added` all keep their current basis. `libs-shared/calculate-prices.mjs` is not modified at all, which is stronger than the original plan's unproven "no behavior change" assertion.

### Out of scope

- Any DDL. Storage layout and column naming belong to `██████████████████████████████████████████████████████`.
- A season-grain (week 0) net value — cut for the reason in Background.
- Net rank and position-rank columns. No consumer was named; data views sort on the value directly.
- Careerlog net first-N-season and best-season exposure. Real but unrelated parity work; its own task.
- A `points_added_shortfall` derived field. It means different things at realized and projected grain and had no consumer.
- Unified format-specific value and a net-based projected salary. The denominator collapses toward zero once deep-bench negatives enter `total_pts_added` (`libs-shared/calculate-distributional-baselines.mjs:243-250`), which needs its own design.

### Assumptions

- `week = 'ros_net'` rows are present for every league format in the current year. Verified 2026-08-03 in the redesign task's own audit: 23 of 23 format ids across `'0'`, `'ros'` and `'ros_net'`.
- Historical years are largely unpopulated for league-format ROS rows. The new field returns null for those years, acceptable for a forward-looking metric.
- Registering a data-view field needs no saved-view migration, because no existing saved view can reference an id that did not exist.
- The redesign cutover will preserve a rest-of-season net value under some column name. If that task is abandoned, this field keeps reading the sentinel row and nothing breaks.

### Knowledge Base Impact

Tag is the existing `user:tag/league-xo-football.md`; no new tag, no new domain entry, no `ABOUT.md` change.

- `user:text/league/player-valuation-system.md` — correct the projected-side description, which currently overstates what exists. Add the optionality doctrine: the floor as an option value, positive as the perfect-optionality bound, net as the no-optionality bound, and why a season-grain net cannot answer a weekly question.
- `███████████████████████████████████████████████████` — add the rule that pricing derives from the floored variant and that a net-based price requires a stated population restriction.
- `██████████████████████████████████████████████████████` — receives the naming correction as an observation, not a body edit.

`█████████████████████████████████████████████████████` is Completed and describes accurate history; left alone.

### Working Tier

None. The original scratch slug existed only to hold a rename inventory, and there is no rename.

## Notes

**The pre-deploy valuation gate applies.** `███████████████████████████████████████████████████` § "Validate a change before it reaches a league" requires three things before any valuation change deploys: a backtest across every available season on the point-in-time board, showing the resulting board to the operator, and stating which of the three goal questions it improves and which it leaves alone or degrades. This task adds a metric to a roster-decision surface, so the gate applies even though `market_salary` is untouched.

**Do not build the frontend from the shared checkout.** `yarn build` reads the working tree, not the pushed ref, and this checkout is shared with concurrent sessions. Build from a throwaway worktree pinned to the pushed ref.

**CI minutes.** `mistakia/league` runs CI against a limited monthly allowance and ████████ pushes on a roughly one-minute cycle, so every commit becomes its own billed run. Commit per coherent unit.

## Plan

### Contribute Upstream

- [x] **Record the naming correction on the redesign task** (done 2026-08-03)
  - Description: add an observation to `██████████████████████████████████████████████████████` stating that its planned `pts_added_earned` name lands on a column holding signed values, with the `positive`/`net` argument and the evidence at `libs-shared/calculate-values.mjs:37-39`.
  - Purpose: the storage naming decision belongs to the task that owns the columns; a competing rename would re-rename a just-renamed column inside a serialized DDL slot.
  - Verify: `base entity get` on that task shows the observation, and its line 357 rationale is addressed by name.

### Expose the Field

- [x] **Register `player_rest_of_season_projected_points_added_net`** (done 2026-08-04, `67278d518`)
  - Description: add the field to `libs-shared/data-view-fields-index.mjs` and its column definition to `libs-server/data-views-column-definitions/player-projected-column-definitions.mjs`, reading `week = 'ros_net'`.
  - Purpose: the value is persisted and reachable by nothing.
  - Verify: the field returns non-null values for the current year across multiple league formats, and a player with known negative weeks returns a value below his positive rest-of-season number.

- [x] **Correct the weekly field description** (done 2026-08-04, `67278d518`)
  - Description: update the `player_week_projected_points_added` description to state the value is signed and can be negative.
  - Purpose: the field reads an unfloored column under a label implying otherwise.
  - Verify: at least one player in the current week returns a negative value through the field.

- [x] **Run the data-view SQL validity gate** (done 2026-08-04, GATE OK from a clean worktree)
  - Description: `git worktree add` at HEAD, symlink `node_modules`, `yarn test:db:up`, then `node db/adhoc/check-data-view-sql-validity.mjs`.
  - Purpose: the gate reads the working tree and this checkout is shared, so a sibling's uncommitted edits are otherwise indistinguishable from your defects.
  - Verify: exit 0, and any finding names a table this task touches.

- [x] **Add a golden for the new field** (done 2026-08-04, red at `50f63cf5a`, green at `67278d518`)
  - Description: add a data-view query fixture exercising the new field.
  - Purpose: nothing currently pins its SQL.
  - Verify: confirm the new test fails at HEAD in a clean worktree before trusting it green. Check `skip_query_match` is not set, or the fixture asserts nothing.

- [x] **Key the projection-values table alias by period** (done 2026-08-04, `67278d518`)
  - Description: `league_format_player_projection_values_table_alias` keyed only on year/week/seas_type, so two period columns in one view hashed to one alias and shared a join carrying only the seeding column's week predicate. Now a factory taking `period_week`.
  - Purpose: not optional for this task — the new field would have landed in the same join group as `ros`. It was also a live defect on the shipped `week`/`rest_of_season` pair for both `pts_added` and `market_salary`.
  - Verify: measured at `50f63cf5a` — `[week, ros]` emitted `week = 'ros'` for both, `[ros, week]` emitted `week = '0'` for both. After the fix each period gets its own join and predicate, order-independent, across the plain, `year`, and `year+week` shapes.

### Surface It

- [x] **Render net beside the existing value on four SPA surfaces** (done 2026-08-04, `09a437d7d`)
  - Description: add it to `selected-player`, `player-roster`, `player-roster-total`, and the team page.
  - Purpose: net currently appears in one generic stat-row component and nowhere a roster decision is made.
  - Verify: load each surface against a real league and confirm a player with negative weeks shows a net value below his positive one.

- [x] **Render the net that `selected-player-gamelogs` already fetches** (done 2026-08-04, `09a437d7d`)
  - Description: `selected-player-gamelogs.js:13-14` passes `points_added_net` in its filter; `:104-105` renders only `points_added_earned`.
  - Purpose: the fetch is already paid for.
  - Verify: the gamelogs table shows a net column with values distinct from the earned column for a player with a sub-replacement week.

### Validate Before Deploy

- [x] **Backtest across every available season on the point-in-time board** (done 2026-08-04, six seasons)
  - Description: per `███████████████████████████████████████████████████`, use `projections_index` week 0 as the frozen preseason snapshot; report per-season gaps and sign consistency between the positive and net rest-of-season values.
  - Purpose: required gate, not a nicety.
  - Verify: the report exists and covers every season with data.

- [x] **Show the resulting board to the operator** (done 2026-08-04, operator approved deploy)
  - Description: top of the board, positional shares, and concentration under the net view beside the positive view.
  - Purpose: required by the same guideline. Interactive gate — surface it and wait.
  - Verify: operator response recorded before any deploy step runs.

- [x] **State which goal questions this improves and which it does not** (done 2026-08-04)
  - Description: one paragraph naming the three goal questions from the guideline and this change's effect on each.
  - Purpose: required by the same guideline.
  - Verify: `(no verify — written deliverable, reviewed at the operator gate above)`

- [x] **Deploy and confirm** (done 2026-08-04, `ca5c3aca0` on both hosts)
  - Description: `yarn deploy:all` from a throwaway worktree pinned to the pushed ref, never from the shared checkout.
  - Purpose: `yarn build` bundles the working tree, and `dist/` can hold a sibling's undeployed build.
  - Verify: read the referenced bundle hash out of the deployed page and fetch it under `/dist/`, checking content type and body rather than status code. Then confirm the field returns data through the live API on a payload not previously requested, since redis caches `/data-views/*` responses for up to 12 hours.

### Knowledge Base Updates

- [x] **Correct and extend `user:text/league/player-valuation-system.md`** (done 2026-08-04)
  - Fix the projected-side description, which overstates what exists.
  - Add the optionality doctrine and the reason a season-grain net cannot answer a weekly question.
  - Verify: no statement in the document contradicts the live schema; spot-check each column name against `db/schema.postgres.sql`.

- [x] **Add the pricing rule to `███████████████████████████████████████████████████`** (done 2026-08-04)
  - Pricing derives from the floored variant; a net-based price requires a stated population restriction.
  - Verify: `(no verify — guideline prose)`

### Follow-Ups To File

- [x] **File the deferred items as their own tasks** (done 2026-08-04)
  - Careerlog net first-N-season and best-season data-view exposure.
  - Extending the distributional draw pass beyond week 0, which neither rest-of-season variant benefits from today.
  - Start-probability-weighted value, the third variant, feeding the unified format-specific value and salary.
  - Verify: each exists as an entity with a `relates` edge back to this task.
