---
title: Retire the write-only teams.salary_cap column
type: task
status: completed
description: >-
  teams.salary_cap is a second source of truth for a quantity Roster.availableCap already derives,
  and nothing reads it: two auction writers, no decision-making reader on the server or the client.
  Delete the column and its dependents.
base_uri: user:task/league/retire-write-only-team-salary-cap.md
created_at: '2026-09-06T18:23:21.090Z'
entity_id: e7f31ace-ff77-4ef6-8e57-354e9e0697ff
finished_at: '2026-09-06T19:38:34.247Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
started_at: '2026-09-06T18:56:13.036Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-06T19:38:34.247Z'
visibility_analyzed_at: '2026-11-05T23:50:14.463Z'
---

## Outcome

`teams.salary_cap` is dropped from `league_production`. The column was a second source of truth for a quantity `Roster.availableCap` already derives — the league cap minus the ACTIVE roster's salaries — and it had writers but no reader that decided anything.

It was initialized to the league cap at team creation and thereafter decremented ONLY by auction settlements, never by a waiver, a draft pick, a trade or a free agent add, so on any league that had run a season it described nothing. Four separate user-base notes had independently recorded it as a data-quality trap; all four are now corrected.

## What shipped

- `940abe569` — the code half. Five writers removed (both auction writers, both team-creation paths, the test fixture) plus an unused select in `generate-tag-board`. The settlement's charge-landed equality assertion went with the write it checked; the cap-monotonicity guard stayed and is now the only cap check in `persist_auction_settlement`. `Team` record, teams reducer case, swagger team schema and the driver's cap-restore teardown all removed. Six specs were CONVERTED to derive the cap rather than deleted, so the monotonicity invariant is still asserted.
- `d89bfc56f` — the DDL half: the drop, `yarn export:schema`, regenerated types, and the `dev-fixture-projection.json` disposition.
- `1ba0a0fe1` — the generalizable rule, in `docs/guides/schema.md`.

## Why it was two commits

The auction was live (league 1's free agency period ran to 2026-09-08). Dropping the column while the deployed API still wrote it would have raised 42703 inside `persist_auction_settlement`, rolled the settling transaction back, and — by the settlement-refusal design — erased and refused the manager's election with it, in a mode where completeness is the only thing that advances the auction and there is no clock to retry.

Expand-contract closed that window at no cost: the code shipped first naming nothing, and the drop followed once the deployed process could not name the column. Neither commit held code that could not run against its own committed schema, so the one-commit rule was satisfied rather than bent. The rule now lives in `docs/guides/schema.md` § "A DROP against a LIVE service wants TWO commits".

## Evidence

- Full suite green on both schemas, 7557 passing / 0 failing, and the green discriminates: a control spec reading the column fails 42703 under the candidate schema and passes under the tracked one, proving `LEAGUE_SCHEMA_FILE` was honored.
- The deploy was verified rather than assumed — the deployed tree carried zero cap writes against a positive control, and the pm2 `server` process started after the deploying commit.
- `scripts/drive-auction-end-to-end.mjs --lid 119` run AFTER the drop: 127 passed, 1 failed. A real second-price settlement executed, with "the price is charged to the winner, exactly once" passing — the direct proof that removing the stored column did not break the charge. The single failure was a stale assertion unrelated to this work, fixed in `50cf1955e`.
- CI green at `c140c43bb`.

## Follow-on

The sale-payload convergence and the missing transactions-slice case moved to [[user:task/league/converge-the-auction-sale-payload.md]]. `free_agent_acquisition_budget_balance` sits beside the dropped column on the same table and IS a genuine stored balance with real readers — it stays.
