Render directional-CLV badge — Analyst+Desk, server-gated, receipt-bearing
CARD BADGE ONLY. Ticker-CLV explicitly DEFERRED (named, not lost).
PHASE 1 — SERVER-SIDE GATE AT THE DATA LAYER. A Free request never
RECEIVES dclv data: the CLV columns are appended to the SELECT only behind
canAccess(tier,'clv_badge') (new capability, analyst+desk), and responses
are ALSO stripped as defence in depth so a future SELECT change cannot
quietly leak. No CSS/client gate — data that reaches the browser has left
the building. dclv_fair_lock/fair_close are de-vig internals and are never
sent at all.
SURFACE AUDIT, all six channels, each test-locked to contain no CLV:
public profile (share link), snapshot/card feed, ticker feed, share
card/OG, embeddable widget, newsletter. A test also asserts no
ledger_entries read uses select('*') — a star would auto-leak every new
column, which is exactly how a gate becomes theatre.
PHASE 2 — IMMUTABLE ONCE COMPUTED. A settle can re-run (stat correction,
protested game) and a badge that flips positive->negative AFTER a user saw
or screenshotted it is a credibility failure. First computation wins: dclv
is only computed when dclv_computed_at is null, so a re-settle can never
rewrite a shown badge. Same discipline as the locked grade.
PHASE 3 — RENDER, test-first, ABSENCE IS HONEST. unknown / flat / null /
missing-receipt all render NOTHING — no element, no placeholder, no
"pending". Proven on an ALL-NULL board (today: 0 badges) and a MIXED board
(tomorrow: 1 of 4 badged, badge-less cards clean). Binary states only:
positive -> MOVED TOWARD US "graded -110 · closed -145"
negative -> MOVED AWAY "graded -110 · closed +120"
The RECEIPT is the persuasive part, so a badge with no numbers is
suppressed rather than shown as a bare claim. Negative is neutral context
and NEVER touches the locked grade — no back-door re-grading.
NO aggregate, count or rollup exists by construction: the module exports
exactly {clvBadge, fmtPrice} and a badge payload carries exactly
{tone,label,receipt} — asserted by test, because an on-screen tally would
be the held aggregate claim through the side door.
Build gotcha hit and fixed: clvBadge is CommonJS (allowJs) with no TS
types, so the .tsx needed an explicit cast at the call site — the build
worker exits 1 on type errors even though compilation "succeeds".
Suite 288/3473 green, build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { clvBadge } from '@/lib/clvBadge';
|
||||
|
||||
/**
|
||||
* Per-read directional-CLV badge (Session 64). Analyst + Desk only — the data
|
||||
* itself is withheld server-side for Free, so this component simply never
|
||||
* receives a state to render.
|
||||
*
|
||||
* ABSENCE IS HONEST: unknown / flat / no-receipt return null — no element, no
|
||||
* placeholder, no "pending". A mixed board (some reads badged, most not) is the
|
||||
* real state as closes land game-by-game, and a badge-less card must not read
|
||||
* as worse or broken.
|
||||
*
|
||||
* NEGATIVE IS NEUTRAL INFORMATION, NOT A DEMOTION. It never touches the locked
|
||||
* grade — the grade is locked at first capture and CLV is a separate signal.
|
||||
*
|
||||
* No aggregate, count or rollup exists here by construction.
|
||||
*/
|
||||
type ClvRead = { dclv_state?: string | null; locked_odds?: string | number | null; closing_odds?: string | number | null } | null | undefined;
|
||||
type Badge = { tone: string; label: string; receipt: string } | null;
|
||||
|
||||
export default function ClvBadge({ read }: { read: ClvRead }) {
|
||||
// clvBadge is CommonJS (allowJs) so it has no TS types — cast at the call
|
||||
// site, exactly as gradeAdapter is handled elsewhere.
|
||||
const badge = clvBadge(read || {}) as Badge;
|
||||
if (!badge) return null; // the honest non-element
|
||||
|
||||
const confirm = badge.tone === 'confirm';
|
||||
return (
|
||||
<span
|
||||
className="mono"
|
||||
title={confirm
|
||||
? 'The market moved toward the side we graded, between our lock and the close.'
|
||||
: 'The market moved away from the side we graded. The grade is unchanged — this is context, not a downgrade.'}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
fontSize: 10, letterSpacing: '.06em', textTransform: 'uppercase',
|
||||
padding: '2px 7px', borderRadius: 3, whiteSpace: 'nowrap',
|
||||
color: confirm ? 'var(--g-a, #00D4A0)' : 'var(--amber, #FFB347)',
|
||||
background: confirm ? 'rgba(0,212,160,.10)' : 'rgba(255,179,71,.10)',
|
||||
border: `1px solid ${confirm ? 'rgba(0,212,160,.30)' : 'rgba(255,179,71,.30)'}`,
|
||||
}}
|
||||
>
|
||||
<span>{badge.label}</span>
|
||||
{/* The RECEIPT is the persuasive part — the label alone is just a claim. */}
|
||||
<span style={{ opacity: 0.75, textTransform: 'none', letterSpacing: 0 }}>
|
||||
{badge.receipt}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -61,3 +61,4 @@ export {
|
||||
gradeHex,
|
||||
gradeBadgeSize,
|
||||
} from '@/lib/vyndrTokens';
|
||||
export { default as ClvBadge } from './ClvBadge';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Directional-CLV badge model (Session 64). CommonJS so it is unit-testable
|
||||
* and shared by the card without a React import.
|
||||
*
|
||||
* ABSENCE IS HONEST. unknown / flat / null / missing render NOTHING — not a
|
||||
* placeholder, not a zero, not "pending". A board where some reads are badged
|
||||
* and others are not is the REAL state (closes are captured per game as each
|
||||
* locks), so a badge-less card must never look broken or worse.
|
||||
*
|
||||
* THE RECEIPT IS THE POINT. "MOVED TOWARD US" alone is a claim; "graded −110 ·
|
||||
* closed −145" is evidence. The numbers are what make it persuasive, so the
|
||||
* badge always carries them and is suppressed if they are missing.
|
||||
*
|
||||
* NO AGGREGATE. There is deliberately no count, rollup, or "N reads moved
|
||||
* toward us" — an on-screen tally is the held aggregate claim through the side
|
||||
* door. Per-read only.
|
||||
*/
|
||||
|
||||
/** Format an American price for the receipt. */
|
||||
function fmtPrice(v) {
|
||||
if (v == null || v === '') return null;
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return null;
|
||||
return n > 0 ? `+${Math.round(n)}` : `${Math.round(n)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} read { dclv_state, locked_odds, closing_odds }
|
||||
* @returns {null|{tone,label,receipt}} null = render NOTHING
|
||||
*/
|
||||
function clvBadge(read) {
|
||||
if (!read) return null;
|
||||
const state = read.dclv_state;
|
||||
// flat and unknown are both silence — a held line is not news, and a missing
|
||||
// close is not a zero.
|
||||
if (state !== 'positive' && state !== 'negative') return null;
|
||||
|
||||
const graded = fmtPrice(read.locked_odds);
|
||||
const closed = fmtPrice(read.closing_odds);
|
||||
// No receipt → no badge. A bare label is an assertion without evidence.
|
||||
if (!graded || !closed) return null;
|
||||
|
||||
return {
|
||||
tone: state === 'positive' ? 'confirm' : 'caution',
|
||||
label: state === 'positive' ? 'MOVED TOWARD US' : 'MOVED AWAY',
|
||||
receipt: `graded ${graded} · closed ${closed}`,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { clvBadge, fmtPrice };
|
||||
Reference in New Issue
Block a user