0997334f8b
THE PROMOTION WAS NOT PERFORMED. Champion grade path byte-identical (diff empty
across intelligence/, gradeSlateService, snapshotService). Projection, p_win and
the CLV instrument untouched.
REVIEW ZERO IS A GATE AND THREE OF FOUR PREREQUISITES FAIL:
0.1 scores are ESTIMATED priors from the founding spec, not measured. The
premise's cited values are not in the code either — the module holds
nba:points .80 and mlb:total_bases .55; there is no NBA 0.72 and no WNBA
score at all.
0.2 VERSION-BOUNDARY TAG DID NOT LAND — config/modelEras.js has zero shading
references. It was deliberately not applied twice (nothing had been
promoted) and reported both times. The order's own rule says STOP.
0.3 NO ROLLBACK FLAG EXISTS — zero occurrences of SHADING_ENABLED /
EDGE_SHADING / shadingEnabled anywhere in src/.
0.4 takeable tags DID land (migration 034, 1246/1254 rows). PASS.
AND THE APPROVED DELTA DOES NOT MATCH THE MEASURED ONE. Approved: 43.6% of
grades re-letter, efficient markets tighten and soft hold. Measured on all 1250
live rows: 97.4% change (1217), 79.8% move UP, 17.6% down, resulting in 79.0%
A-family (MLB 93.4%) against the champion's 0.2%. And rows_actually_shaded = 0
of 1250 — 96.5% of markets are unscored (f=1) and the one scored market present
is the anchor (f=1.0 by construction). The entire re-letter comes from switching
to edge-vs-fixed-bar grading, NOT from efficiency shading, which is inert on
this board. That is an unapproved grading-basis change riding along, which the
order's own "no new scaling changes riding along" guardrail forbids.
Flipping would re-letter 97.4% of an append-only public record, move 79.8% of
grades UP and mint A's on 79% of the board, on a letter whose measured
correlation with outcomes is r ~ 0.005 — the exact scenario the permanent
founder ruling forbids.
SHIPPED — ORDER B (independent of the promotion, and a live falsehood):
edge_pct display retired from GradeResultCard (confidence strip, EDGE stat cell
now honest-absent, alt-ladder rung) and SoccerGradeResult. DeskShowcase kept
(already honest). Computation and the board's signed-edge sort fallback SURVIVE
— deleting them would re-break the sort fixed on 2026-07-29; a test asserts all
three survive and the sort still orders agrees -> disagrees -> absent.
Fixed two build-breakers the retirement caused (orphaned edgeColor import,
orphaned edge_pct destructure; edge_pct stays on the props contract). Two
pre-existing tests superseded rather than deleted: they asserted the edge figure
is sign-coloured, and now assert the stronger property that no edge percentage
renders at all.
Floor: 314 suites / 3908 tests green (9 new), web build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
353 lines
12 KiB
TypeScript
353 lines
12 KiB
TypeScript
'use client';
|
||
|
||
import { useMemo } from 'react';
|
||
|
||
/**
|
||
* Soccer result card — renders an analyze/prop response with
|
||
* soccer-specific visual treatment. We can't surface raw feature
|
||
* values (the backend response carries only `reasoning.summary` +
|
||
* `kill_conditions_triggered` per the engine1 → legacy adapter), so
|
||
* we parse the summary for known soccer-signal phrases and surface
|
||
* each as a colored chip above the prose.
|
||
*
|
||
* Free-tier responses already arrive gated (the Session 7h
|
||
* `applyTierGating` redacts `reasoning` and `kill_conditions`); we
|
||
* just need to detect the `tier_gated` / `locked` markers and show
|
||
* an upgrade CTA over the blurred content.
|
||
*/
|
||
|
||
interface KillCondition {
|
||
code: string;
|
||
reason: string;
|
||
locked?: boolean;
|
||
}
|
||
|
||
interface Reasoning {
|
||
summary?: string;
|
||
steps?: unknown;
|
||
locked?: boolean;
|
||
}
|
||
|
||
export interface SoccerGradeResultProps {
|
||
player: string;
|
||
stat_type: string;
|
||
line: number;
|
||
direction: 'over' | 'under';
|
||
league: string;
|
||
grade: string;
|
||
confidence?: number;
|
||
edge_pct?: number;
|
||
reasoning?: Reasoning;
|
||
kill_conditions_triggered?: KillCondition[];
|
||
tier_gated?: boolean;
|
||
upgrade_hint?: string;
|
||
onUpgradeClick?: () => void;
|
||
onClose?: () => void;
|
||
}
|
||
|
||
type SignalTone = 'positive' | 'caution' | 'warning' | 'neutral';
|
||
|
||
interface ParsedSignal {
|
||
icon: string;
|
||
label: string;
|
||
detail: string;
|
||
tone: SignalTone;
|
||
}
|
||
|
||
const SIGNAL_TONE_STYLE: Record<SignalTone, { color: string; bg: string; border: string }> = {
|
||
positive: { color: 'var(--grade-a)', bg: 'rgba(0,200,150,0.08)', border: 'rgba(0,200,150,0.40)' },
|
||
caution: { color: 'var(--grade-c, #B8BCC8)', bg: 'rgba(255,179,71,0.08)', border: 'rgba(255,179,71,0.40)' },
|
||
warning: { color: 'var(--grade-d, #ff5a5a)', bg: 'rgba(255,90,90,0.08)', border: 'rgba(255,90,90,0.40)' },
|
||
neutral: { color: 'var(--text-secondary)', bg: 'transparent', border: 'var(--border)' },
|
||
};
|
||
|
||
// Pattern-match the concrete sentences `buildSoccerReasoningLines`
|
||
// emits in src/services/intelligence/analyzeViaEngine1.js. Order
|
||
// matters — earlier patterns win when multiple match the same line.
|
||
const SIGNAL_PATTERNS: Array<(line: string) => ParsedSignal | null> = [
|
||
(line) => {
|
||
const m = line.match(/scores ([\d.]+) goals per 90 minutes/i);
|
||
if (m) return { icon: '⚽', label: 'Goals / 90', detail: `${m[1]}`, tone: 'positive' };
|
||
return null;
|
||
},
|
||
(line) => {
|
||
const m = line.match(/Expected goals \(xG\): ([\d.]+) per 90 — (.+)/i);
|
||
if (m) {
|
||
const trend = m[2].toLowerCase();
|
||
const tone: SignalTone = trend.includes('regression') ? 'caution'
|
||
: trend.includes('breakout') ? 'positive' : 'neutral';
|
||
return { icon: '📊', label: 'xG / 90', detail: `${m[1]} — ${m[2]}`, tone };
|
||
}
|
||
return null;
|
||
},
|
||
(line) => {
|
||
if (/Designated penalty taker/i.test(line)) {
|
||
return { icon: '🎯', label: 'Penalty Taker', detail: '+0.15 goals/90 boost', tone: 'positive' };
|
||
}
|
||
return null;
|
||
},
|
||
(line) => {
|
||
if (/Direct free-kick specialist/i.test(line)) {
|
||
return { icon: '🏹', label: 'Free-Kick Taker', detail: 'shot/goal probability boost', tone: 'positive' };
|
||
}
|
||
return null;
|
||
},
|
||
(line) => {
|
||
if (/corner taker/i.test(line)) {
|
||
return { icon: '⛳', label: 'Corner Taker', detail: 'assist probability boost', tone: 'positive' };
|
||
}
|
||
return null;
|
||
},
|
||
(line) => {
|
||
const m = line.match(/Match at ([\d,]+)ft altitude\.\s*(.+)/i);
|
||
if (m) {
|
||
const isAcclimated = /acclimated host/i.test(m[2]);
|
||
return {
|
||
icon: '🏔️',
|
||
label: 'Altitude',
|
||
detail: `${m[1]}ft — ${isAcclimated ? 'host acclimated' : 'visitor risk'}`,
|
||
tone: isAcclimated ? 'neutral' : 'warning',
|
||
};
|
||
}
|
||
return null;
|
||
},
|
||
(line) => {
|
||
const m = line.match(/(.+?) averages ([\d.]+) cards per match/i);
|
||
if (m) {
|
||
const cardsPerGame = parseFloat(m[2]);
|
||
const tone: SignalTone = cardsPerGame >= 5 ? 'caution' : 'neutral';
|
||
return { icon: '🟨', label: `Referee: ${m[1].trim()}`, detail: `${m[2]} cards/match`, tone };
|
||
}
|
||
return null;
|
||
},
|
||
(line) => {
|
||
const m = line.match(/Averaging only ([\d.]+) minutes per match/i);
|
||
if (m) return { icon: '⏱️', label: 'Minutes', detail: `${m[1]}/90 — under-line discount`, tone: 'caution' };
|
||
return null;
|
||
},
|
||
(line) => {
|
||
const m = line.match(/(.+?) concedes ([\d.]+) goals per game/i);
|
||
if (m) {
|
||
const conceded = parseFloat(m[2]);
|
||
const tone: SignalTone = conceded <= 0.8 ? 'warning' : conceded >= 1.6 ? 'positive' : 'neutral';
|
||
return { icon: '🛡️', label: `Defense: ${m[1].trim()}`, detail: `${m[2]} GA/match`, tone };
|
||
}
|
||
return null;
|
||
},
|
||
(line) => {
|
||
const m = line.match(/Tournament pedigree: (\d+) career World Cup goals/i);
|
||
if (m) return { icon: '🏆', label: 'WC Pedigree', detail: `${m[1]} career goals`, tone: 'positive' };
|
||
return null;
|
||
},
|
||
];
|
||
|
||
function parseSignals(summary: string | undefined): ParsedSignal[] {
|
||
if (!summary) return [];
|
||
const out: ParsedSignal[] = [];
|
||
// The buildSoccerReasoningLines output is a single `lines.join(' ')`,
|
||
// so split on period+space and trim. Some sentences contain periods
|
||
// (e.g. "0.67 goals per 90"), so re-match conservatively.
|
||
const fragments = summary.split(/(?<=\.)\s+(?=[A-Z⚽📊🎯🏹⛳🏔️🟨⏱️🛡️🏆])/);
|
||
for (const frag of fragments) {
|
||
for (const fn of SIGNAL_PATTERNS) {
|
||
const sig = fn(frag);
|
||
if (sig) {
|
||
out.push(sig);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function gradeColor(grade: string): string {
|
||
const g = (grade || '').trim().toUpperCase().charAt(0);
|
||
if (g === 'A') return 'var(--grade-a)';
|
||
if (g === 'B') return 'var(--grade-b, #F0F0F0)';
|
||
if (g === 'C') return 'var(--grade-c, #B8BCC8)';
|
||
return 'var(--grade-d, #ff5a5a)';
|
||
}
|
||
|
||
export default function SoccerGradeResult(props: SoccerGradeResultProps) {
|
||
const {
|
||
// ORDER B: `edge_pct` stays on the props CONTRACT (callers still pass it and the
|
||
// backend still computes it) but is no longer destructured for display.
|
||
player, stat_type, line, direction, league, grade, confidence,
|
||
reasoning, kill_conditions_triggered, tier_gated, upgrade_hint,
|
||
onUpgradeClick, onClose,
|
||
} = props;
|
||
|
||
const signals = useMemo(() => parseSignals(reasoning?.summary), [reasoning?.summary]);
|
||
const color = gradeColor(grade);
|
||
const locked = !!tier_gated || !!reasoning?.locked;
|
||
const kills = Array.isArray(kill_conditions_triggered) ? kill_conditions_triggered : [];
|
||
|
||
return (
|
||
<article
|
||
className="surface diagonal-cut"
|
||
style={{
|
||
padding: 24,
|
||
border: `1px solid ${color}`,
|
||
background: 'var(--bg-elevated)',
|
||
borderRadius: 8,
|
||
marginTop: 16,
|
||
position: 'relative',
|
||
}}
|
||
data-testid="soccer-grade-result"
|
||
>
|
||
{onClose && (
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
aria-label="Close"
|
||
style={{
|
||
position: 'absolute', top: 8, right: 12,
|
||
background: 'transparent', border: 0, cursor: 'pointer',
|
||
fontSize: 18, color: 'var(--text-tertiary)',
|
||
}}
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
|
||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, marginBottom: 16 }}>
|
||
<div>
|
||
<div style={{ fontSize: 18, fontWeight: 700 }}>{player}</div>
|
||
<div
|
||
className="mono"
|
||
style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4, textTransform: 'uppercase', letterSpacing: '0.06em' }}
|
||
>
|
||
{direction.toUpperCase()} {line.toFixed(1)} {stat_type.replace(/_/g, ' ')} · {league.toUpperCase()}
|
||
</div>
|
||
</div>
|
||
<div style={{ textAlign: 'right' }}>
|
||
<div className="mono" style={{ fontSize: 44, fontWeight: 800, color, lineHeight: 1, letterSpacing: '-0.04em' }}>
|
||
{grade}
|
||
</div>
|
||
{typeof confidence === 'number' && (
|
||
<div className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', marginTop: 4 }}>
|
||
{confidence.toFixed(0)}% conf
|
||
{/* ORDER B (2026-07-31) — "% edge" retired. edge_pct is price-free and
|
||
line-scaled; calling it an edge is false at any scale. The prop is
|
||
still passed and still computed — only the claim is withdrawn. */}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</header>
|
||
|
||
{signals.length > 0 && !locked && (
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 16 }}>
|
||
{signals.map((sig, idx) => {
|
||
const style = SIGNAL_TONE_STYLE[sig.tone];
|
||
return (
|
||
<div
|
||
key={`${sig.label}-${idx}`}
|
||
style={{
|
||
padding: '8px 12px',
|
||
border: `1px solid ${style.border}`,
|
||
background: style.bg,
|
||
borderRadius: 6,
|
||
fontSize: 12,
|
||
display: 'flex',
|
||
alignItems: 'baseline',
|
||
gap: 6,
|
||
maxWidth: '100%',
|
||
}}
|
||
>
|
||
<span aria-hidden style={{ fontSize: 14 }}>{sig.icon}</span>
|
||
<span
|
||
className="mono"
|
||
style={{
|
||
color: style.color,
|
||
fontWeight: 600,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.04em',
|
||
fontSize: 10,
|
||
}}
|
||
>
|
||
{sig.label}
|
||
</span>
|
||
<span style={{ color: 'var(--text-primary)' }}>{sig.detail}</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{!locked && reasoning?.summary && (
|
||
<p style={{ fontSize: 14, color: 'var(--text-secondary)', lineHeight: 1.6, marginBottom: 16 }}>
|
||
{reasoning.summary}
|
||
</p>
|
||
)}
|
||
|
||
{locked && (
|
||
<div
|
||
style={{
|
||
padding: 20,
|
||
border: '1px dashed var(--border)',
|
||
borderRadius: 6,
|
||
background: 'rgba(0,0,0,0.20)',
|
||
textAlign: 'center',
|
||
marginBottom: 16,
|
||
}}
|
||
>
|
||
<div
|
||
className="mono"
|
||
style={{
|
||
filter: 'blur(4px)',
|
||
userSelect: 'none',
|
||
color: 'var(--text-tertiary)',
|
||
fontSize: 13,
|
||
marginBottom: 16,
|
||
}}
|
||
aria-hidden
|
||
>
|
||
⚽ Goals/90: 0.67 · 📊 xG: 0.52 — overperforming · 🏔️ altitude 7,349ft · 🟨 ref 4.7 cards/match · 🎯 penalty taker
|
||
</div>
|
||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 12 }}>
|
||
{upgrade_hint || 'Unlock full intelligence — xG regression, altitude, referee, set-piece role.'}
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={onUpgradeClick}
|
||
className="btn-primary"
|
||
style={{ padding: '8px 18px', fontSize: 13 }}
|
||
>
|
||
Unlock full analysis
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{kills.length > 0 && (
|
||
<section style={{ marginTop: 8 }}>
|
||
<h3
|
||
className="mono"
|
||
style={{ fontSize: 10, color: 'var(--grade-d, #ff5a5a)', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: 8 }}
|
||
>
|
||
Kill conditions ({kills.length})
|
||
</h3>
|
||
<ul style={{ display: 'grid', gap: 6 }}>
|
||
{kills.map((k, idx) => (
|
||
<li
|
||
key={`${k.code}-${idx}`}
|
||
style={{
|
||
padding: '6px 10px',
|
||
border: '1px solid rgba(255,90,90,0.30)',
|
||
background: 'rgba(255,90,90,0.04)',
|
||
borderRadius: 4,
|
||
fontSize: 12,
|
||
}}
|
||
>
|
||
<span className="mono" style={{ color: 'var(--grade-d, #ff5a5a)', fontWeight: 700, marginRight: 6 }}>
|
||
{k.code}
|
||
</span>
|
||
<span style={{ color: 'var(--text-secondary)' }}>{k.reason}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</section>
|
||
)}
|
||
</article>
|
||
);
|
||
}
|