Session F (night2): Phase 5 — records + dossier

5.1 Archetype defined on-page: one line from the archetype library under
    ARCHETYPE DNA (BOMBER — elite raw power…); expander keeps the long form.
5.2 VYNDR-on-team live: getModelAggregate team scope (migration-020 column)
    + /api/ledger/model?team= + ModelRecord mounted on the Team Hub header.
5.3 WNBA/NBA profile parity: minutes-based usage (+MIN season cell) when
    the feed carries minutes — absent beats invented.
5.4 Settings read meter: real rolling-24h usage from the SAME store the
    limiter enforces (GET /api/user/scan-meter + proxy). Metered tiers see
    'X of N reads today' + bar; unlimited tiers see nothing. Corrects the
    stale '5 scans / month' copy.
5.5 Per-tier calibration on the MODEL tab: A+/A/B/C chips with hit% at
    n>=20 PER TIER, 'building (n/20)' below — the separation between tiers
    is the proof the grades mean something.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-11 01:05:29 -04:00
parent 5d19660f8e
commit f110bd63f1
13 changed files with 206 additions and 4 deletions
+28
View File
@@ -44,6 +44,7 @@ interface LedgerRow {
revised_from_grade?: string | null;
}
interface TierRecord { settled: number; hits: number; misses: number; hit_pct: number | null }
interface ModelAggregate {
settled: number;
hits: number;
@@ -55,6 +56,8 @@ interface ModelAggregate {
beat_close_pct: number | null;
pending: number;
min_sample?: number;
// Session 60 (5.5) — calibration by grade tier (n≥20 rule per tier).
by_tier?: Record<string, TierRecord>;
}
const SPORT_COLOR: Record<string, string> = {
@@ -160,6 +163,30 @@ export default function LedgerPage() {
);
}
const TIER_ORDER = ['A+', 'A', 'B', 'C', 'D', 'F'];
/** Session 60 (5.5) — per-tier calibration: the separation between tiers is
* the proof the grades mean something. A tier under n≥20 shows "building",
* never a small-sample percentage. Self-hides until ANY tier is ready. */
function TierCalibration({ agg, minSample }: { agg: ModelAggregate; minSample: number }) {
const tiers = agg.by_tier || {};
const anyReady = TIER_ORDER.some((t) => tiers[t] && tiers[t].hit_pct != null);
if (!anyReady) return null;
return (
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginTop: 14, paddingTop: 12, borderTop: '1px solid var(--border)' }}>
{TIER_ORDER.filter((t) => tiers[t] && tiers[t].settled > 0).map((t) => {
const b = tiers[t];
const ready = b.hit_pct != null;
return (
<span key={t} className="mono" style={{ fontSize: 11.5, fontWeight: 700, padding: '4px 10px', borderRadius: 6, border: '1px solid var(--border-hi)', color: ready ? 'var(--text-primary)' : 'var(--text-tertiary)' }}>
{t}-TIER · {ready ? `${b.hits}-${b.misses} · ${b.hit_pct}%` : `building (${b.settled}/${minSample})`}
</span>
);
})}
</div>
);
}
function ModelHeader({ agg, minSample }: { agg: ModelAggregate; minSample: number }) {
const ready = agg.settled >= minSample && agg.hit_pct != null;
return (
@@ -200,6 +227,7 @@ function ModelHeader({ agg, minSample }: { agg: ModelAggregate; minSample: numbe
</p>
</div>
)}
<TierCalibration agg={agg} minSample={minSample} />
</div>
);
}