S6 (a1): display — the full picture under the grammar
- specs/ROW-GRAMMAR.md: the row grammar law (slot order, color law, mark
law, mobile stacking, no-truncation), locked by tests/unit/rowGrammar.
StatStrip violations fixed: MovementChip before the grade (market
context before model output); ViabilityChips after the archetype
(identity is one contiguous run).
- Line-movement sparklines: intradayRefreshService.trackHistory captures
real {t,line} points per grade (seeded with the lock, deduped when
flat, capped 24) inside the snapshot write-back; StatStrip.LineSparkline
renders at >=3 points (green toward / amber against / dim flat).
- Last-10 dot strips: services/last10Dots (streaksService accessors) ->
/api/snapshot/:sport attaches last10_dots from rosterlogs:{sport};
StatStrip.DotStrip renders vs the LOCKED line, newest first.
- CLV distribution: getModelAggregate emits clv_distribution (7 signed
buckets, outliers clamped) only past the centralized n>=20 gate;
ledger MODEL header renders the bar strip.
- Global search: SearchModal (cmd-K via GlobalHosts + window.__search),
players via /api/players/search per sport + static lib/teams.js
(soccer deliberately absent); Nav search icon + Search first in the
mobile More sheet. Explore tab untouched.
- Landing LCP: fade-up floored at opacity .6 (hero h1 contentful on
first frame, S33 visible-floor rule) + IBM Plex Mono preload:false
(4 decorative font files off the slow-4G critical path).
2654 -> 2698 tests (226 suites) green; web build exit 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -271,3 +271,64 @@ describe('indexProps — prefers a book row with both sides priced', () => {
|
||||
expect(idx['a guy|hits'].book).toBe('fanduel');
|
||||
});
|
||||
});
|
||||
|
||||
// S6 (A1 board) — CLV distribution on the model aggregate. The n>=20 gate is
|
||||
// centralized HERE (getModelAggregate) — consumers never re-derive it.
|
||||
describe('getModelAggregate — clv_distribution (n>=20 gate lives in the service)', () => {
|
||||
const { clvBucketIndex, CLV_BUCKETS } = ledger.__internals;
|
||||
|
||||
test('below 20 settles → clv_distribution is null (never a small-sample chart)', async () => {
|
||||
const sb = fakeSb();
|
||||
sb._state.selectResults = [
|
||||
Array.from({ length: 5 }, () => ({ outcome: 'hit', clv_result: 'beat', clv: 0.5 })),
|
||||
];
|
||||
const agg = await ledger.getModelAggregate({ sb });
|
||||
expect(agg.clv_distribution).toBeNull();
|
||||
});
|
||||
|
||||
test('at 20+ settles → seven ordered buckets, counts bucketed by signed clv', async () => {
|
||||
const sb = fakeSb();
|
||||
const rows = [
|
||||
...Array.from({ length: 8 }, () => ({ outcome: 'hit', clv_result: 'beat', clv: 0.5 })),
|
||||
...Array.from({ length: 4 }, () => ({ outcome: 'hit', clv_result: 'beat', clv: 1.5 })),
|
||||
...Array.from({ length: 5 }, () => ({ outcome: 'miss', clv_result: 'faded', clv: -0.5 })),
|
||||
...Array.from({ length: 2 }, () => ({ outcome: 'miss', clv_result: 'flat', clv: 0 })),
|
||||
{ outcome: 'push', clv_result: 'faded', clv: -3 }, // outlier clamps into the edge bucket
|
||||
];
|
||||
sb._state.selectResults = [rows];
|
||||
const agg = await ledger.getModelAggregate({ sb });
|
||||
const dist = agg.clv_distribution;
|
||||
expect(dist).toHaveLength(7);
|
||||
expect(dist.map((b) => b.label)).toEqual(CLV_BUCKETS.map((b) => b.label));
|
||||
expect(dist[0]).toMatchObject({ side: 'faded', count: 1 }); // [-2,-1) — clamped -3
|
||||
expect(dist[2]).toMatchObject({ side: 'faded', count: 5 }); // [-.5,0) — -0.5 closed on the left
|
||||
expect(dist[1]).toMatchObject({ side: 'faded', count: 0 }); // [-1,-.5) — empty
|
||||
expect(dist[3]).toMatchObject({ side: 'flat', count: 2 }); // exactly 0
|
||||
expect(dist[4]).toMatchObject({ side: 'beat', count: 8 }); // (0,.5]
|
||||
expect(dist[6]).toMatchObject({ side: 'beat', count: 4 }); // (1,2] — 1.5
|
||||
});
|
||||
|
||||
test('rows without a settled clv value never fabricate a bucket', async () => {
|
||||
const sb = fakeSb();
|
||||
sb._state.selectResults = [
|
||||
Array.from({ length: 20 }, () => ({ outcome: 'hit', clv_result: 'beat', clv: null })),
|
||||
];
|
||||
const agg = await ledger.getModelAggregate({ sb });
|
||||
expect(agg.clv_distribution).toBeNull(); // no real clv values → nothing
|
||||
});
|
||||
|
||||
test('clvBucketIndex edges: boundaries land per the spec intervals', () => {
|
||||
expect(clvBucketIndex(0)).toBe(3);
|
||||
expect(clvBucketIndex(0.5)).toBe(4); // (0,.5] closed on the right
|
||||
expect(clvBucketIndex(0.51)).toBe(5);
|
||||
expect(clvBucketIndex(1)).toBe(5); // (.5,1]
|
||||
expect(clvBucketIndex(1.01)).toBe(6);
|
||||
expect(clvBucketIndex(-0.5)).toBe(2); // [-.5,0) closed on the left
|
||||
expect(clvBucketIndex(-0.51)).toBe(1); // [-1,-.5)
|
||||
expect(clvBucketIndex(-1)).toBe(1); // [-1,-.5) closed on the left
|
||||
expect(clvBucketIndex(-1.01)).toBe(0); // [-2,-1)
|
||||
expect(clvBucketIndex(-9)).toBe(0); // clamp
|
||||
expect(clvBucketIndex(9)).toBe(6); // clamp
|
||||
expect(clvBucketIndex(null)).toBe(-1); // absent beats wrong
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user