---
title: Migrate the roster row's pos and rid to their physical column names
type: task
status: planned
description: >-
  Remove the last rename alias layer on the roster row: rosters_players.player_position as pos and
  .roster_id as rid become the physical names across the read boundary, the Roster class, the wire,
  the SPA and the tests. Three alias lines, 55+ dependent call sites.
base_uri: user:task/league/migrate-roster-row-position-and-roster-id-to-physical-names.md
created_at: '2026-08-06T14:40:39.810Z'
entity_id: 5cdfcce2-8855-4f22-8c6c-abeaa89e839a
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - blocked_by [[user:task/league/collapse-column-alias-layer-and-gate-rename-residue.md]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-10-05T15:34:22.557Z'
visibility_analyzed_at: '2026-11-05T23:49:43.187Z'
---

## Overview

Remove the last rename alias layer on the roster row: `rosters_players.player_position as pos` and `.roster_id as rid` become the physical names across the database read boundary, the `Roster` class, the wire, the SPA, and the tests.

Three alias lines, 55+ dependent call sites. This is a shared-class API rename, not a query edit.

**Sequenced behind [[user:task/league/collapse-column-alias-layer-and-gate-rename-residue.md]].** That task ships `db/gates/check-rename-alias-residue.mjs`, which is the instrument that makes this migration auditable — including the unqualified-alias anchor that finds `get-roster.mjs:48`. Do not start this one until that gate is green and its acceptance test has passed.

## Background

### Why this is the alias-residue class

A column whose physical name differs from its in-memory name has to be aliased back at every producer, and nothing enforces the repetition. Miss one and the consumer gets `undefined`, which arithmetic turns into `NaN` and a `|| 0` turns into a plausible wrong number. `72346e579` proved it twice — `transactions.value` and the `practice.*` short names — and both are now migrated to one name across all three layers. `pos` and `rid` are what remain.

The comment at `libs-server/get-league-rosters-from-database.mjs:41-47` argues for keeping the aliases. It was added by `72346e579` itself, the original rename commit, and it is the author's rationale rather than a fresh decision. It goes away with the aliases.

### The target names are correct as they stand

`rosters_players.player_position` holds a real football position constrained by a vocabulary `CHECK` (`QB`, `RB`, `WR`, …); the roster SLOT is a separate column called `slot`. So there is no naming problem to solve on the way — the physical name is already the right name, and this migration is purely the deletion of a translation layer.

### The alias layer here is symmetric, which the other two were not

`libs-shared/roster.mjs:141-160` has a `get rosters_players()` that translates `pos`/`rid` back to `player_position`/`roster_id` for database inserts. So this is a real class boundary with translation on both sides, unlike `transactions.value`, which had translation on one side and nothing on the other. That getter already emits the target vocabulary and its callers are unaffected.

### Both producers currently emit BOTH keys

`get-league-rosters-from-database.mjs:40` selects `'rosters_players.*'` and `get-roster.mjs:48` selects `'*'`, each alongside their aliases. So every row from these two functions carries `player_position` AND `pos`, `roster_id` AND `rid`, as distinct keys. Two consequences:

- Some consumers already read the bare name today with no visible breakage — `libs-server/process-restricted-free-agency-bid.mjs:52` and `libs-server/tag-board/build-tag-board.mjs:332` both do.
- Removing the alias breaks exactly the set still reading the short name, silently, and only at runtime.

### `tid` and `lid` are NOT in this class

`rosters_players` also carries `tid` and `lid`. Those are terse names, but they are not aliased: the database, the wire, and memory all say `tid`, consistently. They have a single owner and nothing can forget to translate them. Migrating `rid` does not create an inconsistency with them, and they are out of scope.

### Site census, 2026-08-06

| Surface | Count |
|---|---|
| SQL producers aliasing back | 3 lines, 2 files |
| `new Roster({...})` call sites requiring `pos` on input rows | 40 |
| `.addPlayer({pos: ...})` call sites | 15 |
| SPA sites reading or constructing `pos`/`rid` | ~15, centered on `app/core/rosters/reducer.js` |
| Hand-built API response objects emitting `pos`/`rid` | 5 in `libs-server`, plus one socket broadcast |
| Test fixtures constructing `Roster` inputs | 17 sites in `test/roster.spec.mjs`, plus 2 spec files asserting the alias |

`api/sockets/` carries no alias-back here. The prior cluster's three-alias-backs-in-sockets hazard does not recur — the only roster-shaped thing in sockets is hand-built, and it is covered under the response-shape item below.

## Design

### The class field renames too, and that is the contested part

Most `pos` values do not come from `rosters_players` at all. At nearly every `addPlayer` site the value is `player_map.get('primary_position')` — the player dimension, not the roster row. So naming the `Roster` class field `player_position` names it after a column that is not its source at the majority of call sites, and there is a real argument for leaving the class vocabulary alone.

**Rename it anyway.** Leaving the class on `pos` while the row goes bare reintroduces the exact translation layer this work removes: the constructor would have to map `player_position` to `pos`, at one site today and at every future producer thereafter. A translation layer with one owner is better than one with none, but no translation layer is better than either, and the clean end state is one name. The value's provenance is a separate question from the field's name — `primary_position` and `rosters_players.player_position` hold the same vocabulary, enforced by the same `CHECK` constraint.

**Out of scope: `pos` as a pure-function PARAMETER name.** `has_position_capacity(pos)`, `isEligibleForSlot({slot, pos})`, `getExtensionAmount({pos, ...})`, `get_eligible_slots({pos})` and the tag-board pricing helpers take `pos` meaning "a position code" generally. Those are function arguments, not roster-row fields, and renaming them is a different change with none of this one's risk. Leave them.

Also out of scope, flagged so they are not mistaken for hazards: the optimizer's synthetic `pos` (`libs-shared/optimize-standings-lineup.mjs`, whose own doc comment disclaims the column), the `--pos` CLI flag in `libs-server/find-player-row.mjs`, every vendor-payload `.pos` in `scripts/import-*.mjs`, the `<Position pos={...}>` display prop, and `gamelog.pos` / `baseline.pos` in the SPA.

### The SPA deploy is part of the apply

`pos` and `rid` are SPA-visible through `app/core/rosters/reducer.js`. Per league `CLAUDE.md`, a rename touching a column the SPA reads makes the frontend deploy part of the apply, not a follow-up — the window between the backend landing and `yarn deploy:dist` is a user-facing outage. Sequence `yarn deploy:all`, which is a superset and cannot be partially skipped.

Note that no DDL is involved here. The physical columns already carry the target names; this is a code-only migration, which removes the usual apply-window hazard and leaves only the deploy-ordering one.

### The gate is the oracle, and the suite is not sufficient

The `transactions.value` migration found ten consumer sites, and **six of them survived a full green suite at 3304 passing**. Execution is necessary and bounded by coverage, so a green suite is evidence and not proof. Run, in order:

1. `node db/gates/check-rename-alias-residue.mjs` — must report nothing for `rosters_players` when the migration is complete.
2. `yarn check:cluster` — the full gate manifest.
3. The full suite from a clean worktree at your HEAD, with a control run at HEAD carrying none of your files, so a failure count is attributable.
4. The producer-anchored sweep documented in `CLAUDE.md`: `<identifier>.pos` / `.rid` where the identifier is a `Roster` instance, plus every consumer of `.all` / `.active` / `.players` / `.get`.

### Knowledge Base Impact

- No new `text/league/` entity and no `CLAUDE.md` change. The general rules this migration follows are already recorded there by the gate task; this is an instance of them.
- `db/gates/rename-alias-residue-adjudications.json` — the `rosters_players` entries created by the gate task must be REMOVED by this one. An adjudication that suppresses nothing is itself a finding, so a repaired site forces its entry out.

### Working tier

None. Every artifact is a source edit.

### Orchestrator shape

Not warranted. One migration cluster with a deploy, bounded by a census that is already complete.

## Plan

**Producers.**

- [ ] Remove the three alias-backs: `libs-server/get-league-rosters-from-database.mjs:48,49` and `libs-server/get-roster.mjs:48`. Delete the keep-the-alias comment at `get-league-rosters-from-database.mjs:41-47` with them.

**The shared class.**

- [ ] Rename the `Roster` class's input and output field vocabulary in `libs-shared/roster.mjs` — the constructor destructure, `addPlayer`, and every internal read. `rid` is never read off an input row (it is re-derived as `roster.uid`/`this.uid`), so it is an output-shape rename only; `pos` is both. Leave the `get rosters_players()` getter alone — it already emits the target names.
- [ ] Update all 40 `new Roster({...})` call sites and 15 `.addPlayer({pos: ...})` call sites, server-side and in `app/core/selectors.js`.

**The wire.**

- [ ] Update the five `libs-server` hand-built response producers — `submit-reserve.mjs:330-336,443-449`, `submit-deactivate.mjs:167-174`, `submit-activate.mjs:90-97`, `submit-acquisition.mjs:183-188,240-245`. Trace each calling route through to its `res.send()` rather than assuming the shape is sent verbatim.
- [ ] Fix or delete the dead broadcast fields at `api/sockets/auction.mjs:840-843` (see Notes). Do not carry them through the rename as-is.

**The SPA.**

- [ ] Update `app/core/rosters/reducer.js:31,32,44,48,104,108,232,244,248` and the ~14 component and saga sites that read or construct `pos`/`rid` on roster-shaped objects. `app/core/rosters/roster.js` stores players as plain objects in a `List`, so there is no Immutable `Record` key-drop hazard — but confirm that rather than assuming it.

**Tests.**

- [ ] Update `test/roster.spec.mjs`'s 17 fixture sites, `test/teams.add.spec.mjs:72-73,140-141`, and `test/leagues.rosters.update.spec.mjs:81`.
- [ ] Reconcile `test/leagues.rosters.update.spec.mjs` with `test/leagues.rosters.add.spec.mjs:77`, which assert opposite vocabularies on the same row shape today.

**Gates and deploy.**

- [ ] Run the four oracles in the Design's order, including the control run at HEAD.
- [ ] Remove the `rosters_players` entries from `db/gates/rename-alias-residue-adjudications.json`.
- [ ] Deploy with `yarn deploy:all` — backend and frontend together, not as two steps.

## Notes

- **`api/sockets/auction.mjs:840-843` broadcasts two dead placeholder fields.** It sends `rid: bid.rid || 0` and `pos: bid.pos || ''`, and neither field is ever set on the bid object upstream; the original author's uncertainty is still in the comments (`This might need to be passed from the roster object`). So the SPA receives a hardcoded `0` and `''` on every auction broadcast. Pre-existing, unrelated to the rename, and worth settling here rather than migrating two fields that carry nothing.
- **Two near-identical specs disagree on the vocabulary today.** `test/leagues.rosters.add.spec.mjs:77` asserts `player_position` and `test/leagues.rosters.update.spec.mjs:81` asserts `pos`, on the same `getRoster()` row shape, and both are green — because the row carries both keys. That is the standing hazard in miniature.
- **The dissent is on the record.** The session that ran the `transactions.value` migration read `pos`/`rid` as distinctive enough that the alias is defensible; the task's own analysis found `pos` needs 34 sites of `not-a-db-read` adjudication to explain away, which is the signature of a name that is not auditable. Adjudicated MIGRATE by the operator at the design gate on 2026-08-06, with the gate sequenced first so the migration is measured rather than judged.
