Wave 6: Combat Intelligence Layer (honest free v1)

Net-new MMA/UFC vertical — fight-card discovery, tale-of-the-tape,
style-blend archetypes, ML + round-total odds, and a style-edge VERDICT
(a MODEL read, explicitly NOT a settled grade). Built to
specs/combat-intelligence.md.

Backend:
- combatAdapter: ESPN MMA scoreboard (date-pinned, free JSON) -> fight
  cards + tale-of-tape (record/weight class/rounds/ESPN athlete id);
  defensive parse (null on unknown shape, never throws); injectable
  fetchImpl + cache; pure normalizeCombatOdds (odds-api h2h/totals ->
  ML + round total, allow-listed books, best price). Number(null) guard.
- archetypeService: 6 pinned combat styles in a SEPARATE COMBAT_ARCHETYPES
  registry (FINISHER collides with soccer + its green trips the signal-
  green gate); classify('mma') blends range/tempo/outcome, honest-empty on
  thin data (no forced fallback); styleMatchup() honest verdict.
- oddsService: SPORT_KEYS.mma + MMA_MARKETS=['h2h','totals'] + SPORT_MARKETS
  (no spreads suffix). oddsNormalizer MARKET_MAP h2h/totals.
- config/sports.js + web mirror: mma.active=true (collectData stays false;
  NOT in the graded-props pipeline SPORT_CONFIG or snapshot/settle loop).
- routes/combat.js: GET /api/combat/:date + GET /api/fight/:id (public,
  cached, honest empty off-card) + Next proxies.

Frontend:
- FightCard: two-fighter tale-of-the-tape (initials monogram — no photos),
  GRAPPLER/STRIKER blend bars, discipline pedigree tags, shared
  ArchetypeBadge (sport="mma", unicode glyphs), CENTER VERDICT, ML +
  round-total real; method/round/KO = honest "data-limited", never
  fabricated. Self-hides on a non-two-fighter bout.
- /fight/[id] page (server wrapper + client), EmptyState off-season.
- MMA SportBadge token (#D4AF37); archetypes.js sport-aware resolution.

DEFERRED (per spec, NOT built): matchup-GRADE engine, method/round/props
board, combat settlement, ufcstats scraping.

Tests: +3 suites (31 tests) — combat archetype cross-file color/glyph
match, classify blends, styleMatchup honesty, adapter defensive parse +
odds normalize, FightCard honesty grep; extended oddsNormalizer +
sportMarkets. Full suite 253/253 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-13 16:58:22 -04:00
parent 016758e014
commit 54fa5853f5
22 changed files with 1525 additions and 18 deletions
+185
View File
@@ -0,0 +1,185 @@
// Wave 6 — combatAdapter defensive parse. Fixtures modeled on the REAL ESPN
// MMA scoreboard shape (site.api.espn.com/.../mma/ufc/scoreboard) captured
// live during the build. NO network — fetchImpl + cache are injected.
const combat = require('../../src/services/adapters/combatAdapter');
// A trimmed but structurally-faithful ESPN MMA scoreboard payload: one UFC
// event ("card") with two bouts. Athlete ids live in the player-card link href.
const ESPN_FIXTURE = {
events: [
{
id: '600059599',
name: 'UFC Fight Night: Du Plessis vs. Usman',
shortName: 'UFC Fight Night',
date: '2026-07-18T21:00Z',
competitions: [
{
id: '1',
type: { id: '1007', abbreviation: 'W Flyweight', text: "Women's Flyweight" },
format: { regulation: { periods: 5 } },
venue: { fullName: 'UFC APEX' },
status: { type: { state: 'pre', completed: false } },
competitors: [
{
id: '10', order: 0, winner: false,
athlete: {
fullName: 'Dricus du Plessis', displayName: 'Dricus du Plessis', shortName: 'D. du Plessis',
links: [{ href: 'https://www.espn.com/mma/fighter/_/id/4801725/dricus-du-plessis' }],
},
records: [{ name: 'overall', type: 'total', summary: '22-2-0' }],
},
{
id: '11', order: 1, winner: false,
athlete: {
fullName: 'Kamaru Usman', displayName: 'Kamaru Usman', shortName: 'K. Usman',
links: [{ href: 'https://www.espn.com/mma/fighter/_/id/3088843/kamaru-usman' }],
},
records: [{ name: 'overall', type: 'total', summary: '20-4-0' }],
},
],
},
{
id: '2',
type: { abbreviation: 'Lightweight' },
format: { regulation: { periods: 3 } },
competitors: [
{ id: '20', order: 0, athlete: { displayName: 'Fighter A', links: [] }, records: [{ type: 'total', summary: '10-0' }] },
{ id: '21', order: 1, athlete: { displayName: 'Fighter B', links: [] }, records: [{ type: 'total', summary: '8-3' }] },
],
},
],
},
],
};
describe('combatAdapter.normalizeScoreboard — ESPN shape → fight cards', () => {
it('normalizes a real-shaped payload into a card with bouts + tale-of-tape', () => {
const { events } = combat.normalizeScoreboard(ESPN_FIXTURE);
expect(events).toHaveLength(1);
const card = events[0];
expect(card.id).toBe('600059599');
expect(card.name).toMatch(/Du Plessis/);
expect(card.bouts).toHaveLength(2);
const bout = card.bouts[0];
expect(bout.weightClass).toBe("Women's Flyweight");
expect(bout.rounds).toBe(5);
expect(bout.fighters).toHaveLength(2);
const a = bout.fighters[0];
expect(a.name).toBe('Dricus du Plessis');
expect(a.id).toBe('4801725'); // parsed from the link href
expect(a.record.wins).toBe(22);
expect(a.record.losses).toBe(2);
expect(a.record.display).toBe('222'); // en-dash display form
// Physicals absent from the free feed → null, NEVER fabricated 0.
expect(a.stance).toBeNull();
expect(a.reach).toBeNull();
});
it('date-pins defensively — an off-date event is dropped', () => {
const onDate = combat.normalizeScoreboard(ESPN_FIXTURE, { date: '2026-07-18' });
expect(onDate.events).toHaveLength(1);
const offDate = combat.normalizeScoreboard(ESPN_FIXTURE, { date: '2026-01-01' });
expect(offDate.events).toHaveLength(0);
});
it('DEFENSIVE: unrecognized/garbage shapes return empty, never throw', () => {
expect(() => combat.normalizeScoreboard(null)).not.toThrow();
expect(combat.normalizeScoreboard(null).events).toEqual([]);
expect(combat.normalizeScoreboard({}).events).toEqual([]);
expect(combat.normalizeScoreboard({ events: 'nope' }).events).toEqual([]);
// An identifiable event with only unusable bouts is kept with empty bouts.
expect(combat.normalizeScoreboard({ events: [{ id: 'x', competitions: [{ competitors: [{}] }] }] }).events[0].bouts).toEqual([]);
// An event with no id AND no usable bouts is dropped entirely.
expect(combat.normalizeScoreboard({ events: [{ competitions: [{ competitors: [{}] }] }] }).events).toEqual([]);
});
it('numOrNull never coerces null/empty to 0 (the fabrication trap)', () => {
expect(combat.numOrNull(null)).toBeNull();
expect(combat.numOrNull('')).toBeNull();
expect(combat.numOrNull(undefined)).toBeNull();
expect(combat.numOrNull('5')).toBe(5);
expect(combat.numOrNull(0)).toBe(0);
});
it('parseAthleteId is defensive on bad links', () => {
expect(combat.parseAthleteId(null)).toBeNull();
expect(combat.parseAthleteId([{ href: 'no-id-here' }])).toBeNull();
expect(combat.parseAthleteId([{ href: '/mma/fighter/_/id/999/x' }])).toBe('999');
});
});
describe('combatAdapter.getFightCards — injectable, no network', () => {
it('fetches via injected fetchImpl + normalizes (cache stubbed)', async () => {
const calls = [];
const res = await combat.getFightCards('2026-07-18', {
fetchImpl: async (url) => { calls.push(url); return ESPN_FIXTURE; },
cacheGet: async () => null,
cacheSet: async () => true,
});
expect(res.events).toHaveLength(1);
expect(calls[0]).toMatch(/dates=20260718/);
});
it('returns honest empty on a fetch error, never throws', async () => {
const res = await combat.getFightCards('2026-07-18', {
fetchImpl: async () => { throw new Error('network'); },
cacheGet: async () => null,
cacheSet: async () => true,
});
expect(res.events).toEqual([]);
});
});
describe('combatAdapter.normalizeCombatOdds — odds-api MMA → ML + round total', () => {
const ODDS_FIXTURE = [
{
id: 'evt1', commence_time: '2026-07-18T21:00Z',
home_team: 'Dricus du Plessis', away_team: 'Kamaru Usman',
bookmakers: [
{
key: 'draftkings',
markets: [
{ key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -230 }, { name: 'Kamaru Usman', price: 190 }] },
{ key: 'totals', outcomes: [{ name: 'Over', price: -110, point: 2.5 }, { name: 'Under', price: -110, point: 2.5 }] },
],
},
{
key: 'fanduel',
markets: [{ key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -215 }, { name: 'Kamaru Usman', price: 200 }] }],
},
// A non-allow-listed book must be ignored.
{ key: 'bovada', markets: [{ key: 'h2h', outcomes: [{ name: 'Dricus du Plessis', price: -999 }] }] },
],
},
];
it('maps h2h to per-fighter moneyline (best price) + totals to round total', () => {
const map = combat.normalizeCombatOdds(ODDS_FIXTURE);
const rec = map['dricus du plessis|kamaru usman'];
expect(rec).toBeDefined();
expect(rec.moneyline.home).toBe(-215); // best (higher) of -230 / -215
expect(rec.moneyline.away).toBe(200); // best of 190 / 200
expect(rec.roundTotal.line).toBe(2.5);
expect(rec.roundTotal.over).toBe(-110);
});
it('ignores non-allow-listed books (bovada never leaks a price)', () => {
const map = combat.normalizeCombatOdds(ODDS_FIXTURE);
expect(map['dricus du plessis|kamaru usman'].moneyline.home).not.toBe(-999);
});
it('matchBoutOdds joins in either name orientation', () => {
const map = combat.normalizeCombatOdds(ODDS_FIXTURE);
const bout = { fighters: [{ name: 'Kamaru Usman' }, { name: 'Dricus du Plessis' }] };
expect(combat.matchBoutOdds(bout, map)).toBeTruthy();
});
it('DEFENSIVE: garbage odds input returns {}, never throws', () => {
expect(() => combat.normalizeCombatOdds(null)).not.toThrow();
expect(combat.normalizeCombatOdds(null)).toEqual({});
expect(combat.normalizeCombatOdds([{ bookmakers: 'x' }])).toEqual({});
});
});
+108
View File
@@ -0,0 +1,108 @@
// Wave 6 — combat intelligence: archetype registry cross-file match,
// classify('mma') blends from fixtures (thin data → fewer claims, never
// fabricated), and styleMatchup honesty. NO network.
const svc = require('../../src/services/archetypeService');
const arch = require('../../web/src/lib/archetypes');
describe('combat archetype registry — pinned + cross-file agreement', () => {
const PINNED = {
STRIKER: { color: '#E8703A', glyph: '✦' },
GRAPPLER: { color: '#2FA4E7', glyph: '⊗' },
PRESSURE: { color: '#E4574C', glyph: '➤' },
COUNTER: { color: '#8E7BE0', glyph: '◊' },
FINISHER: { color: '#12B886', glyph: '▲' },
GRINDER: { color: '#B0883B', glyph: '▦' },
};
it('backend COMBAT_ARCHETYPES carries exactly the six pinned styles', () => {
expect(Object.keys(svc.COMBAT_ARCHETYPES).sort()).toEqual(Object.keys(PINNED).sort());
});
it('backend colors + glyphs match the pinned spec', () => {
for (const [name, p] of Object.entries(PINNED)) {
expect(svc.COMBAT_ARCHETYPES[name].color).toBe(p.color);
expect(svc.COMBAT_ARCHETYPES[name].glyph).toBe(p.glyph);
}
});
it('frontend COMBAT_ARCHETYPE_MAP colors + glyph chars MATCH the backend', () => {
for (const [name, a] of Object.entries(svc.COMBAT_ARCHETYPES)) {
const front = arch.COMBAT_ARCHETYPE_MAP[name];
expect(front).toBeDefined();
expect(front.c).toBe(a.color);
expect(front.char).toBe(a.glyph);
}
// No extra frontend combat archetypes beyond the pinned six.
expect(Object.keys(arch.COMBAT_ARCHETYPE_MAP).sort()).toEqual(Object.keys(svc.COMBAT_ARCHETYPES).sort());
});
it('combat FINISHER is namespaced — it does NOT collide with the soccer FINISHER', () => {
// Soccer FINISHER stays #FF5C5C in the shared map; combat FINISHER is #12B886.
expect(arch.ARCHETYPE_MAP.FINISHER.c).toBe('#FF5C5C');
expect(arch.combatArchetypeColor('FINISHER')).toBe('#12B886');
// sport-aware resolution keeps them apart:
expect(arch.archetypeColor('FINISHER')).toBe('#FF5C5C'); // no sport → soccer
expect(arch.archetypeColor('FINISHER', 'mma')).toBe('#12B886'); // mma → combat
});
});
describe("classify('mma', …) — blends from fixtures", () => {
it('a high-volume distance striker profiles STRIKER-primary', () => {
const r = svc.classify('mma', { slpm: 6, sapm: 3, strAcc: 0.55, strDef: 0.62, tdAvg: 0.2, koRate: 0.6, decRate: 0.3 });
expect(r.primary && r.primary.name).toBe('STRIKER');
expect(r.blend.length).toBeGreaterThan(0);
expect(r.blend.map((b) => b.archetype)).toContain('STRIKER');
});
it('a takedown-heavy submission threat profiles GRAPPLER-primary', () => {
const r = svc.classify('mma', { tdAvg: 4.5, subAvg: 1.8, slpm: 2.5, sapm: 2, strDef: 0.5, koRate: 0.1, subRate: 0.5, decRate: 0.4 });
expect(r.primary && r.primary.name).toBe('GRAPPLER');
});
it('a high-finish record profiles FINISHER via method rates', () => {
const r = svc.classify('mma', { koWins: 10, subWins: 5, decWins: 1, totalWins: 16 });
expect(r.blend.map((b) => b.archetype)).toContain('FINISHER');
});
it('THIN data yields NO fabricated style — empty blend, null primary (honest)', () => {
const r = svc.classify('mma', { record: '9-4-0' }); // no strike/td/method inputs
expect(r.blend).toEqual([]);
expect(r.primary).toBeNull();
expect(r.secondary).toBeNull();
});
it('an absent stat never scores an axis (Number(null) === 0 guard)', () => {
const r = svc.classify('mma', { slpm: null, tdAvg: undefined });
expect(r.blend).toEqual([]);
});
});
describe('styleMatchup — honest MODEL read, never a fabricated grade', () => {
const striker = svc.classify('mma', { slpm: 6, sapm: 3, strAcc: 0.55, tdAvg: 0.2, koRate: 0.6 });
const grappler = svc.classify('mma', { tdAvg: 4.5, subAvg: 1.8, slpm: 2.5, subRate: 0.5, decRate: 0.4 });
it('divergent styles produce a clear edge to one side (no confidence %)', () => {
const v = svc.styleMatchup(striker, grappler);
expect(v.verdict).toMatch(/EDGE$/);
expect(['a', 'b']).toContain(v.edgeSide);
expect(v).not.toHaveProperty('edge'); // no fabricated edge %
expect(v).not.toHaveProperty('confidence'); // no fabricated confidence
});
it('thin data on either side → INSUFFICIENT READ', () => {
expect(svc.styleMatchup(striker, svc.classify('mma', {})).verdict).toBe('INSUFFICIENT READ');
expect(svc.styleMatchup([], grappler).verdict).toBe('INSUFFICIENT READ');
});
it('two near-identical style blends → STYLES EVEN, no edge side', () => {
const v = svc.styleMatchup(striker, striker);
expect(v.verdict).toBe('STYLES EVEN');
expect(v.edgeSide).toBeNull();
});
it('accepts raw blend arrays as well as classify() results', () => {
const v = svc.styleMatchup(striker.blend, grappler.blend);
expect(v.verdict).toMatch(/EDGE$|EVEN|INSUFFICIENT/);
});
});
+50
View File
@@ -0,0 +1,50 @@
// Wave 6 — FightCard honesty locks (source-grep, same discipline as
// colorContract.test.js). The card must: self-hide on a non-two-fighter bout,
// render tale-of-the-tape as MONO data, label the verdict a MODEL read, and
// show method/round/KO as honest "data-limited" — never a fabricated grade.
const fs = require('fs');
const path = require('path');
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
describe('FightCard.tsx — honest v1 tale-of-the-tape', () => {
const src = read('components/vyndr/FightCard.tsx');
it('self-hides (returns null) when there arent two named fighters', () => {
expect(src).toMatch(/fighters\.length < 2/);
expect(src).toMatch(/return null/);
});
it('data rows are MONO, and no glitch CLASS is applied to any element (data never glitches)', () => {
expect(src).toContain('className="mono"');
// No glitch animation class on any element (comments about "never glitches" are fine).
expect(src).not.toMatch(/className=["'`][^"'`]*glitch/);
});
it('method / round / KO cells are shown as honest "data-limited", not fabricated', () => {
expect(src).toMatch(/dataLimited/);
expect(src).toContain('data-limited');
expect(src).toMatch(/METHOD/);
});
it('the verdict is explicitly labeled a MODEL read, not a settled grade', () => {
expect(src).toContain('MODEL READ');
expect(src).toContain('INSUFFICIENT READ');
});
it('uses a monogram (no fighter photo / likeness)', () => {
expect(src).toContain('Monogram');
expect(src).not.toMatch(/headshot|espncdn.*headshots|<img/i);
});
it('renders the archetype chip through the shared ArchetypeBadge with sport="mma"', () => {
expect(src).toContain('ArchetypeBadge');
expect(src).toMatch(/sport="mma"/);
});
it('absent odds render as a dash, never a fabricated number', () => {
expect(src).toMatch(/const DASH = '—'/);
expect(src).toMatch(/Number\.isFinite/);
});
});
+10
View File
@@ -244,4 +244,14 @@ describe('oddsNormalizer', () => {
expect(result[0].away_team).toBe('PHX');
});
});
// Wave 6 — combat (MMA) game-level markets. Without these MARKET_MAP keys,
// combat moneyline/round-total odds would silently normalize to zero (same
// silent-failure class as the MLB/NHL gaps closed earlier).
describe('MMA / combat market keys (Wave 6)', () => {
it('maps h2h → moneyline and totals → round_total', () => {
expect(MARKET_MAP.h2h).toBe('moneyline');
expect(MARKET_MAP.totals).toBe('round_total');
});
});
});
+11 -2
View File
@@ -64,14 +64,23 @@ describe('SPORT_MARKETS — isolation', () => {
expect(wc).not.toMatch(/batter_/);
});
test('every market list ends with `spreads`', () => {
for (const list of Object.values(SPORT_MARKETS)) {
test('every PLAYER-PROP market list ends with `spreads`', () => {
for (const [sport, list] of Object.entries(SPORT_MARKETS)) {
// Wave 6 — MMA is a GAME-level sport (h2h + round totals only); it has
// no player-prop `spreads` market and odds-api 422s if one is sent.
if (sport === 'mma') continue;
// We don't require spreads to be the literal final segment,
// only that it's present in the comma-separated list.
expect(list.split(',')).toContain('spreads');
}
});
test('MMA market list is game-level (h2h + totals), no spreads/player props', () => {
expect(SPORT_MARKETS.mma).toBe('h2h,totals');
expect(SPORT_MARKETS.mma).not.toMatch(/spreads/);
expect(SPORT_MARKETS.mma).not.toMatch(/player_/);
});
test('SPORT_MARKETS is frozen at the top level', () => {
expect(Object.isFrozen(SPORT_MARKETS)).toBe(true);
});