---
title: Narrow the external roster removal suppression so one unmappable player does not freeze a team
type: task
status: completed
description: >-
  Narrow the mirror roster sync rule that suppresses every removal on a team when any one external
  player id fails to map, so one unmappable player stops freezing a team.
base_uri: user:task/league/narrow-external-roster-removal-suppression.md
created_at: '2026-09-22T20:59:05.466Z'
entity_id: b1450a25-a8e2-4dd7-b983-74a662301545
finished_at: '2026-09-28T22:34:57.021Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - succeeds [[████████████████████████-████████-██████-███████-██████████]]
  - relates [[████████████████████████-██████-████████-█████-██-█████████]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-28T22:34:58.230Z'
visibility_analyzed_at: '2026-11-05T23:49:45.652Z'
---

## Problem

A mirror roster sync suppresses **every** removal on a team when **any** external player id on it fails to map. `libs-server/external-fantasy-leagues/sync/roster-sync.mjs:304` sets `player_mapping_is_complete`, and the delete at `:389-394` is gated on it, so one unresolvable player means the whole team keeps players the vendor no longer rosters.

The suppression is deliberate and its reasoning at `:297-303` is correct: an unresolvable id is indistinguishable from a departed player. It is over-broad rather than wrong.

Eight of twelve teams hit this on the import that produced [[████████████████████████-████████-██████-███████-██████████]]. That task makes the condition VISIBLE — counted and reported as partial. Making it rarer is this task.

## Why the obvious narrowing is unsound

The tempting rule: a pid on our roster carrying the platform's id column has a known external id, so if that id is absent from the vendor's payload it cannot be one of the unmapped ones and may be removed. It rests on disjointness — no unmapped id resolves to a pid whose id we already hold — and two live counterexamples break it.

**A vendor entry with no id key.** `_extract_roster_players` builds `external_player_id: roster_player.player_ids?.[player_id_key]` at `:658` with **no null filter**, where `_build_player_mapping_inputs` filters at `:572` precisely because the key can be absent. So an entry missing its id yields `undefined`, falls to the unmapped branch at `:290`, and is pushed as the literal string `"undefined"` at `:291-293`. That unmapped entry corresponds to a real rostered player whose own id we may well hold — disjointness fails, and the narrowed gate deletes someone the vendor still rosters.

**A re-issued vendor id.** The vendor lists a new id for a player our catalog knows under an old one. The new id is unmapped; the old id is known and absent from the payload; disjointness fails again. `player-id-mapper`'s name/position/team fallback with id back-write exists because this happens.

## What the severity is now

Lower than when first assessed, because of `c05ab7e88` (2026-09-22): a mirror roster is a weekly series and the diff runs against `current_roster_rows` for that week's `roster_id`. A delete therefore corrects a row **we** authored for week N rather than destroying live state, and a later sync re-adds the player once the vendor serves a mappable id. On a first backfill the set is empty and no removal can fire at all.

So a wrong removal is a transiently wrong mirror, not permanent loss. Still wrong, and still not worth shipping on a false premise.

## Restating the rule correctly

Week-agnostic, and phrased as set-equality rather than as an inference that a player was dropped — "dropped" was only ever true when a roster was one current snapshot, and in a backfilled week absence from week N's payload is a fact about week N:

> The operation makes our week-N roster equal the vendor's week-N roster. Suppression exists because an unmapped id means we cannot assert set-equality. A pid may be removed only when no unmapped id could possibly resolve to it.

The narrowing is sound only under a precondition the tree does not currently satisfy: every vendor roster entry carries a real external id. Fixing the `:658` null filter is therefore a prerequisite, not a nicety — and it does not rescue the re-issued-id case, which needs its own answer.

## Scope

- Fix the null-filter asymmetry at `roster-sync.mjs:658` so an entry with no id is filtered rather than stringified into `"undefined"`. This is a defect regardless of the narrowing: it puts the literal text `undefined` into a user-visible error message.
- Decide the re-issued-id case before narrowing anything.
- Normalize id coercion on both sides. `unmapped_external_player_ids.push(String(...))` at `:291-293` stringifies, while `player.sleeper_player_id` comes off the database; if the two are not coerced identically every known id reads as absent and the gate deletes the whole roster. Note also the canonical key is `sleeper_id` (`:17`) while the column is `sleeper_player_id` — two namespaces.

## Done when

- A team with one unmappable player has its unambiguous removals applied.
- A fixture reproducing each counterexample above does NOT remove the player, and is run against an unperturbed control whose reading differs.

## Plan

- [x] Fix the null filter in `_extract_roster_players`: an entry with no id stays unresolved as `null`, and ids are coerced to strings on both the mapping and roster sides.
- [x] Decide the re-issued-id case: the vendor still names the player, so a pid is held when any unresolved entry shares a surname token with it, and an unnamed unresolved entry holds every removal.
- [x] Allow a removal when the vendor rosters the pid on another team that week. Vendor rosters are disjoint, so this is sound whatever the unresolved entries are.
- [x] Pin each counterexample with a fixture that fails under the naive narrowing, beside a control that removes.
