Intraday is a market overlay: it may not restate belief

`grade` is a band label over `p_win` and nothing else. `servedGrade.gradeFor`
reads exactly { p_win, refused, refusal_reason, insufficient_data,
factor_adjustment } -- no line, no odds, no edge, no side -- and
`gradeFreeze.TOLERANCES.MAX_LETTER_MISMATCH` is 0, so the served letter MUST
equal the band of the served probability.

The intraday refresh re-graded an adversely-moved prop AT THE CURRENT LINE and
then copied only `res.grade` onto the row for the LOCKED line, discarding the
p_win that produced it. Two defects in one write: the letter stopped matching
the probability beside it, and the letter answered a different proposition than
the claim it was stamped on.

MEASURED on ledger_entries in the current model era
(engine1@2026-08-07-fullwindow): 30,047 unrevised rows carry ZERO letter/p_win
mismatches; all 9 intraday-revised rows are mismatched. The revision was the
sole producer of incoherent authoritative rows. The pre-cutover era is
uninterpretable here (its `grade` was the engine index, not a p_win band, and
mismatches 49.4% of the time WITHOUT any revision) -- which is exactly why the
control was run before quoting a rate.

Simulated over the 57 historical revisions: in the current era, keeping the
published grade restores coherence on 9 of 9.

So the adverse branch now records the move and stops there. No grader is
invoked, no ledger revision is applied, and the grade-rank comparator is
deleted rather than parked -- a dead comparator beside the code is how the
mutation gets re-wired. A production coherence gate compares every output row
to its input on every BELIEF field and, on any divergence, publishes the
untouched slate instead.

History is untouched: the 57 existing `revised_from_grade` rows keep their
values, and the UI that renders them is unchanged. Forward-only.

Lineage is not touched and stays OFF.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
Kev
2026-08-28 18:48:07 -04:00
parent f7cc19772b
commit 889a96584a
4 changed files with 451 additions and 96 deletions
+118 -57
View File
@@ -9,12 +9,30 @@
*
* moved WITH the grade (market chasing our number) → STEAM ▲ +N badge.
* Good for the record; entry edge compressed. NO re-grade.
* moved AGAINST ≥ 1.0 → re-grade THAT
* PROP ONLY at the current line:
* grade holds → VALUE ▲ (better number, same read)
* grade drops → PUBLIC revision: grade updates with
* revised_from_grade set (original preserved, struck through in
* the UI + ledger). Never a silent regrade — Ledger ethos.
* moved AGAINST → AGAINST, recorded
* with the signed delta and the real current line. NO RE-GRADE.
*
* -- WHY THE RE-GRADE IS GONE (INTRADAY BELIEF INTEGRITY) ----------------
* `grade` is not a market field. `servedGrade.gradeFor` reads exactly
* { p_win, refused, refusal_reason, insufficient_data, factor_adjustment } —
* no line, no odds, no edge, no side — so the letter is a BAND LABEL OVER
* p_win and nothing else. `gradeFreeze.TOLERANCES.MAX_LETTER_MISMATCH` is 0:
* the served letter MUST equal the band of the served probability.
*
* The old branch re-graded at the CURRENT line and then wrote only
* `res.grade` onto the row for the LOCKED line, discarding the p_win that
* produced it. That is two defects at once: the letter no longer matched the
* probability beside it, and the letter answered a DIFFERENT proposition
* (line L2) than the claim it was stamped on (line L1).
*
* MEASURED on `ledger_entries` in the current model era
* (engine1@2026-08-07-fullwindow): 30,047 unrevised rows had ZERO
* letter/p_win mismatches; all 9 intraday-revised rows were mismatched.
* The revision was the sole producer of incoherent authoritative rows.
*
* So intraday is MARKET-OVERLAY ONLY. It may record what the market did; it
* may not restate what the model believes. A new belief needs a new published
* claim, which is a snapshot's job, not a refresh's.
*
* Every displayed line stays a REAL book value from this refresh — the
* refresh CAPTURES market numbers, never computes them. Each run also
@@ -24,8 +42,7 @@
* QUOTA MATH (zero out-of-pocket): one getOdds call per sport per run.
* 20-min cadence × 12 slate hours = 36 runs/day/sport × 4 sports =
* ≤144 PropLine requests/day — against 9,000/day free capacity (3 keys ×
* 3,000). Re-grades are internal feature computation (free) and bounded
* to props that moved against ≥ 1.0.
* 3,000). No grading is invoked at all — the refresh reads the market.
*
* Everything injectable → unit-tested with zero network.
*/
@@ -40,8 +57,50 @@ const { isModelBook } = require('../config/bookRoles');
const HISTORY_CAP = 24; // {t, line} points per grade (S6 sparklines)
const GRADE_RANK = { 'A+': 0, A: 1, 'A-': 2, 'B+': 3, B: 4, 'B-': 5, 'C+': 6, C: 7, 'C-': 8, D: 9, F: 10 };
const rank = (g) => (g && GRADE_RANK[g] !== undefined ? GRADE_RANK[g] : 99);
/**
* FIELDS THIS REFRESH MAY NEVER WRITE.
*
* Kept as a LOCAL constant rather than imported from the lineage taxonomy so
* the market path carries no dependency on the shadow recorder — but a test
* cross-checks it against `readLineage.CLAIM_FIELD_CLASSES` BELIEF, so drift
* between the two is a suite failure rather than a silent divergence.
*
* `line` and `direction` are included because they identify WHICH proposition
* the belief is about: changing either would move the claim to a different
* question rather than answer the same one differently. The real current line
* is reported as `movement.currentLine`, never by overwriting the locked one.
*/
const BELIEF_FIELDS = Object.freeze([
'grade', 'engine_grade', 'served_grade', 'revised_from_grade',
'p_win', 'projection', 'confidence', 'confidence_basis',
'fair_prob', 'fair_odds', 'model_odds', 'kelly',
'refused', 'insufficient_data', 'refusal_reason',
'line', 'direction',
]);
/**
* Count fields the overlay changed that it had no right to change.
* Returns the violations rather than a bare boolean — a guard that cannot name
* what it caught is hard to act on. Used by the refresh itself (reported as
* `belief_mutations`) and asserted at 0 by the coherence suite.
*/
function beliefMutations(before, after) {
const out = [];
const b = before || {};
const a = after || {};
for (const f of BELIEF_FIELDS) {
const x = b[f] === undefined ? null : b[f];
const y = a[f] === undefined ? null : a[f];
if (JSON.stringify(x) !== JSON.stringify(y)) out.push({ field: f, before: x, after: y });
}
return out;
}
// NOTE: the grade-rank comparison table that used to live here is GONE, not
// parked. Under the market-overlay contract nothing in this file may compare or
// choose a letter, and a dead comparator sitting beside the code is how the
// mutation gets re-wired. The guard against its return is the coherence suite,
// not a helper kept warm.
const sideOver = (dir) => String(dir || 'over').toLowerCase() !== 'under';
@@ -104,13 +163,14 @@ function signedDelta(side, lockedLine, currentLine) {
/**
* Run one intraday refresh for a sport. Returns
* { sport, status, checked, steam, value, revised } — never throws.
* { sport, status, checked, steam, against, belief_mutations } — never throws.
* `status: 'blocked'` means the coherence gate refused to publish an overlay
* that would have mutated belief; the previous slate is left standing.
*/
async function runIntradayRefresh(sport, opts = {}) {
const sp = String(sport || '').toLowerCase();
const deps = {
getOdds: opts.getOdds || require('./oddsService').getOdds,
analyze: opts.analyze || require('./intelligence/analyzeViaEngine1').analyzeViaEngine1,
cacheGet: opts.cacheGet || require('../utils/redis').cacheGet,
cacheSet: opts.cacheSet || require('../utils/redis').cacheSet,
ledger: opts.ledger || require('./ledgerService'),
@@ -121,22 +181,22 @@ async function runIntradayRefresh(sport, opts = {}) {
const snap = await deps.cacheGet(`snapshot:${sp}:latest`);
if (!snap || !Array.isArray(snap.grades) || snap.grades.length === 0) {
return { sport: sp, status: 'skipped', reason: 'no snapshot', checked: 0, steam: 0, value: 0, revised: 0 };
return { sport: sp, status: 'skipped', reason: 'no snapshot', checked: 0, steam: 0, against: 0, belief_mutations: 0 };
}
let odds;
try {
odds = await deps.getOdds(sp);
} catch (e) {
return { sport: sp, status: 'error', reason: e.message, checked: 0, steam: 0, value: 0, revised: 0 };
return { sport: sp, status: 'error', reason: e.message, checked: 0, steam: 0, against: 0, belief_mutations: 0 };
}
const props = odds && Array.isArray(odds.props) ? odds.props : [];
if (props.length === 0) {
return { sport: sp, status: 'skipped', reason: 'no odds', checked: 0, steam: 0, value: 0, revised: 0 };
return { sport: sp, status: 'skipped', reason: 'no odds', checked: 0, steam: 0, against: 0, belief_mutations: 0 };
}
const byKey = indexOddsProps(props);
let steam = 0; let value = 0; let revised = 0; let checked = 0;
let steam = 0; let against = 0; let checked = 0;
const moveEvents = [];
const grades = [];
@@ -171,47 +231,46 @@ async function runIntradayRefresh(sport, opts = {}) {
continue;
}
// Moved AGAINST the grade.
if (Math.abs(delta) < REGRADE_TRIGGER) {
grades.push(withHist({ ...g, movement: { kind: 'against', delta, currentLine: current, at: ts } }));
continue;
// Moved AGAINST the grade — recorded, never re-graded.
//
// A market move is a fact about the market. It does not tell us the model
// now believes something different; it tells us the price of the same
// belief changed. The magnitude rides on `delta` and the real book number
// on `currentLine`, so nothing is lost by declining to relabel the letter.
against += 1;
grades.push(withHist({ ...g, movement: { kind: 'against', delta, currentLine: current, at: ts } }));
// A big move is still newsworthy: the ticker event fires at the same
// threshold it always did.
if (Math.abs(delta) >= REGRADE_TRIGGER) {
moveEvents.push(moveEvent(sp, g, locked, current, ts));
}
}
// ≥ 1.0 against → re-grade THIS PROP ONLY at the current (real) line.
let res = null;
try {
res = await deps.analyze({
player, stat_type: stat, line: current,
direction: g.direction || 'over', sport: sp, book: prop.book,
});
} catch { /* re-grade unavailable → treated as hold below */ }
moveEvents.push(moveEvent(sp, g, locked, current, ts));
if (!res || !res.grade || res.insufficient_data || rank(res.grade) <= rank(g.grade)) {
// Grade holds (or the model refuses to re-read) → better entry, same read.
value += 1;
grades.push(withHist({ ...g, movement: { kind: 'value', delta, currentLine: current, at: ts } }));
continue;
}
// Grade DROPS → public revision. Original grade preserved once, forever.
revised += 1;
const fromGrade = g.revised_from_grade || g.grade;
grades.push(withHist({
...g,
grade: res.grade,
revised_from_grade: fromGrade,
movement: { kind: 'revised', delta, currentLine: current, at: ts },
}));
try {
await deps.ledger.applyRevision(sp, {
playerKey: nameKey(player), stat, line: Number(locked),
side: sideOver(g.direction) ? 'over' : 'under',
newGrade: res.grade, fromGrade,
});
} catch (e) {
console.warn(`[intraday] ledger revision failed for ${player}:`, e.message);
}
// COHERENCE GATE. Every output row is compared to the row it came from on
// every belief field. This is not a test double's assertion — it runs in
// production and reports itself, because the failure it guards against
// (a new letter beside an old probability) is invisible in any single row.
const beliefIndex = new Map();
for (const g of snap.grades) {
const k = `${nameKey(g.player || g.player_name)}|${String(g.stat_type || g.stat || '').toLowerCase()}|${g.line}|${g.direction}`;
if (!beliefIndex.has(k)) beliefIndex.set(k, g);
}
const violations = [];
for (const g of grades) {
const k = `${nameKey(g.player || g.player_name)}|${String(g.stat_type || g.stat || '').toLowerCase()}|${g.line}|${g.direction}`;
const before = beliefIndex.get(k);
if (!before) continue;
for (const v of beliefMutations(before, g)) violations.push({ key: k, ...v });
}
if (violations.length > 0) {
// Fail CLOSED: publish the untouched slate rather than an incoherent one.
// The overlay is a convenience; a torn authoritative Read is not.
console.warn(`[intraday] BELIEF MUTATION BLOCKED for ${sp}: ${violations.length} field(s)`,
JSON.stringify(violations.slice(0, 5)));
return {
sport: sp, status: 'blocked', reason: 'belief_mutation',
checked, steam, against, belief_mutations: violations.length,
};
}
// Write back: refreshed movement state + the higher-fidelity close.
@@ -249,7 +308,7 @@ async function runIntradayRefresh(sport, opts = {}) {
await deps.pushTickerItems(moveEvents.slice(0, TICKER_MOVE_CAP), deps);
}
return { sport: sp, status: 'ok', checked, steam, value, revised };
return { sport: sp, status: 'ok', checked, steam, against, belief_mutations: 0 };
}
function moveEvent(sport, g, locked, current, ts) {
@@ -283,5 +342,7 @@ module.exports = {
runIntradayRefresh,
runAllIntradayRefreshes,
inSlateHours,
__internals: { signedDelta, indexOddsProps, moveEvent, rank, trackHistory, STEAM_NOISE, REGRADE_TRIGGER, HISTORY_CAP },
BELIEF_FIELDS,
beliefMutations,
__internals: { signedDelta, indexOddsProps, moveEvent, trackHistory, beliefMutations, BELIEF_FIELDS, STEAM_NOISE, REGRADE_TRIGGER, HISTORY_CAP },
};
+7 -2
View File
@@ -383,8 +383,13 @@ function startSnapshotScheduler(opts = {}) {
// Soccer is excluded: a 20-min odds-api refresh would drain the 500/mo key.
const results = await refreshAll({ sports: cadence.intradaySports() });
const touched = results.filter((r) => r.status === 'ok');
const revised = results.reduce((n, r) => n + (r.revised || 0), 0);
if (touched.length > 0) console.log(`[intraday] refresh — ${touched.length} sports, ${revised} public revisions`);
// Intraday is a MARKET OVERLAY: it reports what the market did, never a
// revised belief. `blocked` is the coherence gate refusing to publish an
// overlay that would have mutated a published claim — it must be visible.
const against = results.reduce((n2, r) => n2 + (r.against || 0), 0);
const blocked = results.filter((r) => r.status === 'blocked').length;
if (touched.length > 0) console.log(`[intraday] refresh — ${touched.length} sports, ${against} adverse moves recorded`);
if (blocked > 0) console.warn(`[intraday] ${blocked} sport(s) BLOCKED by the belief-coherence gate`);
} catch (e) {
console.warn('[intraday] refresh failed:', e.message);
}