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:
@@ -0,0 +1,72 @@
|
||||
# SPEC — WAVE 3: /compare BUILT · resolution tail SCOPED (2026-07-31)
|
||||
No grade, ledger, or scoring change (diff empty across `intelligence/`, `ledgerService`,
|
||||
`outcomeService`). Push scoring untouched.
|
||||
|
||||
## REVIEW ZERO 0.3/0.4 — THE RESOLUTION TAIL: **IT DOES NOT FIRE**
|
||||
|
||||
The resolver IS `POST /api/grading/resolve` (`src/routes/grading.js:208`, documented "called by
|
||||
the ESPN poller at FINAL", internal-key + loopback gated). Its fanout at `:356-371`:
|
||||
|
||||
| tail output | state | evidence |
|
||||
|---|---|---|
|
||||
| **share-card generation** | **SPEC'D-NOT-BUILT** (as a pipeline step) | **`grep shareCard src/routes/grading.js` = 0** — it is not in the fanout at all. `services/shareCards/renderer.js` exists with **zero callers** → the component is BUILT-NOT-FIRING, but no step would ever invoke it. |
|
||||
| **push notifications** | **BUILT-NOT-FIRING** | `webPush.sendPushToSport(...)` is in the fanout but gated on `webPush.configured()` (VAPID keys). **`push_subscriptions` = 0 rows, `user_notifications` = 0 rows** — nothing has ever been subscribed or delivered. |
|
||||
| **Telegram result posts** | **BUILT-NOT-FIRING** | `telegram.postToTelegram(...)` in the fanout, gated on `configured()` = `BOT_TOKEN && CHANNEL_ID`. |
|
||||
| **Discord result posts** | **BUILT-NOT-FIRING** | `discord.postToDiscord('results', ...)`, gated on `webhookFor('results')`. |
|
||||
| **recap (all-Final trigger)** | **SPEC'D-NOT-BUILT** | **No recap file exists anywhere in `src/`**, and `grep recap src/routes/grading.js` = 0. |
|
||||
|
||||
**🔴 AND THE WHOLE TAIL IS UNREACHABLE: nothing calls `/api/grading/resolve`.** There is **no ESPN
|
||||
poller in the repo** (`grep grading/resolve` outside the route itself = 0 hits). The live settlement
|
||||
path is the scheduler's `settleAllOutcomes` + `settleAllLedgers`, which fans out to **`opsNotify`
|
||||
only** (ops alerts to ntfy) — **no user-facing output of any kind**. So even the BUILT-NOT-FIRING
|
||||
channels have no trigger.
|
||||
|
||||
**Therefore ShareCard, /notifications, result posts and recap are ALL scoped, none shipped** — per
|
||||
the order's own rule: nothing ships as UI over a pipeline step that does not fire.
|
||||
|
||||
## PHASE 1 — /compare BUILT
|
||||
|
||||
**0.1** confirmed: the page was the honest in-development state (fabrication retired earlier).
|
||||
**0.2 semantics — a meaningful compare is a SAME-MARKET head-to-head:** two players, every row a
|
||||
measure **both sides are scored on**, aligned so the numbers are actually comparable. Deliberately
|
||||
NOT "two arbitrary graded props side by side", which is two disconnected cards wearing a compare label.
|
||||
|
||||
Built on the live feed `/api/stats/player/:name?sport=` (the same aggregate the player profile
|
||||
renders — real: Aaron Judge → `found:true`, team, BOMBER archetype, season rows `[{k,v}]`).
|
||||
`alignRows` unions both sides' stat keys and renders each side's value or `—`.
|
||||
|
||||
**Honest-absent, three ways:** a side that does not resolve renders **NO DATA** and the other side
|
||||
still shows · a measure only one side has renders **`—`, never 0** · if **neither** resolves the page
|
||||
**refuses to compare** ("we will not invent a comparison"). There is **NO VERDICT** — it shows the
|
||||
measures and explicitly says the reader draws the call.
|
||||
|
||||
## PHASES 2/3 — SCOPED, NOT SHIPPED
|
||||
|
||||
**ShareCard** — needs a generation step inside the resolve fanout (render → store → serve URL), and
|
||||
that fanout needs a trigger. Not shipped: a share card with no generator is a dead shell, and per the
|
||||
order a public marketing artifact must carry Truth-Law-honest numbers, which requires a real resolved
|
||||
result to render from.
|
||||
|
||||
**/notifications** — the store EXISTS (`push_subscriptions`, `user_notifications`, both empty) and
|
||||
`webPush` exists, but **there is no trigger and no consent/subscribe surface**. Shipping a settings
|
||||
screen now would silently drop every notification — explicitly forbidden. Scope: (1) a caller for
|
||||
`/api/grading/resolve` (or move the fanout into the scheduler's settle pass), (2) VAPID config, (3) a
|
||||
consented subscribe UI, (4) then the settings screen. Types stay **result/status-driven**
|
||||
("your graded pick resolved"), never promotional — the anti-capper line holds.
|
||||
|
||||
**Result posts / recap** — same gate. Recap has no code at all; it is a user-facing + SEO artifact, so
|
||||
it ships only when it can render real slate results with honest-absent thin data.
|
||||
|
||||
## VERIFY
|
||||
**316 suites / 3,930 tests green** (10 new), **web build exit 0**. Two pre-existing tests
|
||||
(`vyndrPhaseE`, `vyndrParityQA`) asserted the in-dev placeholder; both **superseded, not deleted** —
|
||||
they now assert the stronger properties against the real page (live fetch, no sample players, NO
|
||||
VERDICT, NO DATA, "not a zero").
|
||||
|
||||
**Chrome manifest additions:** `/compare` logged-out — enter two real players, confirm aligned rows
|
||||
and that a misspelled name reads NO DATA rather than a fabricated card.
|
||||
|
||||
## TAGS
|
||||
VERIFIED: all five tail states; /compare built on the live feed. CANNOT DETERMINE: whether VAPID /
|
||||
Telegram / Discord env is configured in prod (not visible externally) — moot while no trigger exists.
|
||||
**BLOCKED: ShareCard, /notifications, result posts, recap — all on a resolution tail that never fires.**
|
||||
Reference in New Issue
Block a user