---
title: Ship an alias-site-anchored gate for rename residue
type: task
status: completed
description: >-
  A renamed column whose in-memory name still differs must be aliased back at every producer, and
  nothing enforces it. Ship a third gate anchored on the alias site, which needs neither a table
  FROM-literal nor a distinctive word, and migrate the three nfl_plays aliases it reports.
base_uri: user:task/league/collapse-column-alias-layer-and-gate-rename-residue.md
created_at: '2026-08-06T14:17:19.350Z'
entity_id: f17a9554-f677-497c-9c6d-d688eed21926
finished_at: '2026-08-06T15:13:58.968Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
tags:
  - user:tag/league-xo-football.md
  - ███████████████-██████-███████████
  - user:tag/software-task.md
updated_at: '2026-11-06T00:02:58.751Z'
visibility_analyzed_at: '2026-11-06T00:17:42.883Z'
---

## Overview

Ship a third rename gate, anchored on the alias site, and remove the one remaining alias cluster it would report outside the roster row.

1. **`db/adhoc/check-rename-alias-residue.mjs`** — given a schema diff against a base ref, mechanically find residual alias-backs, split producers, and orphaned aliases. These are the classes the two existing gates provably cannot see.
2. **Migrate the three `nfl_plays` aliases** in `scripts/generate-player-snaps.mjs`.
3. **Correct two doc surfaces** that assert coverage the gates do not have.

The `rosters_players.player_position as pos` / `.roster_id as rid` migration was split out — it is a shared-class API rename across 55+ call sites and is tracked separately in [[user:task/league/migrate-roster-row-position-and-roster-id-to-physical-names.md]]. Ship this gate first; it is the instrument that makes that migration auditable.

## Background

### The defect that prompted this

`72346e579` renamed `transactions.value` to `transactions.player_salary`. The in-memory and wire name stayed `value`, so every producer feeding the `Roster` constructor had to alias it back. Only one did.

- `libs-server/get-roster.mjs:52` — aliased, repaired in `782b78907`
- `libs-server/get-league-rosters-from-database.mjs:51` — bare `transactions.player_salary`, never repaired

Players arrived with `value: undefined`, `availableCap` summed to `NaN`, and `NaN || 0` at `libs-shared/roster.mjs:97` collapsed it to `0`. Every team showed the raw `$200` cap as its salary space.

That defect is now fixed. `8643dc8a7` and `33721c0b9` renamed the in-memory field to `player_salary` across all ten consumer sites, so no aliasing of that column remains anywhere. The gate below exists so the next one is found mechanically.

### Why the audit trail said the class was clean

`check-renamed-column-consumers.mjs` gate 2 anchors its candidate set on `db('<table>')` FROM-target literals (line 492). `get-league-rosters-from-database.mjs` reaches `transactions` through `.leftJoin('transactions', ...)` off `db('rosters_players')`, so it was **structurally unreachable** by the gate — not merely missed by a reviewer. Verified: the gate at `--base 62ca45544` produces 36 sites for `transactions.value` and that file is not among them.

This is a **new blind spot**, not one the gate's header documents. The header's blind-spot list stops at "a table name held in a constant, or reached through a knex instance bound elsewhere"; it never says a JOINED table is invisible. That gap is general — 22 join-only sites across the same window. The adjudications file records the structural reason as of `b94d29fae` and `c8210053e`; the gate header and league `CLAUDE.md` still do not.

### Second live defect, same commit, opposite direction

`72346e579` also renamed `practice.m`/`.tu`/`.w`/`.th`/`.f`/`.s`/`.su` to `monday_practice_status` and siblings, rewriting the in-memory keys in `libs-shared/is-reserve-eligible.mjs` while adding an alias-back to the OLD short names in `libs-server/verify-reserve-status.mjs`. The two halves contradicted each other and the practice path was dead from that commit. Fixed in `86195f64b`.

Gate 2 could not see it for two independent reasons:

- `m`, `tu`, `w`, `th`, `f`, `s`, `su` are all under `MIN_MATCHABLE_COLUMN_LENGTH` (3) and were silently counted into `unmatchable_count`.
- The shape is inverted. Gate 2 hunts a consumer reading an OLD name off a wholesale select. Here the read boundary correctly aliased to a name the consumer no longer used — an **orphaned alias-back**, which gate 2 has no concept of.

### What makes a rename auditable

A distinctive token (`rid`, `dwn`, `qtr`) is greppable; a generic word (`value`, `type`, `year`, `total`) is not. But the property that decides auditability is narrower:

> A rename is auditable when the OLD name's read sites can be enumerated. Anchoring on the TABLE (as gate 2 does) buys that for FROM-target queries and loses it for joined ones. Anchoring on the WORD buys it only for distinctive tokens.

Neither anchor alone is complete. The gate below adds a third that needs neither: the **alias-back site itself**.

### What the shipped gate measures, 2026-08-06

Every figure below is from the SHIPPED gate against `--base 62ca45544`, not from the prototype scans. The prototype reported 6 findings; the gate as designed reports **18** before the migration and **15** after. The gap is not drift — the prototype counted only the SPLIT PRODUCERS shape on a narrower anchor, while the shipped gate carries all three finding classes and both anchor forms, which is what the Design specifies.

Denominator: **345 alias literals** in server code (196 qualified, 149 unqualified), of which 76 are unqualified with no resolvable table — a declared blind spot, counted rather than dropped silently.

The 15 remaining findings, all adjudicated:

```
api/routes/percentiles.mjs:101-109   percentiles.percentile_*/min/max  keep-aliased (wire contract)
libs-server/get-players.mjs:368      scoring_..._points.total          keep-aliased (both producers agree)
scripts/process-███████████████:212  scoring_..._points.total          keep-aliased (same shape)
libs-server/get-league-rosters...:48,49  rosters_players.pos/rid       migrate-tracked
libs-server/get-roster.mjs:48        rosters_players.pos/rid           migrate-tracked (unqualified anchor)
```

The prototype's 5-site list was accurate for what it measured and missed the `percentiles` and `total` clusters entirely, both of which the shipped gate reports and both of which are genuine alias-backs over lost columns.

### Three corrections the re-measurement forced

**The scan misses unqualified alias-backs.** `libs-server/get-roster.mjs:48` writes `.select('*', 'player_position as pos', 'roster_id as rid')` with no table qualifier. Anchored on `'<table>.<new> as <old>'`, the scan cannot see it — so the gate as originally specified would have missed one of the two producers it exists to find.

**The registry-path exclusion is load-bearing, not belt-and-braces.** The original design asserted that registry alias names are never former columns, so the lost-column filter already removes them. Asserted rather than assumed, it fails: `libs-server/data-views-column-definitions/player-adp-column-definitions.mjs:107` aliases to `adp`, which `player_adp_index` did lose.

**The ORPHANED ALIAS read-shape matcher needs four cases that failed in production.** The sibling's `player_salary` migration found ten consumer sites, six of which survived a green 3304-test suite, and its greps missed: `get('value')` not matching `get('value', 0)`; `value:` not matching object shorthand `value,`; and one caller enumeration finding 2 of 9.

## Design

### The gate — `db/adhoc/check-rename-alias-residue.mjs`

Anchor: **the alias-back site**, which needs neither a table FROM-literal nor a distinctive word.

**Derivation.** Diff `db/schema.postgres.sql` against a base ref to get, per table, the set of columns it LOST. Then scan `api/`, `libs-server/`, `libs-shared/`, `scripts/`, `jobs/` for an alias whose TARGET name is in that lost set. That condition is the whole discriminator — it separates a rename alias-back from an ordinary join-disambiguation alias (`waivers.uid as wid`, `leagues.name as league_name`), which no other filter can do.

**Two anchor forms, not one.** A qualified literal (`'<table>.<new> as <old>'`) resolves its own table. An unqualified literal (`'<new> as <old>'`) resolves the table from the enclosing `db('<table>')` statement, the same statement scoping gate 2 already performs. Omitting the second form is not a coverage gap in the abstract — it is a miss on a known site.

**Three finding classes, each with its own verdict question:**

1. **SPLIT PRODUCERS** — the column is aliased back at one site and projected bare at another. Ask: does the bare producer feed the same consumer? This is the `transactions.value` shape.
2. **ORPHANED ALIAS** — the old name is not read by the consumer the aliased row reaches. This is the `practice.*` shape. Detecting it needs the consumer's key set, so the practical form is: flag any alias-back whose NEW column name is ALSO read as an in-memory key somewhere in the tree. Two vocabularies for one column means some producer/consumer pair disagrees.
3. **UNIFORM** — every producer aliases back and the consumer reads the old name. Correct today; still reported, because it is the standing hazard the migration half exists to remove.

**The read-shape matcher carries the four production-proven cases as named patterns with a fixture each**: `get('<name>')` with and without a `notSetValue` second argument, `<name>:` object-property form, `<name>,` object-shorthand form, and destructuring. A matcher that reports clean over live breakage is the failure mode this gate exists to prevent, so these are asserted, not assumed.

**Two refinements the prototype proved necessary:**

- **Projection context only.** `libs-server/get-players.mjs:49,55,71,77` name `rosters_players.roster_id`/`.player_position` inside `groupBy(...)`, not a projection. Counting those produced four false SPLIT PRODUCERS findings. Restrict bare-reference sites to `select`/`first`/`pluck`/`returning` argument positions.
- **Registry paths excluded from the alias side, as a real filter.** `column-definitions/` and `data-view` paths declare wire ids; a zero-read alias there is expected. One such site passes the lost-column filter today, so the exclusion carries weight on its own and must be documented as a filter with its own reasoning.

**What stays OUT of the gate: the producer-anchored sweep.** The sibling's `player_salary` migration found its ten sites with a fourth anchor — sweep `<identifier>.<field>` where the identifier is an instance of the class whose field moved, plus enumerate every consumer of that class's collection getters (`Roster` `.all`/`.active`/`.players`/`.get`). It anchors on the PRODUCER, so it works for a generic word and needs neither a greppable token nor a spec. It is the anchor that actually worked, and it does not belong in gate code: it cannot self-derive its input. The alias anchor derives its whole list from the schema diff; this one needs a known moved field and a known class, so encoding it would mean hand-maintaining a class-to-field registry that drifts silently. It goes into league `CLAUDE.md` as a required rename-cluster step instead, cross-referenced from the gate header.

**Should the gate refuse a rename because the name is generic?** No. Renames CONCENTRATE on generic names, so such a rule would fire on nearly every real migration and get bypassed. Encode the property instead: the anchor is the alias site, equally findable for `value` and for `rid`. Report a per-rename **auditability note** — old name length, occurrence count across the tree, and whether the old name survives on another table — as reviewer context, never as a pass/fail condition.

**CI placement.** Out, same reasoning as gate 2: it diffs against a base ref, reads the working tree, and its findings need per-site adjudication. Register it in `scripts/check-cluster-gates.mjs` so `yarn check:cluster --base <ref>` runs it.

**Negative control, mandatory and always-on.** Per the standing rule in league `CLAUDE.md`, a control that stays green fails the run. Two mutations: inject a synthetic alias-back for a known lost column and assert it is reported; delete a known bare producer and assert the SPLIT PRODUCERS class loses that finding. Inject the reader as a parameter with a default rather than monkeypatching `fs` — an ESM namespace object is frozen, so a patched member silently does nothing and the control reports STAYED GREEN over a working gate.

**Acceptance test.** At `782b78907^` in a worktree, the gate must report `transactions.player_salary as value` as SPLIT PRODUCERS naming `libs-server/get-league-rosters-from-database.mjs`. Per the unqualified-anchor correction it must find both the qualified site and the unqualified one at `get-roster.mjs:48`. At current HEAD it must report nothing for that pair. Same gate, same base, only the fix varies.

### The `nfl_plays` migration

Three aliases in one file, reads are same-file and local, no SPA surface: `scripts/generate-player-snaps.mjs:71,75,76`. Take `win_probability`, `down_number` and `quarter` through to the destructure and every downstream read in that script.

Note this script has a spec (`test/scripts.generate-player-snaps.spec.mjs`, written `e88f9650c`) that EXECUTES the insert. Run it — a test inspecting the payload object passes over a column that does not exist, and only the round trip tells those apart.

### Knowledge Base Impact

- `db/adhoc/check-renamed-column-consumers.mjs` header — add the JOIN blind spot to the blind-spot list, and cross-reference the new gate.
- `repository/active/league/CLAUDE.md` — same addition at the gate-2 section, plus the producer-anchored sweep as a required rename-cluster step.
- `db/adhoc/rename-alias-residue-adjudications.json` — new, keyed `(table, column, file)` with a required reason, following the `documentation-schema-drift` rule that **an adjudication suppressing nothing is itself a finding**, so a repaired site forces its entry out rather than leaving a standing exemption.
- No new `text/league/` entity. The gate documents itself in its header, matching the two existing gates.
- Already landed, not carried here: the `transactions.value` adjudication correction (`b94d29fae`, `c8210053e`).

### Working tier

None. The prototype scans are throwaway and the shipped artifact is the single gate file plus its adjudications.

### Orchestrator shape

Not warranted. Bounded — one gate file, one three-line migration, two doc corrections.

## Plan

**The gate.**

- [x] Write `db/adhoc/check-rename-alias-residue.mjs` per the Design: lost-column derivation from the schema diff, both anchor forms (qualified and statement-scoped unqualified), projection-context restriction, registry-path exclusion, three finding classes, the four read-shape patterns, and the per-rename auditability note.
- [x] Give it the always-on negative control — two mutations, reader injected as a parameter rather than monkeypatched — and make a control that stays green fail the run.
- [x] Run the acceptance test in a worktree at `782b78907^`. Confirm it reports `transactions.player_salary as value` as SPLIT PRODUCERS naming BOTH `get-league-rosters-from-database.mjs` and `get-roster.mjs:48`, and reports nothing for that pair at current HEAD.
- [x] Register it in `scripts/check-cluster-gates.mjs` with its oracle string, ordered by cost among the base-ref gates.
- [x] Create `db/adhoc/rename-alias-residue-adjudications.json` and adjudicate the current findings. Assert that an entry suppressing nothing fails the gate.

**The `nfl_plays` migration.**

- [x] Migrate `nfl_plays.win_probability as wp`, `.down_number as dwn`, `.quarter as qtr` at `scripts/generate-player-snaps.mjs:71,75,76` to the physical names, through the destructure and every downstream read.
- [x] Run `test/scripts.generate-player-snaps.spec.mjs` and confirm the insert executes. Backend-only — no frontend deploy.

**The docs.**

- [x] Add the JOIN blind spot to the blind-spot list in `db/adhoc/check-renamed-column-consumers.mjs`'s header, and cross-reference the new gate from it.
- [x] Add the same to league `CLAUDE.md`'s gate-2 section, plus the producer-anchored sweep as a required rename-cluster step.

## Notes

- **Gate 2's join blind spot is worth fixing independently, and is deliberately not in scope.** Extending its candidate set to `.join`/`.leftJoin` string literals is a small change to the `from_re` at `db/adhoc/check-renamed-column-consumers.mjs:492`, and it would have caught the `value` defect directly. It is out because it reopens every existing adjudication (the site lists are keyed on file, and adding join sites adds files), which per `CLAUDE.md` defers every session's push behind a red master. Do it as its own cluster with its own re-adjudication pass. Re-affirmed at the design gate on 2026-08-06.
- The `MIN_MATCHABLE_COLUMN_LENGTH = 3` floor in gate 2 is honest about being an under-report but reports only a COUNT. The `practice.*` rename put seven columns under that floor in one migration. This gate has no such floor — an alias site is matchable regardless of the old name's length — which is a second reason its anchor is the right one.
- `check-renamed-column-consumers.mjs` exits **0** with `GATE OK` when handed a base ref git cannot resolve, printing one `SKIPPED` line. Do not reproduce that shape: an unresolvable base ref must be a hard failure.
