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:
@@ -251,6 +251,25 @@ describe('getModelAggregate — per-tier calibration (n≥20 per tier)', () => {
|
||||
expect(agg.by_tier.B.settled).toBe(3);
|
||||
expect(agg.by_tier.B.hit_pct).toBeNull(); // under 20 → building
|
||||
});
|
||||
|
||||
test('A+ stands ALONE; A/A- do NOT fold into it (Addition 2 bucketing)', async () => {
|
||||
const sb = fakeSb();
|
||||
const rows = [
|
||||
...Array.from({ length: 5 }, () => ({ outcome: 'hit', clv_result: null, grade: 'A+' })),
|
||||
...Array.from({ length: 4 }, () => ({ outcome: 'hit', clv_result: null, grade: 'A' })),
|
||||
...Array.from({ length: 2 }, () => ({ outcome: 'miss', clv_result: null, grade: 'A-' })),
|
||||
];
|
||||
sb._state.selectResults = [rows];
|
||||
sb._state.countResult = 0;
|
||||
const agg = await ledger.getModelAggregate({ sb });
|
||||
// A+ is its own bucket — never merged into the first-letter A bucket.
|
||||
expect(agg.by_tier['A+'].settled).toBe(5);
|
||||
expect(agg.by_tier['A+'].hits).toBe(5);
|
||||
// A + A- fold together by first letter (but NOT A+).
|
||||
expect(agg.by_tier.A.settled).toBe(6);
|
||||
expect(agg.by_tier.A.hits).toBe(4);
|
||||
expect(agg.by_tier.A.misses).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// Session 61 — the odds index prefers a fully-priced book row (the null
|
||||
|
||||
Reference in New Issue
Block a user