From 016758e0142c1ebc167a0e13688f2d01d73ce23e Mon Sep 17 00:00:00 2001 From: Kev Date: Mon, 13 Jul 2026 16:07:33 -0400 Subject: [PATCH] Combat intelligence spec (Wave 6): honest free v1, pinned archetype registry Co-Authored-By: Claude Opus 4.8 (1M context) --- specs/combat-intelligence.md | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 specs/combat-intelligence.md diff --git a/specs/combat-intelligence.md b/specs/combat-intelligence.md new file mode 100644 index 0000000..05e9c7e --- /dev/null +++ b/specs/combat-intelligence.md @@ -0,0 +1,55 @@ +# VYNDR — COMBAT INTELLIGENCE LAYER (spec) +### Wave 6 of the wiring/data train. Net-new sport (MMA/UFC). Governs the combat build. Honest free v1; the full matchup-GRADE engine is a DEFERRED sub-wave. Build toward `specs/design-reference/vyndr-system.html` (FIGHTER A / VERDICT / FIGHTER B, GRAPPLER%/STRIKER% blend, tale-of-the-tape, MONEYLINE/DECISION/SUBMISSION/KO grades). Keep the live wordmark. + +## DATA SEMANTICS (unchanged, restated for combat) +VYNDR never generates odds — ML/round-total values are REAL book numbers at a timestamp. Fighter records/physicals/style stats are REAL sourced facts. Model output (style edge, any grade) is always labeled MODEL. `Number(null)===0` is the trap — absent stat ⇒ absent, never 0. **Never ingest a fighter's photo/likeness** (same rule as headshots) — initials monogram only. Scraping fragility ⇒ degrade to absent, never fabricate. + +## SCOPE — v1 (this wave) vs DEFERRED +**v1 ships:** fight-card discovery + tale-of-the-tape + style-blend archetypes + ML & round-total odds + a **style-edge VERDICT (a MODEL style read, explicitly NOT a settled grade)**. +**DEFERRED (own sub-wave, do NOT build now):** the matchup-GRADE engine that produces settled ML/method/round grades; method-of-victory & round & fighter props (data-limited on the free feed); combat outcome SETTLEMENT (no free box-score settle path yet — grades would stay `pending`); ufcstats.com scraping (needs a parser dep — CONFIRM before adding). + +## DATA SOURCES (zero-out-of-pocket) +- **ESPN MMA (FREE, JSON, no auth — same family VYNDR already uses):** + - Fight cards / schedule: `site.api.espn.com/apis/site/v2/sports/mma/ufc/scoreboard` (date-pinned like the other sports). + - Event / fight detail + results + tale-of-the-tape: `.../mma/ufc/summary?event={id}` and the athlete endpoints (`athlete.id`, record, stance, reach, weight class). ESPN athlete id also gives a headshot via the entity layer's `a.espncdn.com/i/headshots/mma/...` pattern — but per the likeness rule, v1 uses initials monograms; wire the id but default to monogram. + - Depth caveat (honest): ESPN MMA striking/grappling granularity is THINNER than ufcstats. Style-blend is best-effort from what ESPN exposes (finish history, method-of-victory counts, takedown/strike splits where present); when a stat is absent, the blend says less — never invents. +- **The Odds API `mma_mixed_martial_arts` (ALREADY PAID — `ODDS_API_KEY`):** wire `oddsService.SPORT_KEYS.mma = 'mma_mixed_martial_arts'` + `MMA_MARKETS = ['h2h','totals']` (moneyline + round totals). Reuse `oddsNormalizer`. **PropLine has NO combat** → odds-api-only; combat does NOT enter the abundant-props path. + +## COMBAT ARCHETYPE REGISTRY (PINNED — both `src/services/archetypeService.js` AND `web/src/lib/archetypes.js` use these EXACT names/colors/glyphs; a test asserts they match, same as other sports). Colors unique WITHIN combat; may reuse hues used in other sports (cross-sport reuse is fine). +Six styles. `classify('mma', fighterStats)` returns `{ primary, secondary|null, blend:[{archetype,weight}] }` (same contract as other sports) scored from finish-rate / method splits / takedown & strike tendencies. + +| Name | Glyph | Color | One-liner (shown where it leads) | +|---|---|---|---| +| STRIKER | ✦ | `#E8703A` | Wins on the feet — volume + power at range. | +| GRAPPLER | ⊗ | `#2FA4E7` | Fight hits the mat on his terms — control + subs. | +| PRESSURE | ➤ | `#E4574C` | Forward, relentless, breaks the pace. | +| COUNTER | ◊ | `#8E7BE0` | Patient — punishes what you show him. | +| FINISHER | ▲ | `#12B886` | Ends nights — high KO/SUB rate. | +| GRINDER | ▦ | `#B0883B` | Goes the distance, wins the rounds. | +STRIKER↔GRAPPLER is the primary range axis the mockup renders as the two blend bars; PRESSURE/COUNTER is tempo; FINISHER/GRINDER is the outcome tendency. A fighter is a BLEND (e.g. GRAPPLER 80% / STRIKER 30%). Discipline pedigree tags (Combat Sambo, Dagestan Wrestling, BJJ, Wrestling Base, Kickboxing, Muay Thai, Boxing) are **verifiable credentials**, rendered separately from the archetype blend — VERIFIABLE ONLY, mark unknowns absent, never guess a fighter's base. + +## STYLE-MATCHUP VERDICT (v1 — a MODEL read, not a grade) +`styleMatchup(fighterA, fighterB)` → a style-edge verdict (e.g. "GRAPPLER EDGE · Islam") from comparing the two blends + finish/defense tendencies. This is a **descriptive MODEL read**, labeled as such — NOT a settled grade, NOT an edge %, NO fabricated confidence. The mockup's CENTER VERDICT. Keep it honest: if the data is too thin to call, say "STYLES EVEN / INSUFFICIENT READ". + +## CONFIG WIRING (the recurring 4-layer-desync trap — do all in sync) +- `src/config/sports.js` `SPORT_CONFIG` + legacy `SPORTS` (flip `mma.active`), `web/src/config/sports.ts` mirror. +- `oddsService.SPORT_KEYS`/`SPORT_MARKETS` + `oddsNormalizer` MMA market keys. +- `src/config/statFilters.js` + `web/src/config/statFilters.ts` — combat stat categories (if any props surface). +- Combat is NOT added to `snapshotService.ACTIVE_SPORTS` in v1 (no settle path) — fight cards + odds + style card render on-read; grades stay OUT of the locked-snapshot loop until the deferred engine. + +## FRONTEND (toward the mockup) +- **Tale-of-the-tape / head-to-head style card** — `web/src/components/vyndr/FightCard.tsx`: FIGHTER A (initials monogram, name, record e.g. 26–1, stance, reach) · GRAPPLER%/STRIKER% blend bars · discipline pedigree tags · archetype chip(s) via the shared `ArchetypeBadge` · CENTER VERDICT (style edge) · FIGHTER B mirror · a grades/odds row (MONEYLINE + round total from the odds feed; method/KO/SUB shown as "— data-limited" honest placeholders, NOT fabricated). Two fighters side-by-side (NOT the player-strip row grammar). Mono data, no glitch on data. +- **MMA badge/color** — the `#D4AF37` combat token already exists in `shareCards/tokens.js`; add a frontend MMA sport token + `SportBadge` support. +- **Route** — `web/src/app/fight/[id]/page.tsx` (server wrapper + client card) and/or a combat surface on the schedule; add Next proxies for the ESPN-MMA read (S25 rule). Self-hide / honest empty (reuse `EmptyState`) off-season. +- Sportsbook links via the existing `bookLinks.js`/affiliate layer unchanged. + +## TESTS +- `archetypeService`/`archetypes.js` combat colors MATCH (extend the existing cross-file color test). +- `classify('mma', …)` blends correctly from fixtures; thin data → fewer/absent style claims (never fabricated). +- `styleMatchup` → honest "insufficient read" on thin data; a clear edge on divergent styles. +- combat adapter: defensive parse (ESPN shape → normalized fight card; unrecognized → empty, never throw); injectable, NO network. +- `oddsNormalizer` MMA market keys map (h2h/totals), else combat odds silently zero. +- FightCard self-hides / honest-empty; renders tale-of-the-tape mono; method/round shown as data-limited, not fabricated. + +## OUT OF SCOPE REMINDER +No settled grades, no method/round/props board, no fighter photos, no scraping dep — all DEFERRED and flagged in-UI as data-limited rather than promised. The cheapest honest v1 is the goal: styles + tape + ML/round-totals, beautifully rendered.