9b9aab4262
The landing hero was a static Jokic "Example" card with a name-length pick and a
hardcoded A- 73% +6.2% fallback. Now it's deterministic and live:
- heroPropService.pickHeroProp reads the pre-graded snapshot and selects the
prop with the LARGEST |projection - line| gap among A/B grades (conviction,
not noise) — the read where VYNDR disagrees most with the market, the card
that makes a stranger argue. No curation, no grading (reads cache → no API
credits). GET /api/hero-prop (backend) + repointed Next proxy.
- The card shows the disagreement EXPLICITLY: the book's line vs VYNDR's model,
side by side (model in green), with the real grade timestamp ("Graded 2:14
PM"). The EXAMPLE chip is gone.
- Empty slate → the MOST RECENT real graded read (flagged "LATEST READ", real
date). Nothing cached → { available:false } and the card HIDES. No
hand-written fallback — the Jokic card is deleted. Survives a dead night: a
live rule shows tonight's real MLB read, never a phantom July NBA card.
7 service tests lock the rule (max-gap, A/B gate, projection/line required,
empty→recent, hidden, cross-sport). colorContract updated to the new
disagreement display. Change-affected suites green, web build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
162 lines
7.1 KiB
JavaScript
162 lines
7.1 KiB
JavaScript
// DS3 — THE COLOR CONTRACT (DESIGN-SPEC Part 1).
|
|
// The contract is enforced as failing tests: green means ONE thing (edge),
|
|
// edge/CLV is colored by SIGN, grades by TIER, glow is A/A+ only, and NO
|
|
// archetype hue may dilute the signal green. Extends the QA.20-22 discipline.
|
|
|
|
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');
|
|
|
|
const cc = require('../../web/src/lib/colorContract');
|
|
const tokens = require('../../web/src/lib/vyndrTokens');
|
|
const arch = require('../../web/src/lib/archetypes');
|
|
const svc = require('../../src/services/archetypeService');
|
|
|
|
describe('colorContract.edgeColor — edge/CLV/delta colored by SIGN (#3)', () => {
|
|
it('positive edge is signal-green', () => {
|
|
expect(cc.edgeColor(6.2)).toBe('var(--g-a)');
|
|
expect(cc.edgeColor('12')).toBe('var(--g-a)');
|
|
});
|
|
it('NEGATIVE edge is muted red — a -33.3% edge NEVER renders green', () => {
|
|
expect(cc.edgeColor(-33.3)).toBe('var(--miss)');
|
|
expect(cc.edgeColor(-0.1)).toBe('var(--miss)');
|
|
expect(cc.edgeColor('-5')).toBe('var(--miss)');
|
|
expect(cc.edgeColor(-33.3)).not.toBe('var(--g-a)');
|
|
});
|
|
it('zero / null / NaN is NO edge → neutral (never a fake green)', () => {
|
|
expect(cc.edgeColor(0)).toBe('var(--text-2)');
|
|
expect(cc.edgeColor(null)).toBe('var(--text-2)');
|
|
expect(cc.edgeColor(undefined)).toBe('var(--text-2)');
|
|
expect(cc.edgeColor('n/a')).toBe('var(--text-2)');
|
|
});
|
|
});
|
|
|
|
describe('colorContract.gradeTierColor — grades colored by TIER', () => {
|
|
it('A/A+ green, B blue, C amber, D/F red', () => {
|
|
expect(cc.gradeTierColor('A+')).toBe('var(--g-ap)');
|
|
expect(cc.gradeTierColor('A')).toBe('var(--g-a)');
|
|
expect(cc.gradeTierColor('A-')).toBe('var(--g-a)');
|
|
expect(cc.gradeTierColor('B')).toBe('var(--g-b)');
|
|
expect(cc.gradeTierColor('C')).toBe('var(--g-c)');
|
|
expect(cc.gradeTierColor('D')).toBe('var(--g-d)');
|
|
expect(cc.gradeTierColor('F')).toBe('var(--g-d)');
|
|
});
|
|
it('is case/whitespace tolerant, unknown → neutral', () => {
|
|
expect(cc.gradeTierColor(' a+ ')).toBe('var(--g-ap)');
|
|
expect(cc.gradeTierColor('???')).toBe('var(--text-0)');
|
|
});
|
|
it('stays in lockstep with vyndrTokens.gradeColor for shared keys', () => {
|
|
for (const g of ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C', 'D']) {
|
|
expect(cc.gradeTierColor(g)).toBe(tokens.gradeColor(g));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('colorContract.gradeGlows — GLOW = A/A+ ONLY (#4)', () => {
|
|
it('true for A and A+ only', () => {
|
|
expect(cc.gradeGlows('A+')).toBe(true);
|
|
expect(cc.gradeGlows('A')).toBe(true);
|
|
});
|
|
it('false for every non-A tier — a glowing C devalues the cue', () => {
|
|
for (const g of ['A-', 'B+', 'B', 'B-', 'C', 'D', 'F', '', null, undefined]) {
|
|
expect(cc.gradeGlows(g)).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('colorContract.deltaE / isSignalGreen — perceptual gate', () => {
|
|
it('deltaE of a color with itself is 0', () => {
|
|
expect(cc.deltaE('#00D4A0', '#00D4A0')).toBeCloseTo(0, 5);
|
|
});
|
|
it('flags the signal green itself and near-clones', () => {
|
|
expect(cc.isSignalGreen('#00D4A0')).toBe(true);
|
|
expect(cc.isSignalGreen('#34D399')).toBe(true); // the old MIRROR green
|
|
});
|
|
it('clears clearly-different hues (amber, blue, red)', () => {
|
|
expect(cc.isSignalGreen('#FFB347')).toBe(false);
|
|
expect(cc.isSignalGreen('#4A9EFF')).toBe(false);
|
|
expect(cc.isSignalGreen('#FF5252')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('ARCHETYPE DEDUP — no archetype hue dilutes the signal green (#15,§5)', () => {
|
|
it('every FRONTEND archetype color clears the signal-green ΔE gate', () => {
|
|
const offenders = [];
|
|
for (const [name, info] of Object.entries(arch.ARCHETYPE_MAP)) {
|
|
if (cc.isSignalGreen(info.c)) offenders.push(`${name} ${info.c} (ΔE ${cc.deltaE(info.c, cc.SIGNAL_GREEN).toFixed(1)})`);
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
it('every BACKEND archetype color clears the gate too', () => {
|
|
const offenders = [];
|
|
for (const [name, a] of Object.entries(svc.ARCHETYPES)) {
|
|
if (cc.isSignalGreen(a.color)) offenders.push(`${name} ${a.color}`);
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
it('NO archetype is the literal signal green #00D4A0 (case-insensitive)', () => {
|
|
const hits = Object.values(arch.ARCHETYPE_MAP).map((i) => i.c.toUpperCase()).filter((c) => c === '#00D4A0');
|
|
expect(hits).toEqual([]);
|
|
});
|
|
it('frontend + backend colors still agree after the shift', () => {
|
|
for (const [name, a] of Object.entries(svc.ARCHETYPES)) {
|
|
expect(arch.archetypeColor(name)).toBe(a.color);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---- SOURCE-GREP VIOLATION LOCKS -------------------------------------------
|
|
// These fail if the ad-hoc green-on-negative / non-A-glow patterns creep back.
|
|
|
|
describe('GradeResultCard.tsx — edge by sign, glow by tier', () => {
|
|
const src = read('components/vyndr/GradeResultCard.tsx');
|
|
it('routes edge coloring through edgeColor (never a hardcoded green on edge)', () => {
|
|
expect(src).toContain('import { edgeColor, gradeGlows }');
|
|
expect(src).toContain('color: edgeColor(d.edge)');
|
|
expect(src).toContain('edgeColor(d.edge) : ');
|
|
});
|
|
it('does NOT color an edge figure unconditionally green', () => {
|
|
// the old bug: a green span wrapping the edge %, agnostic to sign
|
|
expect(src).not.toMatch(/color: 'var\(--g-a\)' \}\}>\{d\.edge/);
|
|
expect(src).not.toMatch(/d\.edge != null \? 'var\(--g-a\)'/);
|
|
});
|
|
it('gates the grade-hero glow (textShadow) to A/A+ via gradeGlows', () => {
|
|
expect(src).toContain('gradeGlows(d.grade) ?');
|
|
// the ungated always-on glow must be gone
|
|
expect(src).not.toMatch(/textShadow: `0 0 28px \$\{hex\}aa, 0 0 60px \$\{hex\}55`,/);
|
|
});
|
|
});
|
|
|
|
describe('GradeBadge.tsx — glow gated to A-tier', () => {
|
|
const src = read('components/vyndr/GradeBadge.tsx');
|
|
it('boxShadow glow only when glow && isA (A/A+)', () => {
|
|
expect(src).toContain("const isA = grade === 'A+' || grade === 'A'");
|
|
expect(src).toMatch(/boxShadow: glow && isA \?/);
|
|
});
|
|
});
|
|
|
|
describe('LiveHeroProp.tsx — the disagreement display (item 5)', () => {
|
|
// The hero card now shows the book's line vs VYNDR's model side by side (the
|
|
// largest model-vs-market gap), not a signed edge %. Model = green (our
|
|
// number, the signal), book line = neutral. No static Jokic fallback.
|
|
const src = read('components/LiveHeroProp.tsx');
|
|
it('renders BOOK LINE vs VYNDR MODEL, model in green', () => {
|
|
expect(src).toContain('LINE'); // "{bookLabel} LINE"
|
|
expect(src).toContain('VYNDR MODEL');
|
|
expect(src).toMatch(/VYNDR MODEL[^]*?color: 'var\(--grade-a\)'/);
|
|
});
|
|
it('has no hand-written static fallback (no hardcoded Jokic example)', () => {
|
|
expect(src).not.toContain('Nikola Jokic');
|
|
expect(src).not.toContain('aria-label="Example grade"'); // the old EXAMPLE chip is gone
|
|
expect(src).not.toContain('26.5'); // the old hardcoded line
|
|
});
|
|
});
|
|
|
|
describe('VYNDR INTELLIGENCE panel — de-flooded (#15)', () => {
|
|
const src = read('components/vyndr/GradeResultCard.tsx');
|
|
it('the panel border is a neutral token, not a green wash', () => {
|
|
expect(src).not.toContain("border: '1px solid rgba(0,212,160,0.24)'");
|
|
});
|
|
});
|