---
title: State the market type naming convention and derive the grain predicates from it
type: task
status: completed
description: >-
  Put the TEAM grain marker in a fixed name slot, derive team_grain_market_types and
  team_subject_market_types from the declaring groups, and repair the Caesars markets the previous
  under-scoped derivation left unattributed.
base_uri: user:task/league/state-market-type-naming-convention.md
created_at: '2026-09-08T14:08:50.325Z'
entity_id: 1d921c24-0e87-47c5-85a0-cb7788316c81
finished_at: '2026-09-08T18:02:34.718Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - blocks [[███████████████████████-██████-███-██████████]]
  - relates [[█████████████████████████-████-██████-█████-████████]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-08T18:02:34.718Z'
visibility_analyzed_at: '2026-11-05T23:50:30.743Z'
---

## Overview

Market type constants encode grain three ways at once — the group a constant is declared in, a `TEAM` token in its name, and two separately-maintained predicate sets — and the three disagreed. This task states one naming convention, derives both predicates from the groups, and repairs the production rows the disagreement corrupted.

The trigger was the Kalshi import plan needing to coin a team-grain constant and having no rule to follow. See [[███████████████████████-██████-███-██████████]].

## The convention

```
<SCOPE> [TEAM] [<PERIOD>] [ALT] <STATISTIC>
```

`TEAM` is the grain marker and sits immediately after the scope, so every team-grain constant reads `GAME_TEAM_*`. Position rather than presence is what makes the rule work: `TEAM` also appears inside statistic names, where it means something else entirely.

Three grains exist and only one is marked. `GAME` grain covers both teams and settles once (`GAME_SPREAD`). `TEAM` grain covers one team, so a book posts one market per team (`GAME_TEAM_PASSING_YARDS`). `PLAYER` grain covers one player (`GAME_RECEIVING_YARDS`). Game and player grain are both unmarked, so the name alone cannot separate them — the declaring group does, which is why the derived sets read the group and never the name.

## What was wrong

- `GAME_ALT_TEAM_TOTAL` was the only constant putting `ALT` ahead of the grain marker, and so the only reason the derivation had to test for the `TEAM` token anywhere rather than in its slot. That weaker test admits `GAME_FIRST_TEAM_TOUCHDOWN_SCORER` (a player market) and `GAME_BOTH_TEAMS_TO_SCORE` (a game market).
- `team_grain_market_types` was derived from `team_game_market_types` alone, so the five team-grain constants in `team_props_types` were excluded. `is_team_grain_market_type` returned false for all five, the Caesars importer's team-attribution branch never ran for them, and two teams' markets collided on one `(esbid, market_type, selection_name)` key.
- `GAME_TEAM_TO_SCORE_3_UNANSWERED` was declared in `game_props_types` despite being team grain, so it sat outside every team set.
- `get-selection-id-from-source.mjs` maintained its own union of the three team groups, answering a genuinely different question ("does `selection_pid` hold a team?") under a name that read like the grain predicate.

## Plan

- [x] **Apply the rename migration to production** — applied 2026-09-08, 2,698 rows, commit `3411bf756`
  - File: `db/adhoc/2026-09-08-rename-game-alt-team-total-to-game-team-alt-total.sql`, status APPLIED
  - 2,698 rows in `prop_markets_index`, season_year 2023-2025. `market_type` is `varchar(100)`, so no DDL and no schema export.
  - Verify: the file's own DO block refuses a no-op run and asserts the presence invariant — zero rows left under the old name.
  - Gate: operator approved 2026-09-08.

- [x] **Repair the historical null `selection_pid` rows on the five newly-in-scope constants** — no backfill needed; the live importer re-attributed all 196 selections on 2026-09-08 and only 2026-season rows exist
  - Measured 2026-09-08: 124 of 196 selections across `GAME_TEAM_TOTAL_POINTS_ODD_EVEN` and `GAME_TEAM_TO_SCORE_FIRST_AND_WIN` carry a null pid; the other two constants and `GAME_TEAM_TO_SCORE_3_UNANSWERED` have no production rows.
  - The team is recoverable from the market-name segment already stored in `prop_markets_index.source_market_name`, which is `<event name> - <market name> — <template> — <category>`; the market-name segment carries the team as a genuine prefix.
  - Verify: no `(esbid, market_type, selection_name, time_type)` group on these types may span more than one `source_market_id` with a null pid among them. That query returned 12 colliding groups before the fix and must return zero after.

- [x] **Confirm the importer now attributes on the next Caesars run** — met by the 16:00/20:00 UTC polls on 2026-09-08; the collision oracle returns zero groups and no selection carries a null pid
  - Verify: re-run the collision query above against markets observed after the deploy; it must find the two teams distinguished rather than one side null.
