diff --git a/src/services/lineageCoverage.js b/src/services/lineageCoverage.js index b988e11..c3920a7 100644 --- a/src/services/lineageCoverage.js +++ b/src/services/lineageCoverage.js @@ -190,6 +190,7 @@ async function auditLatestCohort(sport, deps = {}) { const supabase = getClient(); if (!supabase) return Object.freeze({ audit_available: false, reason: 'no_supabase_env', sport: sp, health: HEALTH.AUDIT_UNAVAILABLE }); + // The head identifies the COHORT (snapshot_id), not one date within it. const { data: head, error: headErr } = await supabase .from('model_snapshots') .select('snapshot_id, game_date, captured_at, code_sha') @@ -202,15 +203,22 @@ async function auditLatestCohort(sport, deps = {}) { const h = head[0]; const { paginate } = require('../utils/safePaginate'); + // A COHORT IS A snapshot_id, NOT A DATE SLICE OF ONE. + // + // Measured on the 2026-08-31 03:00Z run: the cohort held 1,970 rows across + // TWO game_dates (1,572 on 08-31 and 398 on 08-30, the late slot spanning + // midnight ET). Filtering to the head row's date measured 646 of 801 + // eligible keys and still reported HEALTHY — so a whole date slice with + // zero lineage would have been invisible. The audit covers the cohort. const rows = await paginate(() => supabase .from('model_snapshots') .select(AUDIT_COLUMNS) .eq('snapshot_id', h.snapshot_id) - .eq('sport', sp) - .eq('game_date', h.game_date), { key: 'id', label: 'lineageCoverage.auditLatestCohort' }); + .eq('sport', sp), { key: 'id', label: 'lineageCoverage.auditLatestCohort' }); const audit = auditRows(rows, { - snapshot_id: h.snapshot_id, sport: sp, game_date: h.game_date, + snapshot_id: h.snapshot_id, sport: sp, + game_date: [...new Set(rows.map((r) => r.game_date))].sort().join(','), code_sha: h.code_sha, captured_at: h.captured_at, retention_terminal: deps.retentionTerminal ?? null, }); diff --git a/tests/unit/lineageProductization.test.js b/tests/unit/lineageProductization.test.js index c04ef60..0d0cda9 100644 --- a/tests/unit/lineageProductization.test.js +++ b/tests/unit/lineageProductization.test.js @@ -354,6 +354,26 @@ describe('coverage observer', () => { .toEqual([...retention.VALID_LINEAGE_ACTION_FIELDS].sort()); }); + test('a cohort spanning two game_dates is audited whole, not one slice', () => { + // The real 2026-08-31 03:00Z shape: a late slot spanning midnight ET. Rows + // from BOTH dates are eligible and both must be counted, or a date slice + // with no lineage stays invisible behind a HEALTHY verdict. + const mk = (d, p, covered) => ({ + sport: 'mlb', game_date: d, player_key: p, stat: 'hits', side: 'over', + line: 0.5, published: true, + ...(covered ? { + read_id: `r-${p}`, read_natural_key: `mlb|${d}|${p}|hits|over|0.500000`, + lineage_action: 'ORIGIN', claim_digest: 'd', revision_ordinal: 0, + lineage_state: 's', lineage_version: 'lin@1', claim_schema_version: 'claim@1', + digest_algorithm_version: 'sha256-json-sorted@1', + } : {}), + }); + const a = coverage.auditRows([mk('2026-08-31', 'a', true), mk('2026-08-30', 'b', false)]); + expect(a.expected_keys).toBe(2); + expect(a.covered_keys).toBe(1); + expect(a.health).toBe(coverage.HEALTH.PARTIAL_COVERAGE); + }); + test('an unavailable audit is not a coverage claim', () => { expect(coverage.classify({ audit_available: false })).toBe(coverage.HEALTH.AUDIT_UNAVAILABLE); });