// 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/); }); });