dd98b0b614
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
228 lines
14 KiB
Markdown
228 lines
14 KiB
Markdown
# 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.
|