STATE: Session 68 — Layer 1 mechanism data live (1,354 rows, 100% join)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
+88
-1
@@ -1,5 +1,5 @@
|
|||||||
# VYNDR — STATE OF THE WORLD
|
# VYNDR — STATE OF THE WORLD
|
||||||
### As of `4e2f488` (main, DEPLOYED + fingerprinted live), 2026-07-20. This file opens every future session. **Start with the CURRENT STATUS + OPEN ITEMS block below.**
|
### As of `a499598` (main, DEPLOYED + fingerprinted live), 2026-07-21. This file opens every future session. **Start with the CURRENT STATUS + OPEN ITEMS block below.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -131,6 +131,93 @@ exist locally; harmless, the data restores completely.
|
|||||||
`specs/snapshot-retention.md` (Phase 2 report + schema) ·
|
`specs/snapshot-retention.md` (Phase 2 report + schema) ·
|
||||||
`specs/audit-data/grade-collapse.md` · `specs/audit-data/gate-simulation.md`.
|
`specs/audit-data/grade-collapse.md` · `specs/audit-data/gate-simulation.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🟢 SESSION 68 — LAYER 1: STATCAST MECHANISM DATA (2026-07-21)
|
||||||
|
Ingest + stay-current. **Layers 2 (archetypes) and 3 (projection) NOT built.**
|
||||||
|
Blueprint: **`docs/MECHANISM-DATA.md`** — the pattern every sport inherits.
|
||||||
|
|
||||||
|
## Phase 0 gate — both match rates 100%
|
||||||
|
- **Batters 40/40 · PITCHERS 66/66** (real rosters: CLE, DET, MIN, NYY, LAD),
|
||||||
|
joined by **MLBAM id** against the 713-pitcher Savant feed. Zero honest-absent
|
||||||
|
on identity — the join is an integer both systems use natively, and the
|
||||||
|
snapshot pipeline **already stores it** (`playerId`) per graded row.
|
||||||
|
- **pybaseball deliberately NOT used** — it is an MIT wrapper over the same CSV
|
||||||
|
URLs; adding it reintroduces a Python runtime in a stack where the existing
|
||||||
|
Python service is already offline. Direct axios + `savantAdapter`'s parser.
|
||||||
|
|
||||||
|
## What landed
|
||||||
|
`statcastAdapter` (6 free Savant feeds) → `statcastAggregateService` →
|
||||||
|
`statcast_aggregates` (migrations 030 + 031). **Live in prod:**
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Rows | **1,354** (604 batters · 750 pitchers) |
|
||||||
|
| Distinct players | 1,312 → **42 two-way players carry BOTH profiles** |
|
||||||
|
| Join rate | **1,354 / 1,354 · 0 unjoined** |
|
||||||
|
| Handedness | 677 pitchers (movement feed) |
|
||||||
|
| Sufficient / thin | 998 / 356 (PA≥50, IP≥10) |
|
||||||
|
| Arsenals | avg **4.4 pitches**, max 8, 679 with ≥3 |
|
||||||
|
| Table size | **5.3 MB** (vs 500 MB plan ceiling) |
|
||||||
|
| Pull time | ~5 s, all six feeds |
|
||||||
|
|
||||||
|
**Backfill and refresh are the SAME call** — full re-pull upserted on
|
||||||
|
`(sport, season, source_id, role)`. Idempotent + self-healing: **5 induced runs
|
||||||
|
→ still exactly 1,354 rows.** No incremental who-played bookkeeping to drift.
|
||||||
|
|
||||||
|
## 🔴 TWO REAL BUGS CAUGHT BY INDUCING, NOT BY REVIEW
|
||||||
|
1. **Two-way players broke the write.** PK `(sport, season, source_id)`
|
||||||
|
collapsed a player's batter and pitcher profiles into one key → one batch hit
|
||||||
|
the same row twice → `ON CONFLICT DO UPDATE command cannot affect row a
|
||||||
|
second time`, first chunk written, rest refused. **ROLE is in the key now**
|
||||||
|
(migration 031). Ohtani stores 419 PA / 17.2% barrel as a batter AND 85.2 IP
|
||||||
|
/ 33.7° arm angle / 52.1% GB as a pitcher — two real profiles, not one
|
||||||
|
invented player.
|
||||||
|
2. **Pitch mix was primary-pitch-only.** `pitch-movement` with an empty
|
||||||
|
`pitch_type` returns **ONE row per pitcher** (677 rows / ~700 pitchers), so a
|
||||||
|
five-pitch arsenal stored as one pitch — which would have classified every
|
||||||
|
pitcher as a one-pitch arm. Mix now comes from **`pitch-arsenal-stats`
|
||||||
|
(3,205 rows = pitcher × pitch)**; movement still supplies velo/break/
|
||||||
|
handedness folded onto the primary pitch. Skubal: 5 pitches, L, FF 96.7.
|
||||||
|
|
||||||
|
## Honesty rules (each has a test)
|
||||||
|
Absent metric → `null`, never 0 · thin sample **stored and flagged** (thin ≠
|
||||||
|
missing) · unjoined player stored with null `player_key`, joins later · **all
|
||||||
|
feeds empty → REFUSES to write** so a bad night can never blank a good table ·
|
||||||
|
`updated_at` on every row.
|
||||||
|
|
||||||
|
## Staleness — verified across simulated conditions
|
||||||
|
`0.1h → false · 26h → false · 50h → TRUE · 168h → TRUE · NEVER-BUILT → false`
|
||||||
|
(threshold 48h). **Never-built is deliberately NOT stale** — different
|
||||||
|
condition, different fix; paging on a fresh install teaches the operator to
|
||||||
|
ignore the alarm. The scheduler pages on a **failed run** AND on **silent
|
||||||
|
staleness** separately: a job that stops being scheduled never produces a
|
||||||
|
failure.
|
||||||
|
|
||||||
|
## Ops
|
||||||
|
Nightly at `STATCAST_HOUR_UTC` (default **11 UTC**, after every game is final);
|
||||||
|
kill switch `STATCAST=0`; gates `STATCAST_MIN_PA` / `MIN_IP` / `MAX_AGE_HOURS`.
|
||||||
|
**Induce:** `POST /api/internal/statcast/refresh` · **probe:**
|
||||||
|
`GET /api/internal/statcast/status`.
|
||||||
|
**⚠️ `/api/internal/*` is NOT reachable on `vyndr.app`** (no Next proxy, by
|
||||||
|
design) — use **`api.vyndr.app`**. Also: local `@supabase/supabase-js` egress
|
||||||
|
from the WSL box FAILS (`fetch failed`); the MCP tool works. Induce DB-writing
|
||||||
|
jobs on the server, not locally.
|
||||||
|
|
||||||
|
## Raw per-pitch — measured, and deliberately NOT stored
|
||||||
|
2026-07-19: **4,791 pitches · 119 cols · 3.26 MB · 680 B/row · 1.2 s** →
|
||||||
|
**~776k pitches/season = 503 MB CSV, 171 MB gzipped, ~0.85 GB in Postgres**
|
||||||
|
against a **500 MB free-plan ceiling** (DB is 26 MB today). Aggregate grain is
|
||||||
|
5.3 MB. Raw stays re-pullable from the free source if Layer 3 needs it; the
|
||||||
|
cold tier (Storage Box) is the two-tier home when it does.
|
||||||
|
|
||||||
|
## Commodity, not moat
|
||||||
|
Raw Statcast is public — anyone can pull it in a second. The edge is Layer 2
|
||||||
|
(which signals define an archetype, and where the boundaries sit), Layer 3
|
||||||
|
(projections), and the settled ledger that proves them. **Having the data is not
|
||||||
|
having an edge.**
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🟢 SESSION 67 — PRICE LAYER GATED + WIRED (2026-07-20)
|
# 🟢 SESSION 67 — PRICE LAYER GATED + WIRED (2026-07-20)
|
||||||
|
|||||||
Reference in New Issue
Block a user