Forward model_price_locked to the hero — a paywall was wearing poison's copy

Live induction on the deployed landing page caught this; markup review and the
unit suite both passed it. With the model leg stripped for anonymous visitors,
LiveHeroProp forwarded book/fair/model/ev/quarantine to PriceTriplet but NOT
model_price_locked, so deriveValueState fell through to the missing-model-price
branch and the card rendered "MODEL READ WITHHELD" — the quarantine state,
whose copy says we suppressed our own price because a leg is poisoned. Nothing
was poisoned. The real reason was the paywall, and the two must never share a
face: one says our data is untrustworthy, the other says you don't have this
tier.

Now forwarded, with a test asserting both the separation in deriveValueState
and the forwarding at the call site.

Tests 3559 passed / 291 suites, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
Kev
2026-07-20 19:56:21 -04:00
parent aaa41134d4
commit 4e2f488341
3 changed files with 31 additions and 1 deletions
+23
View File
@@ -261,6 +261,29 @@ describe('PriceTriplet component', () => {
});
});
// ── 6b. CONSUMERS MUST FORWARD THE LOCK FLAG ─────────────────────────────
describe('a gated model leg never wears quarantine copy', () => {
it('deriveValueState separates GATED from WITHHELD', () => {
const gated = { book_odds: -153, fair_odds: -129, model_odds: null, ev_pct: null, model_price_locked: true };
const withheld = { book_odds: -153, fair_odds: -129, model_odds: null, ev_pct: null };
expect(vs.deriveValueState(gated)).toBe(vs.STATES.NO_VERDICT_LOCKED);
expect(vs.deriveValueState(withheld)).toBe(vs.STATES.QUARANTINE);
});
it('LiveHeroProp forwards model_price_locked (live bug: it did not)', () => {
// Without this the landing hero rendered "MODEL READ WITHHELD" — poison's
// copy — to every anonymous visitor, when the real reason was the paywall.
const hero = read('web/src/components/LiveHeroProp.tsx');
expect(hero).toMatch(/model_price_locked: data\.model_price_locked/);
});
it('the grade adapter forwards it too', () => {
const { buildPriceTriplet } = require('../../web/src/lib/gradeAdapter');
const t = buildPriceTriplet({ book_odds: -153, fair_odds: -129, model_price_locked: true }).priceTriplet;
expect(t.model_price_locked).toBe(true);
});
});
// ── 7. THE SERVER GATE — fair is never the paywall ───────────────────────
describe('free-tier gate strips the model price at the wire', () => {
const { applyTierGating } = require('../../src/utils/tierGating');
+1 -1
View File
File diff suppressed because one or more lines are too long
+7
View File
@@ -39,6 +39,8 @@ type Hero = {
value?: boolean | null;
takeable?: boolean | null;
quarantine_reason?: string | null;
/** Free/anonymous: model leg stripped server-side (utils/snapshotGating). */
model_price_locked?: boolean;
};
const SPORT_LABEL: Record<string, string> = { nba: 'NBA', wnba: 'WNBA', mlb: 'MLB', soccer: 'Soccer' };
@@ -162,6 +164,11 @@ export default function LiveHeroProp() {
model_odds: data.model_odds,
ev_pct: data.ev_pct,
quarantine_reason: data.quarantine_reason,
// Session 67 — MUST be forwarded. Without it a GATED model leg
// falls through to the withheld/quarantine branch and the card
// says "we suppressed our price" when the truth is "you don't
// have this tier". A paywall must never wear poison's copy.
model_price_locked: data.model_price_locked,
}}
/>
</div>