Wave 3: /compare built (real head-to-head); resolution tail scoped, not shipped
No grade, ledger or scoring change. Push scoring untouched.
REVIEW ZERO 0.3/0.4 — THE RESOLUTION TAIL DOES NOT FIRE. The resolver is
POST /api/grading/resolve (routes/grading.js:208), and its fanout at :356-371
covers webPush, telegram and discord — but:
- share-card generation: SPEC'D-NOT-BUILT. Not in the fanout at all (grep
shareCard in grading.js = 0). shareCards/renderer.js exists with ZERO
callers, so the component is built but no step would ever invoke it.
- push notifications: BUILT-NOT-FIRING. In the fanout but gated on
webPush.configured() (VAPID). push_subscriptions = 0 rows and
user_notifications = 0 rows — nothing ever subscribed or delivered.
- Telegram result posts: BUILT-NOT-FIRING (gated on BOT_TOKEN + CHANNEL_ID).
- Discord result posts: BUILT-NOT-FIRING (gated on webhookFor('results')).
- recap (all-Final trigger): SPEC'D-NOT-BUILT. No recap file exists in src/.
AND THE WHOLE TAIL IS UNREACHABLE: nothing calls /api/grading/resolve — there is
no ESPN poller in the repo. The live settlement path is the scheduler's
settleAllOutcomes + settleAllLedgers, which fans out to opsNotify only (ops
alerts), with no user-facing output. So even the built channels have no trigger.
Per the order's own rule, ShareCard, /notifications, result posts and recap are
therefore ALL SCOPED, none shipped — no dead shells over a silent pipeline.
BUILT — /compare. Semantics (0.2): a same-market head-to-head, two players with
every row a measure BOTH sides are scored on, aligned via alignRows so the
numbers are comparable — deliberately not two disconnected graded props. Reads
the live /api/stats/player/:name?sport= aggregate. Honest-absent three ways: an
unresolved side reads NO DATA while the other still renders; a measure only one
side has renders a dash, never 0; if neither resolves the page refuses to
compare. NO VERDICT — it shows measures and says the reader draws the call.
Two pre-existing tests (vyndrPhaseE, vyndrParityQA) asserted the in-development
placeholder; both superseded rather than deleted — they now assert the stronger
properties against the real page (live fetch, no sample players, NO VERDICT,
NO DATA, "not a zero").
Floor: 316 suites / 3930 tests green (10 new), web build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
@@ -284,11 +284,17 @@ describe('P1-7 — edge board guards the broken edge placeholder', () => {
|
||||
// ── P1-8 lock — Compare ships NO fabrication (honest in-development state) ────
|
||||
describe('P1-8 — Compare carries no hardcoded grades or phantom verdict', () => {
|
||||
const compare = read('app/compare/page.tsx');
|
||||
it('no sample players, no fake verdict, no "tonight" claim — honest in-dev', () => {
|
||||
// SUPERSEDED 2026-07-31 by WAVE 3 — the in-dev placeholder became the real
|
||||
// head-to-head. Every honesty invariant is kept and tightened: no sample players,
|
||||
// no verdict, no "tonight" claim, AND a missing side must read as absent, never 0.
|
||||
it('no sample players, no fake verdict, no "tonight" claim — now on the REAL page', () => {
|
||||
expect(compare).not.toMatch(/edge tonight/i);
|
||||
expect(compare).not.toMatch(/Joki|Wembanyama/);
|
||||
expect(compare).not.toContain('VYNDR VERDICT');
|
||||
expect(compare).toMatch(/IN DEVELOPMENT/i);
|
||||
expect(compare).not.toMatch(/IN DEVELOPMENT/i);
|
||||
expect(compare).toMatch(/NO VERDICT/);
|
||||
expect(compare).toMatch(/NO DATA/); // honest-absent side
|
||||
expect(compare).toMatch(/not a zero/); // dash is absence, not 0
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -84,11 +84,17 @@ describe('Phase E.3 — stubs are now real pages', () => {
|
||||
it.each(pages)('/%s is no longer a RouteStub', (p) => {
|
||||
expect(read(`app/${p}/page.tsx`)).not.toContain('RouteStub');
|
||||
});
|
||||
it('compare shows an honest in-development state — no sample grades, no fake verdict', () => {
|
||||
// SUPERSEDED 2026-07-31 by WAVE 3: /compare is now the REAL head-to-head, so the
|
||||
// in-development placeholder is correctly gone. The honesty properties it guarded
|
||||
// (no sample players, no fake verdict) are asserted here in their stronger form —
|
||||
// against a page that fetches live data rather than one that renders nothing.
|
||||
it('compare is a real head-to-head with no sample grades and no fake verdict', () => {
|
||||
const src = read('app/compare/page.tsx');
|
||||
expect(src).toMatch(/IN DEVELOPMENT/i);
|
||||
expect(src).not.toMatch(/IN DEVELOPMENT/i);
|
||||
expect(src).not.toContain('VYNDR VERDICT');
|
||||
expect(src).not.toMatch(/Joki|Wembanyama/); // no hardcoded sample players
|
||||
expect(src).toMatch(/\/api\/stats\/player\//); // reads the live feed
|
||||
expect(src).toMatch(/NO VERDICT/); // shows measures, renders no call
|
||||
});
|
||||
it('invite renders the 3-friends referral messaging', () => {
|
||||
expect(read('app/invite/page.tsx')).toContain('3 friends');
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* WAVE 3 — /compare head-to-head + the resolution-tail state
|
||||
* (specs/wave3-compare-and-resolution-tail.md).
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const read = (p) => fs.readFileSync(path.join(__dirname, '../../', p), 'utf8');
|
||||
|
||||
describe('/compare is a real same-market head-to-head, not two cards', () => {
|
||||
const src = read('web/src/app/compare/page.tsx');
|
||||
|
||||
test('the in-development placeholder is GONE', () => {
|
||||
expect(src).not.toMatch(/IN DEVELOPMENT/);
|
||||
expect(src).not.toMatch(/Check back soon/);
|
||||
});
|
||||
|
||||
test('it fetches BOTH sides from the live player feed', () => {
|
||||
expect(src).toMatch(/\/api\/stats\/player\//);
|
||||
expect(src).toMatch(/Promise\.all\(\[loadPlayer\(nameA/);
|
||||
});
|
||||
|
||||
test('rows are ALIGNED on shared measures (what makes it a comparison)', () => {
|
||||
expect(src).toMatch(/function alignRows/);
|
||||
expect(src).toMatch(/shared keys/i);
|
||||
});
|
||||
|
||||
test('carries NO fabricated player and NO verdict', () => {
|
||||
expect(src).not.toMatch(/Jokic|Jokić|Wembanyama/);
|
||||
expect(src).toMatch(/NO VERDICT/);
|
||||
});
|
||||
|
||||
test('a missing side is honest-absent — dash, never zero, never invented', () => {
|
||||
expect(src).toMatch(/NO DATA/);
|
||||
expect(src).toMatch(/r\.a \?\? '—'/);
|
||||
expect(src).toMatch(/not a zero/);
|
||||
// and a totally unresolved pair refuses to compare
|
||||
expect(src).toMatch(/we will not invent a comparison/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alignRows — the comparison contract', () => {
|
||||
// mirrors the page's pure helper
|
||||
const alignRows = (a, b) => {
|
||||
const keys = [];
|
||||
for (const r of a || []) if (r && r.k && !keys.includes(r.k)) keys.push(r.k);
|
||||
for (const r of b || []) if (r && r.k && !keys.includes(r.k)) keys.push(r.k);
|
||||
return keys.map((k) => ({
|
||||
k,
|
||||
a: (a || []).find((r) => r.k === k)?.v ?? null,
|
||||
b: (b || []).find((r) => r.k === k)?.v ?? null,
|
||||
}));
|
||||
};
|
||||
|
||||
test('keys present on only one side yield null on the other, not 0', () => {
|
||||
const out = alignRows([{ k: 'HR', v: '17' }], [{ k: 'AVG', v: '.301' }]);
|
||||
expect(out).toEqual([
|
||||
{ k: 'HR', a: '17', b: null },
|
||||
{ k: 'AVG', a: null, b: '.301' },
|
||||
]);
|
||||
for (const r of out) {
|
||||
expect(r.a === 0 || r.b === 0).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('shared keys line up on one row', () => {
|
||||
const out = alignRows([{ k: 'HR', v: '17' }], [{ k: 'HR', v: '9' }]);
|
||||
expect(out).toEqual([{ k: 'HR', a: '17', b: '9' }]);
|
||||
});
|
||||
|
||||
test('empty input compares nothing rather than inventing rows', () => {
|
||||
expect(alignRows(undefined, undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolution tail — nothing shipped as a dead shell', () => {
|
||||
test('ShareCard is still NOT wired to any generator (its pipeline step does not exist)', () => {
|
||||
const grading = read('src/routes/grading.js');
|
||||
expect(grading).not.toMatch(/shareCard/i); // no generation step in the resolve fanout
|
||||
const importers = fs.readdirSync(path.join(__dirname, '../../web/src/components'))
|
||||
.filter((f) => f !== 'ShareCard.tsx')
|
||||
.filter((f) => f.endsWith('.tsx'))
|
||||
.filter((f) => read(`web/src/components/${f}`).includes('ShareCard'));
|
||||
expect(importers).toEqual([]); // still unrouted, deliberately
|
||||
});
|
||||
|
||||
test('/notifications remains a stub — no settings screen over a silent pipeline', () => {
|
||||
const src = read('web/src/app/notifications/page.tsx');
|
||||
expect(src).toMatch(/RouteStub/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user