Session 7e: Grade adapter, normalize consolidation, ARCH-2 banners

This commit is contained in:
Kev
2026-06-10 03:37:07 -04:00
parent 6f4a353de9
commit 012c0ef47e
11 changed files with 571 additions and 60 deletions
+204
View File
@@ -0,0 +1,204 @@
const {
toLegacyShape,
__internals: {
FOUR_LETTER_MAP,
fourLetterGrade,
legacyConfidence,
killConditionsFromFactors,
buildReasoningSummary,
legacyEdgePct,
},
} = require('../../src/utils/gradeAdapter');
describe('gradeAdapter — fourLetterGrade', () => {
test('A-tier (A+, A, A-) all map to "A"', () => {
expect(fourLetterGrade('A+')).toBe('A');
expect(fourLetterGrade('A')).toBe('A');
expect(fourLetterGrade('A-')).toBe('A');
});
test('B-tier maps to "B"', () => {
expect(fourLetterGrade('B+')).toBe('B');
expect(fourLetterGrade('B')).toBe('B');
expect(fourLetterGrade('B-')).toBe('B');
});
test('C-tier maps to "C"', () => {
expect(fourLetterGrade('C+')).toBe('C');
expect(fourLetterGrade('C')).toBe('C');
expect(fourLetterGrade('C-')).toBe('C');
});
test('D maps to "D", F maps to "F" (DemoScan handles F → "—")', () => {
expect(fourLetterGrade('D')).toBe('D');
expect(fourLetterGrade('F')).toBe('F');
});
test('unknown / null / non-string returns null', () => {
expect(fourLetterGrade('Z')).toBeNull();
expect(fourLetterGrade(null)).toBeNull();
expect(fourLetterGrade(undefined)).toBeNull();
expect(fourLetterGrade(42)).toBeNull();
});
test('FOUR_LETTER_MAP exposes every engine1 grade', () => {
const expected = ['A+','A','A-','B+','B','B-','C+','C','C-','D','F'];
for (const g of expected) expect(FOUR_LETTER_MAP[g]).toBeDefined();
});
});
describe('gradeAdapter — legacyConfidence', () => {
test('0.0 → 0, 0.5 → 50, 1.0 → 100', () => {
expect(legacyConfidence(0.0)).toBe(0);
expect(legacyConfidence(0.5)).toBe(50);
expect(legacyConfidence(1.0)).toBe(100);
});
test('0.873 → 87 (rounded)', () => {
expect(legacyConfidence(0.873)).toBe(87);
});
test('out-of-range clamps to [0, 100]', () => {
expect(legacyConfidence(-0.5)).toBe(0);
expect(legacyConfidence(2.0)).toBe(100);
});
test('non-numeric input returns null', () => {
expect(legacyConfidence(null)).toBeNull();
expect(legacyConfidence(undefined)).toBeNull();
expect(legacyConfidence('abc')).toBeNull();
});
});
describe('gradeAdapter — killConditionsFromFactors', () => {
test('translates known negative factors to kill condition entries', () => {
const out = killConditionsFromFactors(['trap_composite_high', 'l5_cold_vs_line']);
const codes = out.map((k) => k.code);
expect(codes).toContain('TRAP');
expect(codes).toContain('COLD_L5');
});
test('positive factors are ignored', () => {
const out = killConditionsFromFactors(['l5_hot_vs_line', 'home_game', 'rested_2plus']);
expect(out).toEqual([]);
});
test('deduplicates by code (l5_cold + l5_hot_under both → COLD_L5)', () => {
const out = killConditionsFromFactors(['l5_cold_vs_line', 'l5_hot_vs_under']);
expect(out).toHaveLength(1);
expect(out[0].code).toBe('COLD_L5');
});
test('empty / non-array → []', () => {
expect(killConditionsFromFactors([])).toEqual([]);
expect(killConditionsFromFactors(null)).toEqual([]);
expect(killConditionsFromFactors(undefined)).toEqual([]);
});
});
describe('gradeAdapter — buildReasoningSummary', () => {
test('uses summaryOverride when provided', () => {
const out = buildReasoningSummary({ grade: 'A' }, {}, 'fancy override');
expect(out).toBe('fancy override');
});
test('builds a sentence from top_factors', () => {
const out = buildReasoningSummary({
grade: 'A-',
top_factors: ['l5_hot_vs_line', 'home_game', 'rested_2plus'],
}, { player: 'X' });
expect(out).toContain('A-');
expect(out).toContain('l5_hot_vs_line');
expect(out).toContain('favoring the play');
});
test('falls back gracefully when no factors are surfaced', () => {
const out = buildReasoningSummary({ grade: 'B' });
expect(typeof out).toBe('string');
expect(out).toContain('B');
});
});
describe('gradeAdapter — legacyEdgePct', () => {
test('numeric override passes through', () => {
expect(legacyEdgePct(5.2)).toBe(5.2);
expect(legacyEdgePct(-3)).toBe(-3);
});
test('non-numeric returns 0 (legacy callers expect the field present)', () => {
expect(legacyEdgePct(null)).toBe(0);
expect(legacyEdgePct(undefined)).toBe(0);
expect(legacyEdgePct('abc')).toBe(0);
});
});
describe('gradeAdapter — toLegacyShape (round-trip)', () => {
const engine1Result = {
grade: 'A-',
confidence: 0.78,
top_factors: ['l5_hot_vs_line', 'opp_rank_stat', 'rested_2plus'],
all_factors: ['l5_hot_vs_line', 'opp_rank_stat', 'rested_2plus', 'home_game'],
};
const prop = {
player: 'Jalen Brunson',
stat_type: 'points',
line: 26.5,
direction: 'over',
book: 'draftkings',
sport: 'nba',
};
test('produces every field DemoScan consumes', () => {
const out = toLegacyShape(engine1Result, prop);
expect(out).toHaveProperty('grade', 'A');
expect(out).toHaveProperty('confidence', 78);
expect(out).toHaveProperty('reasoning.summary');
expect(typeof out.reasoning.summary).toBe('string');
expect(out).toHaveProperty('kill_conditions_triggered');
expect(Array.isArray(out.kill_conditions_triggered)).toBe(true);
expect(out).toHaveProperty('edge_pct');
expect(out).toHaveProperty('player', 'Jalen Brunson');
expect(out).toHaveProperty('stat_type', 'points');
expect(out).toHaveProperty('line', 26.5);
expect(out).toHaveProperty('direction', 'over');
expect(out).toHaveProperty('book', 'draftkings');
});
test('edgePct override flows through', () => {
const out = toLegacyShape(engine1Result, prop, { edgePct: 4.7 });
expect(out.edge_pct).toBe(4.7);
});
test('summaryOverride wins over the factor-based sentence', () => {
const out = toLegacyShape(engine1Result, prop, { summaryOverride: 'Hand-written take.' });
expect(out.reasoning.summary).toBe('Hand-written take.');
});
test('partial engine1 input does not crash', () => {
const partial = { grade: 'B+' };
const out = toLegacyShape(partial, prop);
expect(out.grade).toBe('B');
expect(out.confidence).toBeNull();
expect(out.kill_conditions_triggered).toEqual([]);
expect(typeof out.reasoning.summary).toBe('string');
});
test('null engine1 input returns null', () => {
expect(toLegacyShape(null, prop)).toBeNull();
expect(toLegacyShape(undefined, prop)).toBeNull();
});
test('engine1_factors travel through reasoning.steps for debugging', () => {
const out = toLegacyShape(engine1Result, prop);
expect(Array.isArray(out.reasoning.steps.engine1_factors)).toBe(true);
expect(out.reasoning.steps.engine1_factors).toEqual(engine1Result.all_factors);
expect(out.reasoning.steps.final_grade).toBe('A');
});
test('legacy-shape compatibility: every key DemoScan reads is present', () => {
const out = toLegacyShape(engine1Result, prop);
// DemoScan reads: grade, confidence, reasoning.summary,
// kill_conditions_triggered (with .code on each entry),
// implied_probability (optional — adapter doesn't add it but doesn't
// need to since the field is allowed to be undefined).
expect(out.grade).toBeDefined();
expect(out.confidence).toBeDefined();
expect(out.reasoning.summary).toBeDefined();
for (const kc of out.kill_conditions_triggered) {
expect(kc.code).toBeDefined();
expect(typeof kc.code).toBe('string');
}
});
});
+57
View File
@@ -0,0 +1,57 @@
const { normalizeName } = require('../../src/utils/normalize');
describe('normalizeName (DUP-1)', () => {
test('basic lowercase + trim', () => {
expect(normalizeName(' Jalen Brunson ')).toBe('jalen brunson');
});
test('strips accents via NFD', () => {
expect(normalizeName('Luka Dončić')).toBe('luka doncic');
});
test('drops suffixes jr/sr/ii/iii/iv/v', () => {
expect(normalizeName('Tim Hardaway Jr.')).toBe('tim hardaway');
expect(normalizeName('Wade Phillips Sr.')).toBe('wade phillips');
expect(normalizeName('Sammy Davis III')).toBe('sammy davis');
// VIII isn't in the suffix list (i, ii, iii, iv, v only) — it's
// realistically not a player-name suffix, so the regex leaves it.
expect(normalizeName('Henry VIII')).toBe('henry viii');
});
test('strips punctuation (default: keepDigits=false drops digits too)', () => {
expect(normalizeName('A.J. Brown')).toBe('a j brown');
expect(normalizeName("Shaq O'Neal #34")).toBe('shaq o neal');
});
test('keepDigits true preserves jersey numbers', () => {
expect(normalizeName("Shaq O'Neal #34", { keepDigits: true })).toBe('shaq o neal 34');
expect(normalizeName('Player 23', { keepDigits: true })).toBe('player 23');
expect(normalizeName('Player 23', { keepDigits: false })).toBe('player');
});
test('collapses runs of whitespace', () => {
expect(normalizeName('Spaced Out\tName')).toBe('spaced out name');
});
test('null / undefined / empty input returns empty string', () => {
expect(normalizeName(null)).toBe('');
expect(normalizeName(undefined)).toBe('');
expect(normalizeName('')).toBe('');
});
test('numeric input gets toStringed', () => {
expect(normalizeName(42, { keepDigits: true })).toBe('42');
});
test('idempotent — normalize(normalize(x)) === normalize(x)', () => {
const samples = ['Luka Dončić', 'Tim Hardaway Jr.', "A.J. Brown #11"];
for (const s of samples) {
const once = normalizeName(s);
expect(normalizeName(once)).toBe(once);
}
for (const s of samples) {
const once = normalizeName(s, { keepDigits: true });
expect(normalizeName(once, { keepDigits: true })).toBe(once);
}
});
});