Wire the composed surface: the honest fields now reach a user
PHASE 0 re-check caught the same failure a THIRD time, mine again. Last
turn I created GradeScaleLegend.tsx and never mounted it, and I reported
that the separation flag "renders per band" -- it did not. grep: zero
frontend references to separates_from_base_rate, served_grade or
factor_adjustment. The honest grade was reaching the API payload and dying
at the adapter boundary.
That is three occurrences in three orders of the same shape: built,
correct, unread. gradeBands, then served_grade, then the legend.
PHASE 2 — the composed surface is wired end to end:
analyzeViaEngine1 -> served_grade + factor_adjustment on the payload
scan/page.tsx -> ScanResponse types them and forwards them
gradeAdapter -> gradeMeaning, separatesFromBaseRate,
bandRealizedRate, factorsApplied, refusalReason
GradeResultCard -> renders "WHAT THIS GRADE MEANS"
What a user now sees that they could not before: what the band has
actually realized, an amber note when the read CANNOT be separated from
the baseline, and -- only where a factor actually fired, with its proven
sign -- what moved the read, in plain language rather than feature names
("where he hits it vs who is standing there").
No narrative on props where nothing fired: factorsApplied is empty and the
block self-hides. Only the three PROVEN hits factors have labels, so an
unproven factor cannot acquire prose by being added to the map.
PHASE 1 — GradeScaleLegend is now MOUNTED on the grade card (compact). The
ceiling is a stated position where the grade is, not a page a user would
have to find.
PHASE 3 hand-verified through the real adapter across twelve states: B+
with 3/0 factors, B with 1, C+/C/C- all flagged not-separable, D, F, a
switch-hitter case where 2 of 3 factors fire, two refusals and a
no-forecast. never-blank PASS, no-manufactured-A PASS, no-narrative-when-
nothing-fired PASS, separation-flag-reaches-card PASS.
No A-threshold loosening. No calibrated number leaks. p_win never mutated.
engine_grade still read by zero serving code. No Bonferroni slot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
This commit is contained in:
@@ -44,6 +44,17 @@ interface Player {
|
|||||||
|
|
||||||
interface ScanResponse {
|
interface ScanResponse {
|
||||||
grade: string;
|
grade: string;
|
||||||
|
served_grade?: {
|
||||||
|
letter: string | null;
|
||||||
|
state: string;
|
||||||
|
label: string;
|
||||||
|
meaning: string;
|
||||||
|
band_realized_rate?: number;
|
||||||
|
separates_from_base_rate?: boolean;
|
||||||
|
basis?: string;
|
||||||
|
factors_applied?: string[];
|
||||||
|
};
|
||||||
|
factor_adjustment?: { applied?: Array<{ factor: string; multiplier: number }> };
|
||||||
// Session 58 (work-order 1.5) — the model refused: no projection, no read.
|
// Session 58 (work-order 1.5) — the model refused: no projection, no read.
|
||||||
insufficient_data?: boolean;
|
insufficient_data?: boolean;
|
||||||
projection?: number;
|
projection?: number;
|
||||||
@@ -864,6 +875,10 @@ export default function ScanPage() {
|
|||||||
sample_size: result.sample_size,
|
sample_size: result.sample_size,
|
||||||
factors: result.factors,
|
factors: result.factors,
|
||||||
alt_lines: result.alt_lines,
|
alt_lines: result.alt_lines,
|
||||||
|
// The honest grade fields -- carried, or the card cannot say
|
||||||
|
// whether a band separates from the baseline.
|
||||||
|
served_grade: result.served_grade,
|
||||||
|
factor_adjustment: result.factor_adjustment,
|
||||||
kelly: result.kelly,
|
kelly: result.kelly,
|
||||||
kill_conditions: result.kill_conditions,
|
kill_conditions: result.kill_conditions,
|
||||||
tier,
|
tier,
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ import { gradeGlows } from '@/lib/colorContract';
|
|||||||
import PriceTriplet, { type PriceTripletData } from './PriceTriplet';
|
import PriceTriplet, { type PriceTripletData } from './PriceTriplet';
|
||||||
import { playerHref } from '@/lib/playerHref';
|
import { playerHref } from '@/lib/playerHref';
|
||||||
|
|
||||||
|
import GradeScaleLegend from './GradeScaleLegend';
|
||||||
|
|
||||||
|
/** Only the three factors PROVEN for hits reach the card, so only these map. */
|
||||||
|
const FACTOR_LABEL: Record<string, string> = {
|
||||||
|
defense_by_direction: 'where he hits it vs who is standing there',
|
||||||
|
platoon_severity: 'his own measured platoon split',
|
||||||
|
pitcher_contact_profile: 'the contact this arm concedes',
|
||||||
|
};
|
||||||
|
|
||||||
export interface GradeResultData {
|
export interface GradeResultData {
|
||||||
player: string;
|
player: string;
|
||||||
team: string;
|
team: string;
|
||||||
@@ -36,6 +45,12 @@ export interface GradeResultData {
|
|||||||
projection: number | null;
|
projection: number | null;
|
||||||
phosphorConfirmed?: boolean;
|
phosphorConfirmed?: boolean;
|
||||||
signals: string[];
|
signals: string[];
|
||||||
|
/** What this band has actually realized, and whether it separates at all. */
|
||||||
|
gradeMeaning?: string | null;
|
||||||
|
separatesFromBaseRate?: boolean | null;
|
||||||
|
bandRealizedRate?: number | null;
|
||||||
|
/** ONLY factors that fired, with their proven sign. Empty is the common case. */
|
||||||
|
factorsApplied?: Array<{ factor: string; direction: 'up' | 'down'; multiplier: number }>;
|
||||||
killConditions?: string[];
|
killConditions?: string[];
|
||||||
books: Array<{ name: string; line: number; odds: string; best?: boolean }>;
|
books: Array<{ name: string; line: number; odds: string; best?: boolean }>;
|
||||||
altLadder?: Array<{ line: number; grade: string; edge?: number | null; base?: boolean }>;
|
altLadder?: Array<{ line: number; grade: string; edge?: number | null; base?: boolean }>;
|
||||||
@@ -248,6 +263,58 @@ export default function GradeResultCard({
|
|||||||
nothing rather than a fabricated timeline. */}
|
nothing rather than a fabricated timeline. */}
|
||||||
<GradeShift history={d.history} side={d.side} grade={d.grade} revisedFrom={d.revisedFrom} gradedLine={d.gradedLine ?? d.line} />
|
<GradeShift history={d.history} side={d.side} grade={d.grade} revisedFrom={d.revisedFrom} gradedLine={d.gradedLine ?? d.line} />
|
||||||
|
|
||||||
|
{/* 4b. WHAT THIS GRADE MEANS — the honest core.
|
||||||
|
A band that cannot be separated from the baseline SAYS so here. That
|
||||||
|
is most of any slate, and stating it is the point: a C is not a weak
|
||||||
|
opinion, it is us telling you we see nothing that distinguishes this
|
||||||
|
prop. Absent fields self-hide, so nothing renders on a refusal. */}
|
||||||
|
{(d.gradeMeaning || (d.factorsApplied && d.factorsApplied.length > 0)) && (
|
||||||
|
<div style={{ padding: '14px 20px', borderTop: '1px solid var(--line)' }}>
|
||||||
|
<SectionHead style={{ marginBottom: 10 }}>WHAT THIS GRADE MEANS</SectionHead>
|
||||||
|
|
||||||
|
{d.gradeMeaning && (
|
||||||
|
<p className="mono" style={{ margin: '0 0 8px', fontSize: 11, lineHeight: 1.6, color: 'var(--text-2)' }}>
|
||||||
|
{d.gradeMeaning}
|
||||||
|
{typeof d.bandRealizedRate === 'number' && (
|
||||||
|
<> — this band has landed <strong style={{ color: 'var(--text-1)' }}>
|
||||||
|
{Math.round(d.bandRealizedRate * 100)}%
|
||||||
|
</strong> of the time.</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{d.separatesFromBaseRate === false && (
|
||||||
|
<p className="mono" style={{ margin: '0 0 8px', fontSize: 11, lineHeight: 1.6, color: 'var(--amber)' }}>
|
||||||
|
We cannot separate this read from the baseline. Shown so you know
|
||||||
|
the model is not claiming an edge here.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{d.factorsApplied && d.factorsApplied.length > 0 && (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
<div className="mono" style={{ fontSize: 10, letterSpacing: '.08em', color: 'var(--text-3)' }}>
|
||||||
|
WHAT MOVED THE READ
|
||||||
|
</div>
|
||||||
|
{d.factorsApplied.map((f) => (
|
||||||
|
<div key={f.factor} className="mono" style={{ fontSize: 11, color: 'var(--text-2)' }}>
|
||||||
|
<span style={{ color: f.direction === 'up' ? 'var(--hit)' : 'var(--miss)' }}>
|
||||||
|
{f.direction === 'up' ? '\u25B2' : '\u25BC'}
|
||||||
|
</span>{' '}
|
||||||
|
{FACTOR_LABEL[f.factor] || f.factor}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 4c. THE SCALE ITSELF. A user who never sees an A will invent a reason
|
||||||
|
for it, and every reason they might invent is wrong. Compact form
|
||||||
|
rides the card; the full band table lives on the scale page. */}
|
||||||
|
<div style={{ padding: '12px 20px', borderTop: '1px solid var(--line)' }}>
|
||||||
|
<GradeScaleLegend compact />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 5. SIGNAL BREAKDOWN */}
|
{/* 5. SIGNAL BREAKDOWN */}
|
||||||
{d.signals.length > 0 && (
|
{d.signals.length > 0 && (
|
||||||
<div style={{ padding: '16px 20px' }}>
|
<div style={{ padding: '16px 20px' }}>
|
||||||
|
|||||||
@@ -86,6 +86,27 @@ function mapScanToGradeResult(input = {}) {
|
|||||||
side,
|
side,
|
||||||
grade: input.grade || '—',
|
grade: input.grade || '—',
|
||||||
confidence,
|
confidence,
|
||||||
|
// ── THE HONEST GRADE FIELDS ────────────────────────────────────────────
|
||||||
|
// The served grade knows whether its band can actually be separated from
|
||||||
|
// the baseline, and which proven factors (if any) moved the forecast. Both
|
||||||
|
// were being computed and thrown away at this boundary. A grade that cannot
|
||||||
|
// separate must SAY so on the card -- that flag is the honest core, and
|
||||||
|
// carrying it in an object nobody reads is the same as not having it.
|
||||||
|
gradeMeaning: (input.served_grade && input.served_grade.meaning) || null,
|
||||||
|
separatesFromBaseRate: input.served_grade
|
||||||
|
? input.served_grade.separates_from_base_rate === true : null,
|
||||||
|
bandRealizedRate: (input.served_grade && input.served_grade.band_realized_rate) ?? null,
|
||||||
|
// Only factors that ACTUALLY fired, with their proven sign. No narrative on
|
||||||
|
// props where nothing fired.
|
||||||
|
factorsApplied: (input.factor_adjustment && Array.isArray(input.factor_adjustment.applied))
|
||||||
|
? input.factor_adjustment.applied.map((a) => ({
|
||||||
|
factor: a.factor,
|
||||||
|
direction: a.multiplier > 1 ? 'up' : 'down',
|
||||||
|
multiplier: a.multiplier,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
refusalReason: (input.served_grade && input.served_grade.state !== 'graded')
|
||||||
|
? input.served_grade.meaning : null,
|
||||||
// DATA SEMANTICS (Session 58): projection is MODEL output and must never
|
// DATA SEMANTICS (Session 58): projection is MODEL output and must never
|
||||||
// be fabricated. The old fallback displayed the LINE as the projection —
|
// be fabricated. The old fallback displayed the LINE as the projection —
|
||||||
// the audit's model==line / +0% edge degenerate. No projection → the
|
// the audit's model==line / +0% edge degenerate. No projection → the
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user