report: model architecture recovery map — the live grade uses 0 of 3 specced layers

Archaeology only; nothing built, reconnected, or promoted.

The champion is two DISCONNECTED estimates: the letter is engine1's additive
factor index (zero references to p_win or any probability in engine1.js), and
p_win is probabilityEstimator's frequencyOver + 5 heuristic layers, computed
after and merely attached. The live grade path never calls the Python service.

The Python three-layer engine is NOT DEPLOYED — no python/pip in the
Dockerfile; app.js only health-checks it. So Layers 1-2 never shipped.
Layer 3 is wired BACKWARDS: grade_thresholds.json maps PROBABILITY->GRADE and
the live JS reads it in reverse to manufacture confidence from an
already-chosen letter. Per-sport market-efficiency scaling is specced-absent.

Consequence stated plainly: every metric audited to date is on the shadow
model, not the specced engine, which has never been measured.

Sport boundary TESTED not asserted: a new sport on the live path is a ~10-file
core edit with four documented silent-failure modes. Per-sport records DO
exist (sports.mlb n=526/62% vs pooled overall n=937/58%, each n>=20 gated),
but /api/accuracy ignores ?sport= and the pooled overall would absorb a new
sport. Park x weather confirmed challenger-only; xwOBA and leash absent.

Recovery map is dependency-ordered with MLB as the reference module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
Kev
2026-07-30 23:16:34 -04:00
parent f3bf300b36
commit dd98b0b614
3 changed files with 320 additions and 0 deletions
+29
View File
@@ -397,3 +397,32 @@ deploy boundary; the anonymous live order is *1. Brionna Jones (edge 29.4) · 2.
**PAID FIELDS: NONE**. A bogus bearer token also yields no paid fields (fail-closed proven in prod). **PAID FIELDS: NONE**. A bogus bearer token also yields no paid fields (fail-closed proven in prod).
The board's proxy path now returns 10 props, so TOP GRADES **renders** instead of falling back to empty. The board's proxy path now returns 10 props, so TOP GRADES **renders** instead of falling back to empty.
The rendered board is client-side → tagged for the Chrome audit, not faked. The rendered board is client-side → tagged for the Chrome audit, not faked.
---
# MODEL ARCHITECTURE RECOVERY MAP — 2026-07-30 (report-only) → `specs/model-architecture-recovery-map.md`
**The live grade uses 0 of the 3 specced layers.** Every audited metric (calibration, CLV, skew audit,
takeable floor, p_win→CLV r=0.375) is measured on the SHADOW model. Those findings stand — the shadow
model served every real grade — but none are evidence about the specced engine, which has never been
measured.
| Specced component | State |
|---|---|
| Layer 1 Similarity (`python/utils/similarity.py`, 101 ln) | BUILT · NOT WIRED · **NOT DEPLOYED** |
| Layer 2 Bayesian (`python/utils/bayesian.py`, 320 ln) | BUILT · NOT WIRED · **NOT DEPLOYED** — and its "sport-agnostic math / per-sport parameters" claim is TRUE of the built code |
| Layer 3 grade scale (`grade_thresholds.json`) | BUILT · **WIRED BACKWARDS** — the table maps PROBABILITY→GRADE; the live JS reads it in reverse to manufacture `confidence` from an already-chosen letter |
| Per-sport market-efficiency scaling | **SPECCED-BUT-ABSENT** |
| Live champion (`engine1` + `probabilityEstimator`) | BUILT · WIRED — but the letter is a factor-index with **zero probability input**, and `p_win` is computed separately and never feeds it |
**The Python engine is not in the deploy image at all** (no python/pip in `Dockerfile`; `app.js` only
health-checks it). **The sport boundary is NOT clean on the live path** — adding a sport is a ~10-file
core edit with four documented silent-failure modes, so "make a sport a module" is itself a prerequisite
build. **Per-sport records DO exist** (`sports.mlb` n=526/62% vs pooled `overall` n=937/58%, each with
its own n≥20 gate) — the rule to enforce is that a new sport renders `sports.{sport}`, never `overall`.
**Park×weather is a CHALLENGER, not the champion** ("measured, never served"); xwOBA and bullpen-leash
are absent entirely.
Recovery is dependency-ordered in the map: decide the grading basis → pick a runtime (recommend porting
Bayesian to Node) → wire Layer 2 → reconnect Layer 3 forward → Layer 1 → per-sport efficiency →
challenger promotion → sport-as-module → NFL/CFB. MLB is specced as the reference module.
+64
View File
@@ -399,6 +399,70 @@
> (renders the band IN USER-VISIBLE COPY — "Takeable band 160 to +200", so a floor change is a copy > (renders the band IN USER-VISIBLE COPY — "Takeable band 160 to +200", so a floor change is a copy
> change too).** > change too).**
> ## 🗺️ MODEL ARCHITECTURE RECOVERY MAP 2026-07-30 (report-only) → **`specs/model-architecture-recovery-map.md`**
> Archaeology of the specced three-layer engine vs the live grade path. Nothing built,
> reconnected, promoted, or changed. **HEADLINE: the live grade uses 0 of the 3 specced layers —
> every metric this project has audited (calibration, CLV, the skew audit, the takeable floor, the
> champion's p_win→CLV r=0.375) is measured on the SHADOW model, not the specced engine.** Those
> findings stand (the shadow model served every real grade) but none of them are evidence about the
> specced architecture, which has never been measured.
> **0.1 THE CHAMPION = two DISCONNECTED estimates.** The letter is `engine1.gradeProp`:
> `idx = NEUTRAL_INDEX(3); for (f of factors) idx += f.delta` — and **`engine1.js` has ZERO
> references to `p_win` or any probability** (grep count 0). The probability is
> `probabilityEstimator` (`frequencyOver` + 5 heuristic layers, clamp [.10,.95]) computed AFTER and
> merely attached. **p_win never feeds the letter; the letter never feeds p_win.** The live grade path
> makes **no call to the Python service at all**.
> **🔴 THE PYTHON ENGINE IS NOT DEPLOYED.** `Dockerfile` has no python/pip/requirements — the image is
> Node-only; `app.js:92-95` only health-CHECKS it and reports `down`. Layers 1-2 are not "bypassed at
> runtime", they have **never shipped**. `grade_thresholds.json` carries `"shadow_mode": true`.
> **LAYER STATE:** L1 Similarity `python/utils/similarity.py` (101 ln, MIN_SIMILARITY .7, NBA+MLB
> factor weights) **BUILT·NOT WIRED·NOT DEPLOYED** — the live grade skips straight to season/recent
> averages. L2 Bayesian `python/utils/bayesian.py` (320 ln) **BUILT·NOT WIRED·NOT DEPLOYED** — and the
> spec's "sport-agnostic math, per-sport parameters" claim is **TRUE of the built code**
> (`BAYESIAN_WEIGHTS` prior/recent/context for 11 stat types across both sports, skewness,
> data-sufficiency degradation, ±0.15 calibration clamp, a calibration disclaimer). L3
> **BUILT·WIRED BACKWARDS** — `grade_thresholds.json` maps **PROBABILITY→GRADE** (A+ = p .85-1.00);
> the live JS reads it in REVERSE, picking a letter from the factor index then looking up that
> letter's band MIDPOINT to manufacture `confidence`. **That one artifact IS the drift, provable in a
> single file.** Per-sport **market-efficiency scaling = SPECCED-BUT-ABSENT** (one global scale; the
> spec's MLB .55 / NBA-stars .80 exists nowhere; the only `efficiency` in code is *usage*-efficiency).
> **⚠️ The spec's "min 15 instances" is NOT in the code — CANNOT DETERMINE its origin** (code's
> abstention is `similar_games_below: 3`; the only 15 is NBA `min_minutes_per_game`; the founding
> three-layer spec text is not in the repo). **NOTE: STATE.md's own "Layer 1/2/3" references are the
> Session 73-77 ARCHETYPE build train — a different numbering. Do not conflate them.**
> **🔴 SPORT BOUNDARY — TESTED, NOT ASSERTED: it is NOT clean on the live path.** Adding a sport to the
> Python spine costs nothing (the spine is dead); adding one to the LIVE path is a **CORE EDIT across
> ~10+ files** — proplineAdapter.MARKETS · oddsNormalizer.MARKET_MAP (miss → silent ZERO) ·
> oddsService.SPORT_KEYS/SPORT_MARKETS · **three stat_type whitelists** (analyze.js, scan.js,
> validation.py — miss one → silent 400s) · featureCache log-field · outcomeService MLB_LOG_FIELD +
> SPORTS · liveTrackingService LIVE_BOX_FIELD · a projection-emitting extractor (**without it every
> prop REFUSES**) · emptyState/statFilters/teams · and a settled feed or the sport never settles.
> Making "a sport" a real module is itself a prerequisite build.
> **PER-SPORT RECORDS EXIST (2.7 satisfied):** `getModelAggregate` filters on `opts.sport`
> (`ledgerService:809,819`) and `/api/accuracy` carries a real breakdown — live **`sports.mlb` n=526
> 62%** alongside **`overall` n=937 58%**, each with its own `min_sample:20`. Two caveats: the pooled
> `overall` would absorb a new sport's rows, and **`/api/accuracy` IGNORES the `?sport=` query param**
> (identical payload for mlb vs wnba — consumers must read `sports.{sport}`).
> **PARK×WEATHER CONFIRMED CHALLENGER, NOT CHAMPION** — attached in `snapshotService` (S77) onto
> `env_*`/`challenger_*` columns; `ledgerService.js:253` says it outright ("measured, never served"),
> and proj-v1.1, which consumes park/weather/platoon/arsenal, came back **NOT PROVEN** on 2026-07-29.
> MLB champion inputs are ONLY l5/l20 + opp_rank + rest + usage. **xwOBA = 0 files (absent); leash = 0
> (absent);** barrel/hard-hit/launch-angle/exit-velo/statcast/platoon all present but challenger-only.
> **RECOVERY ORDER (dependency-sequenced, full detail in the map):** 0 decide the grading BASIS
> (probability-vs-factor-index — a founder call; note that moving to probability grading legitimately
> changes the letter distribution because it is NEW INFORMATION, categorically unlike rescaling
> thresholds to mint A's) → 1 pick a runtime (**recommend PORTING `bayesian.py` to Node** rather than
> resurrecting a service every live path already routes around) → 2 wire Layer 2 FIRST (its
> prior/recent/context inputs are already available from `featureCache.getStatRows`, no new I/O) →
> 3 reconnect Layer 3 FORWARD (grade FROM probability; kills the `confidence` tautology) → 4 Layer 1
> Similarity (now feasible — S73-77 produce the context store it needs) → 5 per-sport efficiency
> scaling → 6 challenger promotion on ledger evidence only → 7 make "a sport" a real module →
> 8 NFL/CFB. **MLB = the REFERENCE MODULE** (deepest stack + a working settled feed + its own 526-row
> record); the map lists the exact contract a new sport must declare.
> **HONEST PER-SPORT LABELING: mechanism EXISTS, rule must be enforced** — every future sport renders
> `sports.{sport}` and reads ACCRUING until its own n≥20 clears; never `overall`, which would silently
> borrow MLB/WNBA credibility.
- **Redirect EXISTS + WIRED:** `closingCapture.buildCaptureRows``closing_captures` (append-only, - **Redirect EXISTS + WIRED:** `closingCapture.buildCaptureRows``closing_captures` (append-only,
provenance: captured_at/book/line_type/both-prices/missed_reason) via `intradayRefreshService:221` provenance: captured_at/book/line_type/both-prices/missed_reason) via `intradayRefreshService:221`
+ internal endpoint; `ledgerService.attachClosingProb``closing_prob` (de-vigs both raw sides, + internal endpoint; `ledgerService.attachClosingProb``closing_prob` (de-vigs both raw sides,
+227
View File
@@ -0,0 +1,227 @@
# VYNDR — MODEL ARCHITECTURE RECOVERY MAP
Report-only archaeology, 2026-07-30. **Nothing was built, reconnected, promoted, or changed.**
This is the map every subsequent reconnection order and every new-sport module builds against.
---
## HEADLINE — the live grade uses approximately NONE of the specced three-layer engine
**Everything this project has audited — calibration, CLV, the over-side skew audit, the
takeable floor, the champion's p_win→CLV r=0.375 — is measured on the SHADOW model, not on
the specced engine.** That does not invalidate those findings (the shadow model is what
actually served every grade), but it does mean none of them are evidence about the specced
architecture, and the specced engine has never been measured at all.
---
## REVIEW ZERO
### 0.1 What the live champion actually is — VERIFIED
Two disconnected estimates, neither Bayesian nor similarity-based:
**The GRADE LETTER**`engine1.gradeProp` (`src/services/intelligence/engine1.js:170-174`):
let idx = NEUTRAL_INDEX; // 3 === 'C'
for (const f of factors) idx += f.delta;
idx = clampIndex(Math.round(idx));
return { grade: GRADE_SCALE[idx], confidence: GRADE_TO_CONFIDENCE[GRADE_SCALE[idx]] ?? 0.25 };
An additive integer index over flat ±1.0/±0.5 factor deltas. **`engine1.js` contains ZERO
references to `p_win` or any probability** (verified: grep count 0). The letter is computed
with no probability input of any kind.
**The PROBABILITY**`probabilityEstimator.estimateProbability`: empirical
`frequencyOver(values, line)` plus five heuristic adjustment layers (last-5 weighted 2x,
opponent bump/fade, home/away ±1.5%, volatility pull toward 0.50), clamped [0.10, 0.95].
Computed AFTER the grade and merely attached to the payload.
**So: `p_win` never feeds the letter, and the letter never feeds `p_win`.** They are two
independent estimates of the same event that are free to disagree — which is exactly why
`confidence` (a band-midpoint lookup of the already-chosen letter) carries zero information.
**It bypasses the three-layer engine completely.** Verified: the live grade path
(`gradeSlateService``analyzeViaEngine1``engine1` / `featureCache`) contains **no call
to the Python service at all**.
### 0.2 Component inventory — every specced piece, tagged
| Component | File | State |
|---|---|---|
| Layer 1 Similarity | `python/utils/similarity.py` (101 ln) | **BUILT · NOT WIRED · NOT DEPLOYED** |
| Layer 2 Bayesian | `python/utils/bayesian.py` (320 ln) | **BUILT · NOT WIRED · NOT DEPLOYED** |
| Layer 3 grade scale | `python/data/grade_thresholds.json` | **BUILT · WIRED BACKWARDS** (see §3) |
| Market-efficiency scaling | — | **SPECCED-BUT-ABSENT** |
| Shared archetypes | `python/utils/archetypes.py` | BUILT · superseded by JS `archetypeService` |
| Sport blueprints | `python/blueprints/mlb.py`, `nba_context.py` | **BUILT · NOT WIRED · NOT DEPLOYED** |
| Live grade engine | `intelligence/engine1.js` | **BUILT · WIRED · IS the champion** |
| Live probability | `intelligence/probabilityEstimator.js` | **BUILT · WIRED · not fed into the grade** |
**🔴 THE PYTHON ENGINE IS NOT DEPLOYED AT ALL.** `Dockerfile` contains no `python`, `pip`, or
`requirements` — the deploy image is Node-only. `app.js:92-95` merely health-CHECKS
`PYTHON_SERVICE_URL` and reports `python: 'down'`. Layers 1 and 2 are not "bypassed at
runtime"; they have never shipped. `grade_thresholds.json` even carries `"shadow_mode": true`.
---
## PHASE 1 — THE THREE LAYERS, ACTUAL STATE
### Layer 1 — Similarity: BUILT, NOT WIRED
`MIN_SIMILARITY = 0.7`; `SIMILARITY_FACTORS` carries NBA factors (opp DRtg .15, pace .12,
rest .08, home/away .06, role match .15, teammate context .10) and MLB factors (pitcher
handedness .12, park .10, opponent quality .12, weather .05, day/night .04, batting order .06).
**The live grade never performs a similarity search — it goes straight to season/recent
averages** (`projectionFor`: `l5_avg ?? l20_avg ?? {stat}_per_90 ?? xg_per_90`).
**⚠️ The spec's "min 15 instances" is NOT in the code — CANNOT DETERMINE its origin.** The
only abstention rule is `ABSTENTION_RULES.similar_games_below = 3` (`bayesian.py:46`); the
only `15` is `nba_player.min_minutes_per_game`. The founding three-layer spec text is not in
the repo, so the 15 could not be reconciled. (STATE.md's "Layer 1/2/3" references are the
Session 73-77 **archetype build train** — a DIFFERENT numbering. Do not conflate them.)
### Layer 2 — Bayesian: BUILT, NOT WIRED, and genuinely as specced
`bayesian.py` header: *"Shared by NBA and MLB. Per-stat-type weights. Similar game confidence
modifier. Skewness parameter. Data sufficiency smooth degradation curve."* It carries
`BAYESIAN_WEIGHTS` = prior/recent/context triples for **11 stat types across both sports**
(e.g. `strikeouts .40/.40/.20`, `hits .30/.45/.25`, `points .35/.45/.20`), plus data-sufficiency
minimums per player type, a calibration offset clamped to ±0.15, and a `CALIBRATION_DISCLAIMER`
("Model in calibration period... Track record begins building now").
**VERDICT: the sport-agnostic-math / per-sport-parameters claim is TRUE of the built code.**
It is simply not connected to anything. **The champion's `p_win` does NOT flow through it.**
### Layer 3 — Grading: the table exists and is WIRED BACKWARDS
`grade_thresholds.json` maps **PROBABILITY → GRADE** (`A+ 0.85-1.00`, `B 0.60-0.65`,
`F 0.00-0.29`). In the specced engine the grade IS a function of the probability.
**The live JS reads that same table in REVERSE**: it picks a letter from the factor index,
then looks up that letter's band MIDPOINT to manufacture `confidence`
(`engine1.js:31-36`, `confidence_basis: 'grade_band'`). A probability→grade table is being
used as a grade→fake-confidence lookup. **This single artifact is the drift, provable in
one file.**
**Market-efficiency scaling per sport: SPECCED-BUT-ABSENT.** One global 11-band scale, no
sport dimension anywhere. The spec's MLB 0.55 / NBA-stars 0.80 does not exist in code; the
only `efficiency` hits are *usage*-efficiency in `nba_context.py`/`redistribution.py`, an
unrelated concept.
### The gap, plainly
**The live grade uses 0 of 3 specced layers.** It uses: recent/season averages, an opponent
rank, rest, usage — summed as flat ±deltas onto a neutral 'C'. No similarity search, no
Bayesian inference, no probability-based grading, no per-sport calibration.
---
## PHASE 2 — THE SPORT-PLUGIN BOUNDARY
**Is the shared spine sport-agnostic?** In the *Python* spine, largely yes: `bayesian.py` is
sport-agnostic math parameterised per stat type. One mild leak: `similarity.py` co-mingles
NBA and MLB factor keys in a single `SIMILARITY_FACTORS` dict rather than per-sport blocks
(harmless — absent keys simply do not contribute — but it is not a clean plug point).
`blueprints/` is a general module directory (coaching, synergy, evolution, resolution…), **not
a sport-plugin registry**; `mlb.py` and `nba_context.py` happen to live there.
### 🔴 TESTED, NOT ASSERTED: what breaks if a sport is added today
Adding a sport to the *Python* spine costs nothing — because the spine is dead. Adding a
sport to the **LIVE** path is a **CORE EDIT across ~10+ files, not a module drop-in**:
1. `proplineAdapter.MARKETS` — request the markets
2. `oddsNormalizer.MARKET_MAP` — miss it and props silently normalize to ZERO
3. `oddsService.SPORT_KEYS` + `SPORT_MARKETS`
4. **THREE stat_type whitelists that must stay in sync**`routes/analyze.js`,
`routes/scan.js`, `python/utils/validation.py` (miss one → silent 400s)
5. `featureCache` log-field map (features)
6. `outcomeService.MLB_LOG_FIELD` + `outcomeService.SPORTS` (settlement)
7. `liveTrackingService.LIVE_BOX_FIELD` (live tracking)
8. A feature extractor emitting a projection field — **without it every prop REFUSES**
9. `lib/emptyState.js`, `config/statFilters.js`, `web/src/lib/teams.js`
10. A settled-result feed, or the sport never settles (NBA/soccer still do not)
**VERDICT: the boundary is NOT clean on the live path.** A new sport today is a core edit
with at least four documented silent-failure modes. Making a sport a genuine module is
itself a prerequisite build, not a given.
### Per-sport records — EXIST (2.7 satisfied at the data layer)
`getModelAggregate` accepts `opts.sport` and filters both the settled and pending queries
(`ledgerService.js:809,819`). `/api/accuracy` returns a real per-sport breakdown — live:
`sports.mlb` = **n 526, 62%**, alongside `overall` = **n 937, 58%** — each carrying its own
`min_sample: 20` gate. **So a new sport CAN accrue an independent record with its own
accruing→proven state.**
**⚠️ Two caveats.** (a) A pooled `overall` exists alongside, and a new sport's rows WOULD
enter it — so any surface rendering `overall` would silently blend a new sport's record with
MLB/WNBA's. (b) `/api/accuracy` **ignores the `?sport=` query param** (identical payload for
`?sport=mlb` and `?sport=wnba`); consumers must read `sports.{sport}` from the full object.
---
## PHASE 3 — THE MODERN-DATA STACK
### MLB — what is wired, and to WHAT
| Input | Present | Feeds the CHAMPION? |
|---|---|---|
| l5 / l20 averages, opp_rank_stat, rest, usage | yes | **YES — this is the whole champion** |
| park factor (19 files), weather (18) | yes | **NO — challenger only** (`env_park_base`, `env_weather_mod`) |
| platoon / handedness (9 / 7 files) | yes | **NO — challenger only** |
| statcast (11 files): barrel%, hard-hit%, launch angle (4 each), exit velo (2) | yes | **NO — challenger only** |
| pitcher arsenal | yes | **NO — proj-v1.1 challenger only** |
| **xwOBA** | **0 files — ABSENT** | — |
| bullpen (1 file) / **leash (0 — ABSENT)** | partial | **NO** |
**Park×weather status CONFIRMED: CHALLENGER, NOT CHAMPION.** Attached in `snapshotService`
(Session 77) onto `env_*`/`challenger_*` ledger columns — `ledgerService.js:253` states it
outright: *"is what served the user; this is measured, never served."* And per the completion
matrix, proj-v1.1 (which consumes park/weather/platoon/arsenal) was tested 2026-07-29 and
came back **NOT PROVEN** — it lost to the champion. So the deep-data stack is accruing
measurement, not serving grades, and has not yet earned promotion.
### Incoming sports — input stacks to scope (NOT built)
- **NFL**: snap share, target share / route participation, team pace (plays per game),
scheme matchup (coverage/front vs role), role change on injury, red-zone usage,
weather (outdoor), Vegas team total as context.
- **CFB**: tempo, **soft-market efficiency (the actual edge thesis — books price CFB props
thinner)**, role concentration (few-star offenses), talent gradient / mismatch, pace,
garbage-time risk, limited-history handling (the Bayesian prior matters MOST here).
Both need a settled-result feed wired BEFORE launch, or they will grade and never settle —
the NBA/soccer failure mode already live in this repo.
---
## PHASE 4 — THE RECOVERY MAP, IN DEPENDENCY ORDER
| # | Step | Depends on | What it takes |
|---|---|---|---|
| **0** | **DECIDE THE GRADING BASIS** — probability-based (spec) vs factor-index (live). Everything below hangs on this. | — | A founder decision, not a build. Note: moving to probability-based grading legitimately CHANGES the letter distribution because it is NEW INFORMATION — that is categorically different from rescaling thresholds to mint A's (permanently forbidden). |
| **1** | **Choose a runtime for Layers 1-2: deploy Python, or PORT to Node.** | 0 | Python is absent from the Dockerfile and has been offline for the project's life; the live path is 100% Node. **Recommend PORTING** `bayesian.py` (320 ln) to Node over resurrecting a service every live path has learned to route around. |
| **2** | **Wire Layer 2 (Bayesian) first.** | 1 | Its inputs (prior / recent / context) are ALL already available from `featureCache.getStatRows` — no new I/O. Reachable soonest; delivers a probability the grade can actually be a function of. |
| **3** | **Reconnect Layer 3 forward** — grade FROM the probability using the existing table. | 2 | Deletes the backwards `confidence` lookup and makes `confidence` a real probability. Kills the tautology in one move. |
| **4** | **Wire Layer 1 (Similarity).** | 2, 3 | Needs a historical instance store with context — S73-77 park/weather/platoon now PRODUCE that, so it is newly feasible. Set the min-instance threshold explicitly (the spec's 15 is unreconciled; code says 3). |
| **5** | **Per-sport market-efficiency scaling.** | 3, 4 | Needs per-sport calibration accrual — the per-sport ledger record already exists to supply it. |
| **6** | **Promote park×weather / challengers** — only on ledger evidence. | 5 | Currently NOT PROVEN (proj-v1.1 lost to champion). Gated, not scheduled. |
| **7** | **Make "a sport" a real module** (collapse the ~10-file core edit into a registry). | 3 | Prerequisite for NFL/CFB shipping as modules rather than core surgery. |
| **8** | **NFL / CFB modules.** | 7 | Each needs its input stack + a settled-result feed before launch. |
### MLB as the REFERENCE MODULE
MLB is the right reference and is already the deepest: free unlimited `statsapi` (game logs,
season stats, probables, box scores), statcast row per batter, park base, weather modifier,
platoon split, pitcher arsenal, a settled-result feed that works, and 526 settled rows of its
own record. **A new sport module should be required to declare the same contract:** market
list → normalizer mapping → stat whitelist (all three) → feature extractor emitting a
projection → log-field map for features → settlement field map + a settled feed → live box
map → archetype set → thresholds/efficiency score → its own ledger record.
### Honest per-sport labeling — MECHANISM EXISTS, RULE MUST BE ENFORCED
The n≥20 per-sport gate and independent `sports.{sport}` records already exist. **The rule for
every future sport: launch with the engine fully built but the record rendered from
`sports.{sport}` and labeled ACCURING until that sport's own games clear its own gate — never
`overall`, which would silently borrow MLB/WNBA credibility.** The pooled `overall` is the
specific hazard to avoid on any new-sport surface.
---
## TAGS
VERIFIED: 0.1, 0.2, all of Phase 1, the sport-boundary break-test, per-sport records,
park×weather challenger status, MLB input inventory, Python-not-deployed.
CANNOT DETERMINE: the spec's "min 15 instances" (founding spec text absent from the repo;
code says 3). BLOCKED: none.