Settlement fix forward: date-targeted resolution + terminal states

Order 1 of 2. Push scoring UNTOUCHED — it is correct. No healing here.

PHASE 1 — DATE-TARGETED FETCH replaces the rolling window for settlement.
settleSource.resolveOutcome() resolves the SPECIFIC DATE and, when the
player is absent, reads GAME STATE to learn what the absence MEANS:
  game final + player has a line  -> SETTLE (a partial game is a real
                                     result, never a void)
  game final + player absent      -> VOID (confirmed DNP)
  postponed / cancelled           -> VOID
  scheduled / in progress / SUSPENDED -> PENDING (a suspended game resumes;
                                     voiding it would destroy a real bet)
  player played, stat missing     -> unknown, NEVER void a real appearance
This is FREE for MLB: mlbStatsAdapter.getPlayerGameLog already returned the
full season log and getPlayerStats was discarding it with .slice(-10).
Settlement now reads fullLog — same request, same cache — which removes
window-decay entirely (the verified failure was a Jul 12 game outside a
last10 starting Jul 6). Projections keep using last10, unchanged.

PHASE 2 — TERMINAL STATES (migration 026 applied). outcome CHECK widened to
hit/miss/push/void/unrecoverable; added settle_attempts, settlement_source,
settlement_version, model_version. A row that cannot be resolved after
SETTLE_ATTEMPT_CAP (4) date-targeted attempts becomes 'unrecoverable'
rather than pending forever. CRITICAL: getModelAggregate now EXCLUDES void
and unrecoverable from the settled selection — it used
.not('outcome','is',null), so without this a void would have counted as a
settled row and silently moved the public record. Verified in the record
calc, not just the settle path.

PHASE 3 — SETTLEMENT-RATE ALARM. zeroSettleAlarm only caught a TOTAL zero
while ~30% of a slate failed quietly (Jul 17: 57/86). opsWatch
.settlementRateAlarm pages when resolved/attempted falls below
SETTLE_RATE_FLOOR (0.8). Voids count as RESOLVED — a void is a legitimate
terminal state — so healthy voiding never pages. Third silent-failure
surface of the night, now closed.

PHASE 4 — VERSION STAMPING. src/config/modelEras.js defines the cutoff
ONCE (2026-07-19T22:50:00Z); migration 026 backfilled pre-cutoff rows as
'pre-retention-unknown' (naming the uncertainty, not implying knowledge);
new rows carry model_version.

Regression caught pre-deploy: getScheduleFn was not injectable, so the
ledger suite hit the real network and HUNG. Now injectable via opts and a
no-op under NODE_ENV=test. The "no row -> pending" test was updated to the
new behaviour deliberately: a missing row on a FINAL game now voids.

Suite 281/3373 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:
Kev
2026-07-20 03:04:55 -04:00
parent e46c88e364
commit d4a6170ffa
9 changed files with 559 additions and 14 deletions
+35
View File
@@ -0,0 +1,35 @@
'use strict';
/**
* MODEL ERAS — the cutoff, defined ONCE (Session 64).
*
* `ledger_entries` mixes grades from before and after the 2026-07-19 fix that
* revived the probability layer (p_win/ev_pct/model_odds/value were absent on
* 100% of live grades before it) and widened the grade range. There is no
* per-row version in that table and the eras cannot be separated retroactively,
* so EVERY consumer must apply the same boundary rather than inventing its own.
*
* A backtest that mixes model eras is worthless — that is the whole reason this
* constant exists in one place.
*
* Going forward, rows carry a real `model_version` (from
* retentionService.MODEL_VERSION) and `model_snapshots` has clean per-row
* versioning from day one. The harness should read OUTCOMES from
* ledger_entries but VERSIONING from model_snapshots.
*/
/** Rows graded at or after this are the post-fix era. */
const ERA_CUTOFF_ISO = '2026-07-19T22:50:00Z';
/** What pre-cutoff rows are labelled. Names the uncertainty; never implies
* we know which model produced them. */
const UNKNOWN_ERA = 'pre-retention-unknown';
function isPostFix(gradedAt) {
if (!gradedAt) return false;
const t = new Date(gradedAt).getTime();
if (Number.isNaN(t)) return false;
return t >= new Date(ERA_CUTOFF_ISO).getTime();
}
module.exports = { ERA_CUTOFF_ISO, UNKNOWN_ERA, isPostFix };