---
title: Standardize the prop market selection side vocabulary across books
type: task
status: in_progress
description: >-
  The same prop product is stored as YES on five books and OVER on two. The synthesized scorer
  lines, the moneyline artifacts, the line-obligation check and the strict-only under-inclusion are
  closed; the four-rung ladder collapse is ruled as one market type plus a retained rung column,
  sequenced into seven commits, and not started.
base_uri: user:task/league/standardize-prop-market-selection-side-vocabulary.md
created_at: '2026-09-16T20:17:59.538Z'
entity_id: 91974b27-c24e-4e7c-a56d-38114bcfd7f1
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - follows [[user:guideline/nfl/league/prop-market-selection-side-vocabulary.md]]
  - relates [[████████████████████████-███████-█████████-██████-█████████-████████]]
started_at: '2026-09-16T22:22:34.542Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-05T15:39:11.102Z'
visibility_analyzed_at: '2026-11-05T23:50:30.279Z'
---

> **SUPERSEDED 2026-09-19 by [[user:task/league/declare-prop-market-types-once.md]].**
>
> Everything ticked below shipped and stands. The one unstarted item — the four-rung ladder collapse — is carried forward VERBATIM by the successor, which does not re-decide it. The 2026-09-17 operator ruling (one market type plus a retained market-grain rung column), the seven-commit sequence, and the persisted-view exposure all remain authoritative and live in the section below; read them there.
>
> What the successor changes is cost, not design: once every type has one declaration, the rungs' vocabulary change is a facet edit on four entries rather than a sweep across the scattered lists.
>
> **One claim here is now measured rather than argued.** This page says the collapse is verdict-identical "by construction" and that its own rule requires an in-process measurement. That measurement exists: PrizePicks and Pinnacle already store these rungs as OVER/UNDER at N-0.5, and all 12,504 settled rows agree with `metric_result_value` with zero disagreeing.

`selection_type` is set from each book's own wording rather than from how the market grades, so the same product is stored two different ways depending on which book posted it. [[user:guideline/nfl/league/prop-market-selection-side-vocabulary.md]] is the standard this task makes true.

## Measured 2026-09-16 against production

Two violations, both narrow and both latent rather than actively wrong:

**Seven market types carry both vocabularies — 240,225 selections.**

    GAME_PLAYER_ANYTIME_TOUCHDOWN      102,770 YES / 2,603 NO     14,206 OVER+UNDER   7 books
    GAME_PLAYER_TWO_PLUS_TOUCHDOWNS     64,482 YES /   203 NO      1,390 OVER+UNDER   6 books
    GAME_PLAYER_THREE_PLUS_TOUCHDOWNS    9,826 YES /     0 NO         44 OVER+UNDER   4 books
    GAME_MATCHUP_BOTH_TEAMS_TO_SCORE     9,383 YES / 7,846 NO     11,044 OVER+UNDER   2 books
    GAME_MATCHUP_LAST_TO_SCORE           7,270 YES / 6,878 NO        254 OVER+UNDER   2 books
    GAME_MATCHUP_FIRST_SCORING_PLAY_TYPE    95 YES /    92 NO      1,564 OVER+UNDER   2 books
    GAME_MATCHUP_SAFETY_SCORED              17 YES /    14 NO        244 OVER+UNDER   2 books

On the touchdown rungs the OVER/UNDER side is PrizePicks and Pinnacle; everyone else posts YES.

**The predicate occurrence markets carried a synthesized line on 252,354 rows across seven books, and no book posts one.** REPAIRED — the count above was 96,144 when this was written, scoped to first-scorer alone; the honest population is all five predicate types. The stored 0.5 was synthesized by `occurrence_market_implicit_lines`, and the repair was to NULL the line rather than flip the token, because a scorer market is a boolean predicate and YES/NO was already correct for it.

## The direction each population moves

Do not reach for a single sweep. The two violations move opposite ways and conflating them is the mistake this section exists to prevent.

- **Touchdown ladder** — genuinely a threshold on a count, so the token becomes OVER/UNDER and the line stays. This lands with the ladder collapse, not before it. STILL OPEN.
- **Scorer and boolean matchup markets** — genuinely predicates, so the token stays YES/NO and the synthesized line is nulled. DONE.

## Plan

- [x] Split `occurrence_market_implicit_lines` in two: the ladder rungs, whose line is a real threshold, and the scorer and boolean entries, whose 0.5 is synthetic — landed `0cbf8dc21`, behaviour-neutral, exporting `ladder_occurrence_market_types` and `predicate_occurrence_market_types` and pinned as a partition of `yes_no_market_types`. Only the first keeps supplying a line once the repair below lands
- [x] Null the synthesized lines on the predicate occurrence markets — landed `9f5896f1f` and `private 535dcaa`. **252,354 index and 644,520 history rows** across seven books, wider than the 96,144 this line was written against because it covers all five predicate types rather than first-scorer alone. 177,078 were graded and **zero verdicts moved**, measured in-process rather than argued: settlement ran twice over every game carrying a predicate market across 2023-2026, against a control nulling OVER/UNDER lines in the same batches that moved 2,196,295
- [ ] **Collapse the four touchdown rungs.** Ruled 2026-09-17: one ALT ladder market type plus a retained rung column, so the rung stays re-derivable at market grain. Each sub-item below is a commit; none is started

  - [ ] **Carry the rung at market grain.** The rung must be written in the same pass and at the same grain as `market_type`, or the collapse converts a self-healing column into an unrepairable one — this is the whole reason for the retained column and the reason the plain `selection_metric_line` shape was rejected. `prop_markets_index` has no market-grain line column today, so this is the one item that IS DDL; read `docs/guides/schema.md` first, against a table holding two rows per market. Any recompute must sit on the same side of the backfill's `key_is_complete` DO-NOT-WRITE gate — a row the backfill declines to type must not receive a recomputed rung either
  - [x] **Make the four rungs agree on what a touchdown is** — landed `535058626`. The 1+ rung held an inline copy of the five scorer columns while the constant beside it promised the whole ladder shared one definition. Lists were identical, so no verdict moved; they can no longer diverge, which is the precondition for folding four rungs into one type
  - [ ] **Close the null-side ANYTIME hazard BEFORE the branch moves.** `is_touchdown_scorer_logic` grades a null `selection_type` as `metric > 0`, and `GAME_PLAYER_ANYTIME_TOUCHDOWN` leaves that family in the collapse — so every null-side ANYTIME row stops grading and becomes residue at exactly that commit. 469 exist today, all FANATICS, all from its CLOSE write path, which populates neither derived column while its OPEN twin populates both. This is the one part of the collapse that is NOT verdict-neutral
  - [ ] **Declare the stat definition once.** `TOUCHDOWN_SCORER_COLUMNS` counts punt, kickoff and fumble return touchdowns; the rename map records `GAME_RUSHING_RECEIVING_TOUCHDOWNS` as a former spelling of the 1+ rung, which excludes them. The collapse turns four unexamined declarations into one, so the book rule has to be established here rather than carried. The 140 graded rows that won on a return score alone are the entire exposure Two constraints bound the ruling. The book boundary is defensive versus special teams, not return versus non-return: FanDuel and Kalshi settle a defensive touchdown on the team Defense selection, so `defensive_touchdowns` and `opponent_fumble_recovery_touchdowns` do not belong in a player rung. And the play-flag path (`attribute_touchdown_scorer`) errs the other way, crediting only a rush, a completed pass or a lateral, so the period-scoped anytime and first/last scorer markets miss the punt and kickoff return touchdowns every book counts — one product, two definitions.
  - [ ] **Retire `touchdown_count` and its settlement branch.** `prop-market-utils.mjs:268` reads `mapping.touchdown_count` under `special_logic === 'touchdown_count_threshold'`; the rung moves to the line and the branch goes. Re-judge the stale comment there in this commit rather than editing its reason to fit — `parse_selection_metric_line` now throws on null, so the reason it states has already expired. Measure in-process before writing, in the direction being written, against a control in the same batch that MUST move
  - [ ] **Move the vocabulary in one commit.** The four rungs leave `yes_no_market_types` for OVER/UNDER, so they leave `ladder_occurrence_market_types` and enter `strict_only_market_types_outside_the_yes_no_vocabulary` — which is empty and exists solely to receive them. The derivation stops covering them at exactly that moment, so these cannot be separate commits. Move the DraftKings subcategory-12422 mapper at `draftkings-market-types.mjs:482` here too, or 1,338 backfilled markets point at a retired type. `git diff HEAD -- libs-server/prop-market-settlement/market-type-mappings.mjs` immediately before writing it
  - [ ] **Re-measure `prop-market-occurrence-line-obligation`, do not edit its prose to fit.** Its ladder arm loses the four rungs, which changes its scope. Its calibration currently claims 0 of 654,258; that reading has already lapsed independently of the collapse — the arm was 19,509 on 2026-09-17
  - [ ] **Migrate the 7 saved views structurally** through the versioned chain, adding the rung predicate. Do NOT route the four names through `MARKET_TYPE_SUBJECT_SLOT_RENAMES` expecting it to preserve the rung: it is a single-pass value lookup and cannot add a predicate. The 19 █████ █████ are unmigratable and widen — the accepted cost of the ruling
  - [ ] **Migrate the data.** No DDL on `market_type` itself — it is `varchar`, not an enum — but `selection_type` IS an enum and needs an explicit cast. Population moves as markets import, so re-measure at apply time rather than trusting the figures on this page. Goes through `private/db/adhoc/` with a `STATUS: PENDING` banner, committing the rewritten header with the apply
  - [ ] **Let both hit-rate ratchets move on their own.** 85 and 40, both derived from `market_type_mappings` by handler. Never hand-adjust one to match
- [x] Route FanDuel and DraftKings through `format_occurrence_selection_type` instead of their private copies of the same rule — landed `7a354f4c1`. DraftKings passes its Saints-stripped name, not the raw label; the cross-book suite now pins the NO leg, which it did not cover
- [x] Stop parsing the "49" in "SF 49ers" as a metric line — landed `b40252ab8`. A handicap is now a TOKEN, measured against all 76,595 distinct FanDuel names. Also stopped truncating quarter lines. The DraftKings writer was a different, already-dead one (pre-`f70920125` `extract_metric_line`, replaced forward-only 2025-12-08 with no repair)
- [x] Repair what that scan wrote, in two passes. FanDuel, `private 1556d09`: 351 index and 420 history rows set to the rung in their own name, then 197 regraded scoped to 10 markets without `--missing_only` — **45 flipped WON to LOST**. DraftKings, `private` dead-scan adhoc: 1,240 occurrence rows to their implied rung and 17,379 to null, nothing regraded because no affected handler reads the line
- [x] Register a data check for the line's CORRECTNESS — `prop-market-selection-line-contradicts-name`, `private 79dd26d`. Red at 201 violations before the repair and green after; its first run also caught a BetMGM `NaN` defect, fixed in `818dfefa0`
- [x] Rule the CLASSIFIED half of the rows still storing 49.0 — landed `private c442d4ce4`. Of 2,790 rows **only 146 are artifacts**, all FanDuel moneylines named "San Francisco 49ers"; the other 2,644 are legitimate game totals (controlled against rungs 46 through 52), which a sweep scoped by VALUE would have destroyed. Enumerated from the defect instead: 164 index and 14,052 history rows across three books. The 31,575 rows under a NULL `market_type` are still not swept and still wait on [[██████████████████████-████-██████-████-█████████████████]]
- [x] Register the OTHER check this task named — `prop-market-occurrence-line-obligation`, `private 47077d300`, both arms derived from `occurrence-market-defaults.mjs` and executed. The ladder half is red on **six units and 157,603 rows**, not the one unit expected here, and 129,019 of those are the four period-scoped touchdown families whose rungs only entered the vocabulary today in `906832378` — handed to the period-scoped settlement lane rather than repaired here. The predicate arm is green on all 18 units and was 252,354 violations hours earlier, so it has been observed both ways over the same query
- [x] Close `strict_only_market_types` under-inclusion — landed `daea3e212`. Seeded from `yes_no_market_types`, covering all 17 rather than 4, behaviour-neutral by measurement: zero yes/no types carry a cushion today. The named set survives beside the derivation and is empty until the collapse puts the rungs' successor in it

## The ladder collapse — measured, not started

The last plan item is the collapse, and this section is what a session picking it up needs so it does not re-derive any of it. **It is not started. Nothing is half-migrated.**

**It is smaller than feared in one way and larger in another.** `prop_markets_index.market_type` is `varchar`, not a Postgres enum, so there is **no DDL** — the migration is a plain `UPDATE` of three columns. But the code sweep reaches **35 files**: eight book market-type tables, `market-type-mappings.mjs`, `prop-market-type-classification.mjs`, `bookmaker-constants.mjs`, `occurrence-market-defaults.mjs`, `prop-hit-rate.mjs`, the simulation projection loader, the client's `market-navigation.js`, and fifteen spec files.

**The population.** 226,660 selections under 15,770 markets, 140,739 of them graded:

    GAME_PLAYER_ANYTIME_TOUCHDOWN     10,064 markets   121,548 selections    80,116 graded
    GAME_PLAYER_TWO_PLUS_TOUCHDOWNS    3,430 markets    67,720 selections    51,898 graded
    GAME_PLAYER_THREE_PLUS_TOUCHDOWNS  2,094 markets    36,921 selections     8,312 graded
    GAME_PLAYER_FOUR_PLUS_TOUCHDOWNS     182 markets       471 selections       413 graded

**The rung is not in the column the collapse wants to read it from, and this is the sharpest thing on this page.** Today the rung is read off `mapping.touchdown_count` rather than off `selection_metric_line`, deliberately, so that a row which missed the implicit-line default cannot silently grade against zero. An ALT ladder type reads the rung from the line, which inherits exactly that failure. Measured before depending on it:

    GAME_PLAYER_ANYTIME_TOUCHDOWN      121,548 sel    1,046 no line     80,116 graded    0 graded with no line
    GAME_PLAYER_TWO_PLUS_TOUCHDOWNS     67,720 sel      858 no line     51,898 graded    0 graded with no line
    GAME_PLAYER_THREE_PLUS_TOUCHDOWNS   36,921 sel   26,657 no line      8,312 graded    0 graded with no line
    GAME_PLAYER_FOUR_PLUS_TOUCHDOWNS       471 sel       21 no line        413 graded    0 graded with no line

**The graded population was already safe and the ungraded one was not — RESOLVED.** Every one of the 140,739 graded rows carried a line, so the collapse could never have corrupted an existing verdict. But 28,582 ungraded rows carried none, and under a line-based rung each would have become un-gradeable the moment its game settled — a gradeable row silently converted into residue. **That is fixed**: those rows were filled as part of the 157,611-row ladder repair in `private 30f849720`, so every selection under all four rung types now carries its rung. **The collapse no longer has to fill anything first.** Re-measure before relying on this — the population drifts as new markets import, and the standing guard is `prop-market-occurrence-line-obligation`, which reads this exact condition on its ladder arm.

**Retire the constant and move its mapper in the SAME commit.** `GAME_PLAYER_THREE_PLUS_TOUCHDOWNS` is emitted by the DraftKings mapper at `draftkings-market-types.mjs:482` for subcategory 12422, and 1,338 of those markets were backfilled to fully classified on 2026-09-16. Retiring the constant without moving the mapper points every one of those rows at a type that no longer exists.

**Two ratchets move mechanically, so re-measure rather than adjust.** The `prop-hit-rate` spec ratchet reads 85 and got there through three lanes in one day (66 to 71 on Fanatics period ALT constants, 71 to 79 on eight period-scoped scorer types, 79 to 85 on six Fanatics first-half ALT mappings); the PLAYER_GAMELOG ratchet beside it reads 40. Both derive from `market_type_mappings` by handler, so collapsing types moves them on their own — never hand-adjust one to match.

**`market-type-mappings.mjs` was stale-copied over twice on 2026-09-16**, once landing on master and taking every spec down at module LOAD because a removed export was still imported elsewhere. Run `git diff HEAD -- libs-server/prop-market-settlement/market-type-mappings.mjs` immediately before writing to it, and re-read from disk if it has been open a while.

**The design decision is RULED: one market type plus a retained rung column.** Operator ruling, 2026-09-17. What follows is why, and what the measurement changed about it.

`MARKET_TYPE_SUBJECT_SLOT_RENAMES` is already many-to-one — two entries point at `GAME_PLAYER_ANYTIME_TOUCHDOWN` — so mapping four names onto one collapsed type fits it. The problem is that the map rewrites the market TYPE and nothing else, while the collapse moves the rung from the type into a data column. A persisted view filtered on `GAME_PLAYER_THREE_PLUS_TOUCHDOWNS` would rename cleanly onto the collapsed type and then silently match **every rung**. And the map cannot be taught to add a rung predicate: `apply_param_value_renames` is a SINGLE-PASS value-for-value lookup, and the module's own header records that short URLs are immutable snapshots that never enter the versioned chain in `data-view-storage/migrations.mjs`, so there is no re-save that can retire an entry.

**The decisive argument is not the saved view, it is re-derivability.** While the rung IS the market type, `ladder_occurrence_market_lines` declares it at market grain, keyed by type — which is how the 157,611-row fill filled "from the rung each market type declares" without parsing anything. Move the rung into a nullable column that nothing recomputes and that property is gone. This is not hypothetical: 18,333 Caesars selections lost their rung within six hours of that fill, when `scripts/backfill-market-types.mjs` re-classified 4,947 markets into the rung types and nothing recomputed the derived columns. Post-collapse that class is unrepairable, because the collapsed type would no longer declare a rung for a fill to read.

**The persisted exposure was measured 2026-09-17 and is far smaller than feared, but is not zero.** Counted against `user_data_views` (215 rows) and `urls` (986 rows):

    GAME_PLAYER_ANYTIME_TOUCHDOWN        7 saved views    19 █████ █████
    GAME_PLAYER_TWO_PLUS_TOUCHDOWNS      0 saved views     0 █████ █████
    GAME_PLAYER_THREE_PLUS_TOUCHDOWNS    0 saved views     0 █████ █████
    GAME_PLAYER_FOUR_PLUS_TOUCHDOWNS     0 saved views     0 █████ █████

Both zeros are controlled in the same query rather than asserted: the ANYTIME row fired on the identical predicate, and 122 of the 986 URLs carry a `market_type` param at all. **The three upper rungs are referenced by nothing persisted anywhere**, so the widening risk is confined entirely to the 0.5 rung. The 7 saved views are versioned and take a structural migration that adds the rung predicate. The 19 █████ █████ cannot be migrated and WILL widen from "1+ touchdown" to "any touchdown rung" — a display-surface loss, not a settlement one, and the whole of it.

## The canonical end state

Established 2026-09-17 from the code and the vendor data rather than from the brief's framing, because the brief's framing is subtly wrong in one place.

**The rung is MARKET-grain, because that is the grain every book publishes it at.** Caesars sends `Player To Score 2 or More Touchdowns — MORE_THAN_ONE_TOUCHDOWN`; Fanatics sends `To Score 2 Or More Touchdowns`. In both the rung is in the MARKET name and the selections underneath are bare player names carrying no handicap — which is the entire reason `occurrence-market-defaults.mjs` exists. So storing the rung only on `selection_metric_line` is a grain DEMOTION: it writes one market-grain fact onto every selection redundantly, and nothing can restore it once lost. This is why the brief's "carry the rung on `selection_metric_line`" is not the end state, and why the ruled retained column has to sit on `prop_markets_index`.

**The decomposition, which is what makes this canonical rather than merely tidy.** Three things vary across this family, and each belongs in exactly one place:

- **What is counted** (touchdowns vs sacks) — the market TYPE
- **When it counts** (game, first half, third quarter) — the market TYPE
- **How many are needed** (1+, 2+, 3+, 4+) — the market-grain rung COLUMN

Today the third is encoded in the type name, so a 5+ rung means a new constant plus eight book-table entries rather than a row of data. Under the decomposition the period-scoped touchdowns and `GAME_PLAYER_ANYTIME_SACK` keep their own types and each carries a rung, and `ladder_occurrence_market_lines` stops being a rung ladder and becomes what it already half is — the DEFAULT rung for a market whose book publishes only one.

**The collapse is verdict-identical by construction, and the existing constants were already chosen to make it so.** Integer metrics against N-0.5 lines:

    1+   today  metric > 0        after  line 0.5, OVER   metric > 0.5   ==  metric >= 1
    2+   today  metric >= 2       after  line 1.5, OVER   metric > 1.5   ==  metric >= 2
    2+   today  metric < 2  (NO)  after  line 1.5, UNDER  metric < 1.5   ==  metric <= 1

A push cannot fire: `|integer - N.5|` is never below `push_epsilon`. This is an argument, not a measurement, and the lane's own rule says measure it in-process against a control in the same batch that must move — but it tells the harness what it has to show, which is zero.

**What the collapse actually retires is larger than the brief says, and better.** Not just `touchdown_count` and the per-book rung tables, but:

- the `touchdown_count_threshold` branch in `prop-market-utils.mjs`
- `GAME_PLAYER_ANYTIME_TOUCHDOWN`'s membership in `is_touchdown_scorer_logic` — the 1+ rung settles through a DIFFERENT branch from its own siblings today, which is the real defect under the four constants
- the **two-unit double encoding**: `market-type-mappings.mjs` holds the rung as whole numbers (2, 3, 4) under `>=`, while `occurrence-market-defaults.mjs` holds the same rung as N-0.5 under strict `>`. One ladder, two modules, two units, two comparison operators

**The one hazard that is not verdict-neutral** is the null-side ANYTIME grading shortcut, sequenced as its own plan item above: fix the FANATICS CLOSE path or fill its 469 rows BEFORE the branch moves.

**The stat definition has to be declared once, and the collapse forces it.** The four rungs share `TOUCHDOWN_SCORER_COLUMNS` since `535058626`, which counts punt, kickoff and fumble return touchdowns. The type's own rename history argues the other way: `MARKET_TYPE_SUBJECT_SLOT_RENAMES` records `GAME_RUSHING_RECEIVING_TOUCHDOWNS` as a former spelling of `GAME_PLAYER_ANYTIME_TOUCHDOWN`, a name that excludes returns. Under four types this could stay unexamined in four places; under one type plus a rung it is a single declaration, and the 140 graded rows that won on a return score alone are the exposure.

**Three things landed today that the collapse must update rather than discover.** Each was written anticipating it:

- `strict_only_market_types_outside_the_yes_no_vocabulary` in `prop-hit-rate.mjs` is empty and exists solely to receive the collapsed type. An ALT ladder is OVER/UNDER, so the four rungs LEAVE `yes_no_market_types` and the derivation stops covering them at exactly that moment — this is the same commit the plan item below means.
- `ladder_occurrence_market_types` loses the four rungs, which changes the scope of `prop-market-occurrence-line-obligation`. Its calibration prose is measured against today's vocabulary and must be re-measured, not edited to fit.
- The `prop-market-utils.mjs` comment at the `touchdown_count_threshold` branch is stale in its stated reason and was deliberately left for this collapse to re-judge.

**Coordinate before starting.** The period-scoped settlement lane is live on the adjacent period-scoped touchdown types and on the shared ladder/scorer branch in `prop-market-utils.mjs`, and it holds the 129,019-row backfill named above.

## Rules that apply

The vocabulary MUST be derived from `market_type_mappings`, never from a hand-kept set beside it. The FanDuel ladder lost both its side and its line for three seasons because a key table lagged the constants it fed; a second set to keep in step is the same defect waiting.

A wrong grade is worse than a null. Measure the settlement interaction in-process before writing to any graded population, keying results on `(source_id, source_market_id, source_selection_id, time_type)` — `batch_calculate_markets` regroups its input by calculator type. The harness is `scratch/prop-predicate-line-null/measure-predicate-line-null.mjs` in the league tree, which runs the real path twice over the same markets and diffs the verdicts. Scratch is gitignored and single-machine, so treat it as rebuildable: its shape is `fetch_markets_for_games` plus the four registered data handlers plus `batch_calculate_markets`, run once as stored and once perturbed.

**Perturb a control in the same run, and require the two readings to differ.** An empty diff proves nothing on its own — the predicate-line measurement returned zero on all four seasons only because the same harness nulling OVER/UNDER lines in the same batches moved 2,196,295 verdicts. The harness also refuses outright on a season where its perturbation touches nothing, so a run that could not distinguish the two cases cannot report a confident zero.

`prop_markets_index` holds two rows per market and `prop_market_selections_index` is keyed by `time_type` too, so any join needs all three key columns or it doubles.
