# 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.** ## POST-DEPLOY FINGERPRINT (commit `bf7c0a3`) **Deploy boundary captured:** attempts 1-4 still served `IN DEVELOPMENT`, attempt 5 flipped — the real page is live (`Enter two players`, `nothing here is modelled`), placeholder gone. **Live head-to-head, through the deployed proxy (Aaron Judge vs Shohei Ohtani, MLB):** .248 AVG .289 17 HR 23 38 RBI 65 .908 OPS .931 59 GP 103 → 5 of 5 shared measures, both found:true **Honest-absent verified live:** a bogus name returns `found:false` → renders NO DATA, no fabricated card. **⚠️ Verification-harness error worth recording:** my first check reported `found=false` for both real players. That was **my script**, not the service — its `except` branch returned `{found:False}` and swallowed a urllib-side failure. curl through **both** the proxy and Express returns `found:true` with 5 season rows, 3/3 repeats. Lesson: a bare except in a verification harness fabricates a negative result, which is the same class of bug as `Number(null) === 0` — the harness must fail loudly, not return a plausible-looking false.