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:
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user