diff --git a/outputs/VYNDR-COMPLETION-MATRIX.md b/outputs/VYNDR-COMPLETION-MATRIX.md index a9c3a25..579980e 100644 --- a/outputs/VYNDR-COMPLETION-MATRIX.md +++ b/outputs/VYNDR-COMPLETION-MATRIX.md @@ -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). 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. + +--- + +# 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. diff --git a/specs/STATE.md b/specs/STATE.md index 1f78f56..e584ae5 100644 --- a/specs/STATE.md +++ b/specs/STATE.md @@ -399,6 +399,70 @@ > (renders the band IN USER-VISIBLE COPY — "Takeable band −160 to +200", so a floor change is a copy > 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, provenance: captured_at/book/line_type/both-prices/missed_reason) via `intradayRefreshService:221` + internal endpoint; `ledgerService.attachClosingProb`→`closing_prob` (de-vigs both raw sides, diff --git a/specs/model-architecture-recovery-map.md b/specs/model-architecture-recovery-map.md new file mode 100644 index 0000000..a5413b7 --- /dev/null +++ b/specs/model-architecture-recovery-map.md @@ -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.