Follow-up from #190 (player_key sweep). The #190 branch swept the enumerated roster/lineup/swap layers off bare-name keying, but three residual bare-name spots remain in the browse-page audit + compare-standings subsystem, which #190 did not enumerate. Each collides a two-way player's hitter and pitcher rows (same name). All latent today (no two-way player rostered) and surfaced repeatedly by loop-review on the #190 branch.
-
Compare endpoint — web/season_data.py:1123 compute_comparison_standings resolves the dropped player via find_player_by_name(roster_player_name, ...) (first bare-name match). The route (season_routes.py ~1719) reads roster_player as a bare HTTP arg with no type, so the compare view can only ever price the first (hitter) row of a two-way player; the pitcher-row comparison is unreachable. Fix needs the compare route + frontend to thread roster_player_type (the sibling other_player side already passes other_type; the roster-audit template already emits entry.player::entry.player_type).
-
Audit SGP/gap display — lineup/roster_audit.py player_sgp / fa_sgp are keyed on bare name, so a two-way player's two rows collide and the browse-page drop-candidate SGP/gap shows the wrong row's value.
-
audit_index — web/season_routes.py:1442 looks up audit_index[(drop_player.name, fa_name)], and audit_index (built from the ROSTER_AUDIT cache) is keyed on bare (player_name, fa_name) tuples. Two-way rows collide in the cache. Fixing requires AuditEntry.player to carry the key/type and a cache-shape change.
These form one coherent cluster: the audit subsystem's identity model (AuditEntry.player, audit_index, the serialized ROSTER_AUDIT cache, and the compare route's HTTP arg) is name-based end-to-end. Fixing properly means re-keying that model on player_key (name::player_type), which is a larger cross-cutting change than #190's scope.
Found by loop-review on fix/190-player-key-sweep, 2026-07-12.
Follow-up from #190 (player_key sweep). The #190 branch swept the enumerated roster/lineup/swap layers off bare-name keying, but three residual bare-name spots remain in the browse-page audit + compare-standings subsystem, which #190 did not enumerate. Each collides a two-way player's hitter and pitcher rows (same name). All latent today (no two-way player rostered) and surfaced repeatedly by loop-review on the #190 branch.
Compare endpoint —
web/season_data.py:1123compute_comparison_standingsresolves the dropped player viafind_player_by_name(roster_player_name, ...)(first bare-name match). The route (season_routes.py~1719) readsroster_playeras a bare HTTP arg with no type, so the compare view can only ever price the first (hitter) row of a two-way player; the pitcher-row comparison is unreachable. Fix needs the compare route + frontend to threadroster_player_type(the siblingother_playerside already passesother_type; the roster-audit template already emitsentry.player::entry.player_type).Audit SGP/gap display —
lineup/roster_audit.pyplayer_sgp/fa_sgpare keyed on barename, so a two-way player's two rows collide and the browse-page drop-candidate SGP/gap shows the wrong row's value.audit_index—web/season_routes.py:1442looks upaudit_index[(drop_player.name, fa_name)], andaudit_index(built from theROSTER_AUDITcache) is keyed on bare(player_name, fa_name)tuples. Two-way rows collide in the cache. Fixing requiresAuditEntry.playerto carry the key/type and a cache-shape change.These form one coherent cluster: the audit subsystem's identity model (
AuditEntry.player,audit_index, the serializedROSTER_AUDITcache, and the compare route's HTTP arg) is name-based end-to-end. Fixing properly means re-keying that model onplayer_key(name::player_type), which is a larger cross-cutting change than #190's scope.Found by loop-review on
fix/190-player-key-sweep, 2026-07-12.