f61ec6b391
Seven orders of measurement-first repair. The served grade does not move. A0/A1 — the unordered page walk returned the right COUNT and the wrong ROWS: 410-617 of 2,490 duplicated with an equal number never returned, while rows.length matched the server exactly. safePaginate orders on a real unique key, verifies the tuple at runtime, and THROWS on a query error instead of treating it as end-of-data. Both hits PROVES are withdrawn: they were drawn through that reader, and defense_by_direction's distinct-n was likely below the gate floor all along. A2/A2b — rolled across every reader: 11 FAIL -> 0. Composite keys pulled from pg_index (the context tables are dated-composite and had no single unique column). The unordered helper is deleted, not parked. A3 — ledgerService and retentionService defaulted the SAME env var to DIFFERENT versions, so no ledger row ever carried the marker eligibility requires. One source now. model_snapshots settlement moved onto the cron: 15,484 -> 28,894 settled, repaired-champion 0 -> 7,556. A4 — hitsFactorContext takes an as-of cutoff. Refusal over reconstruction: no row at-or-before the date means the factor does not apply, never the nearest row. Live path unchanged, proven 400/400 on real rows. A5 — factor_inputs freezes what the factor READ, never the multiplier, so an audit can recompute and check. It also recorded the finding: the three hits factors have NEVER fired. prop.opponent and prop.opposing_pitcher are read by the resolver and written by nothing. A6/A7 — matchupKeys resolves those keys from the posted lineup plus the schedule's probable pitchers, and fires the factors into a SHADOW freeze: 248 fires on 308 props, 245 of which would move the grade. The served forecast is untouched. specs/a8-shadow-factor-gate.md pre-registers the test that decides whether they ever go live. Nothing is turned on. CALIBRATION_DEPLOYED stays []. Both verdicts stay withdrawn. 4,772 tests / 371 suites green, web build exit 0, read-integrity harness 34/34. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
68 lines
2.7 KiB
JavaScript
68 lines
2.7 KiB
JavaScript
'use strict';
|
|
|
|
/** The withdrawal record — spec: specs/read-integrity-harness.md §6. */
|
|
|
|
const wv = require('../../src/services/model/withdrawnVerdicts');
|
|
|
|
describe('withdrawnVerdicts', () => {
|
|
it('records BOTH hits PROVES as withdrawn', () => {
|
|
expect(wv.isWithdrawn('mlb', 'hits', 'defense_by_direction')).toBe(true);
|
|
expect(wv.isWithdrawn('mlb', 'hits', 'pitcher_contact_profile')).toBe(true);
|
|
});
|
|
|
|
it('does not withdraw a factor that was never proven', () => {
|
|
// platoon_severity never returned PROVES (n=452 < 500). Withdrawing it would
|
|
// imply it once stood, which is its own falsehood.
|
|
expect(wv.isWithdrawn('mlb', 'hits', 'platoon_severity')).toBe(false);
|
|
expect(wv.isWithdrawn('mlb', 'hits', 'park_hits')).toBe(false);
|
|
});
|
|
|
|
it('is scoped per stat — a hits withdrawal says nothing about total_bases', () => {
|
|
expect(wv.isWithdrawn('mlb', 'total_bases', 'defense_by_direction')).toBe(false);
|
|
});
|
|
|
|
it('APPENDS rather than replaces: the original verdict and evidence survive', () => {
|
|
for (const w of wv.WITHDRAWALS) {
|
|
expect(w.original_verdict).toBe('PROVES');
|
|
expect(w.original_evidence.n).toBeGreaterThan(0);
|
|
expect(Array.isArray(w.original_evidence.ci)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('every withdrawal carries a measured cause and implicated readers', () => {
|
|
for (const w of wv.WITHDRAWALS) {
|
|
expect(w.cause).toMatch(/unordered-pagination/);
|
|
expect(w.cause).toMatch(/2026-08-09/);
|
|
expect(w.readers_implicated.length).toBeGreaterThan(0);
|
|
expect(w.status).toBe(wv.STATUS.WITHDRAWN_PENDING_REAUDIT);
|
|
}
|
|
});
|
|
|
|
it('names the eligibility arithmetic for defense_by_direction AS arithmetic', () => {
|
|
const d = wv.WITHDRAWALS.find((w) => w.factor === 'defense_by_direction');
|
|
expect(d.eligibility_note).toMatch(/422/);
|
|
expect(d.eligibility_note).toMatch(/not a re-measurement/);
|
|
});
|
|
|
|
it('records the honesty gap: both are STILL SERVED despite withdrawal', () => {
|
|
const served = wv.servedButWithdrawn();
|
|
expect(served).toHaveLength(2);
|
|
for (const w of served) expect(w.still_served_note).toMatch(/hitsFactors/);
|
|
});
|
|
|
|
it('states a reinstatement path that requires a measured clean read', () => {
|
|
for (const w of wv.WITHDRAWALS) expect(w.reinstatement).toMatch(/readIntegrity/);
|
|
});
|
|
|
|
it('is frozen — a withdrawal must not be mutated in place', () => {
|
|
expect(Object.isFrozen(wv.WITHDRAWALS)).toBe(true);
|
|
for (const w of wv.WITHDRAWALS) expect(Object.isFrozen(w)).toBe(true);
|
|
});
|
|
|
|
it('withdrawalsFor filters by sport and stat', () => {
|
|
expect(wv.withdrawalsFor('mlb', 'hits')).toHaveLength(2);
|
|
expect(wv.withdrawalsFor('mlb', 'runs')).toHaveLength(0);
|
|
expect(wv.withdrawalsFor('mlb')).toHaveLength(2);
|
|
});
|
|
});
|