Wave 3: Record by grade tier — shared TierRecord across dashboard, /u, ledger
Addition 2 (non-negotiable): the model's record must show PER GRADE TIER (A+ went X-Y, A X-Y, …) everywhere the record appears. A blended % hides the proof that higher grades win more — the tier calibration IS the credibility. The backend already computed `by_tier` in getModelAggregate; this is display propagation via ONE shared component (the class fix, not five one-offs). - web/src/lib/tierRecord.js — testable CommonJS row-builder. W-L counts ALWAYS (honest at any n); hit-% only when the upstream n≥20 gate passed (hit_pct != null), else "RECORD BUILDING · N settled". Order A+ A B C D F. A-tier is the only edge (green) tier — no glow below A, red reserved for outcomes. - web/src/components/vyndr/TierRecord.tsx — the ONE shared table. Presentational (byTier) for /u + ledger; self-fetch (/api/ledger/model, sport-scoped) for the dashboard. Fully self-hides until a tier has a settled read. - Ledger swaps its inline TierCalibration for the shared component (single source). /u PublicProfile renders it below the blended hero (by_tier added to the aggregate type; it already flows through the route + proxy untouched). Dashboard gains a compact per-tier surface. - Endpoint/proxy audit: profiles.js + ledger.js return the full aggregate (by_tier included); both Next proxies pass the body through — no threading needed. No change to the gate or math in getModelAggregate. Tests: tests/unit/tierRecord.test.js (row logic + tier order + edge contract + source-assert all three surfaces render the shared component). ledgerService test gains an A+-stands-alone bucketing case. Full suite green (243 suites / 2961 tests); web next build exits 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import { useEffect, useState } from 'react';
|
||||
import { GradePill } from '@/components/GradeCard';
|
||||
import PlayerAvatar from '@/components/vyndr/PlayerAvatar';
|
||||
import BookWordmark from '@/components/vyndr/BookWordmark';
|
||||
import TierRecord from '@/components/vyndr/TierRecord';
|
||||
import { buildTierRows } from '@/lib/tierRecord';
|
||||
|
||||
/**
|
||||
* PublicProfile (A1 Session 10) — the public ledger record for one handle.
|
||||
@@ -37,6 +39,8 @@ interface ProfileRow {
|
||||
revised_from_grade?: string | null;
|
||||
}
|
||||
|
||||
interface TierBucket { settled: number; hits: number; misses: number; pushes?: number; hit_pct: number | null }
|
||||
|
||||
interface ProfileAggregate {
|
||||
settled: number;
|
||||
hits: number;
|
||||
@@ -47,6 +51,9 @@ interface ProfileAggregate {
|
||||
clv_beat: number;
|
||||
beat_close_pct: number | null;
|
||||
pending: number;
|
||||
// Wave 3 (Addition 2) — per-tier calibration (survives the API + proxy
|
||||
// untouched, inside the aggregate). The credibility centerpiece.
|
||||
by_tier?: Record<string, TierBucket>;
|
||||
}
|
||||
|
||||
interface ProfilePayload {
|
||||
@@ -126,6 +133,14 @@ export default function PublicProfile({ handle }: { handle: string }) {
|
||||
{/* Record header — never a percentage under min_sample settles. */}
|
||||
<RecordHeader agg={agg} minSample={minSample} />
|
||||
|
||||
{/* Wave 3 (Addition 2) — the record BY GRADE TIER, the proof that higher
|
||||
grades win more. Same shared component as the dashboard + ledger. */}
|
||||
{agg && buildTierRows(agg.by_tier || {}).length > 0 && (
|
||||
<div className="surface diagonal-cut" style={{ padding: 20, marginBottom: 20 }}>
|
||||
<TierRecord byTier={agg.by_tier} minSample={minSample} dense />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.entries.length === 0 ? (
|
||||
<div className="surface diagonal-cut" style={{ padding: 48, textAlign: 'center' }}>
|
||||
<p className="mono" style={{ fontSize: 12, fontWeight: 800, letterSpacing: '0.1em', color: 'var(--text-tertiary)', marginBottom: 8 }}>
|
||||
|
||||
Reference in New Issue
Block a user