e5b20b0509
Three closeout items, traced before building.
THE OBSERVER WAS DIAGNOSTICS, NOT MONITORING. Traced from the deployed tree:
exactly two callsites, both manual internal routes, and nothing in the scheduler
or ops path consumed it. A persistent lineage failure could have sat unnoticed
until a human asked.
The monitor now runs on the scheduler's per-minute tick, throttled to 30
minutes. It is placed there rather than after a snapshot on purpose: an
in-snapshot audit structurally cannot report that no snapshot ran, which is the
failure mode that matters most, and it would run under peak write contention
where the audit already demonstrably times out. It reads only the durable
observer and never `attachLineage`'s counters, and every failure path is
swallowed — a monitor that can take down the pipeline it watches is worse than
no monitor.
HEALTHY IS SILENCE; EVERYTHING ELSE SPEAKS. `coverageAlarm` is pure, so the
policy is testable and cannot drift into the scheduler. AUDIT_UNAVAILABLE says
"could not be measured — the audit did not run", deliberately worded so it can
never be read as "coverage is zero": those are different claims and collapsing
them is how a monitor starts lying in the reassuring direction. Alerts dedupe on
(health, cohort) so a standing fault states itself once and a NEW cohort with
the same fault speaks again.
ANCESTRY BECOMES A PRODUCT CONTRACT. It was internal-only. `GET
/api/ancestry/ledger/:id` (requireAuth, rate-limited) plus the Next proxy that
makes it browser-reachable, keyed on the LEDGER ROW ID — a stable identifier the
ledger API already returns — rather than a raw natural key exposed because it
was convenient. Its own router, so `routes/ledger.js` stays free of lineage
entirely and the grade-badge guard keeps its teeth. Every response declares
`authority: LOCAL, authority_scope: ANCESTRY_ONLY`.
THREE TEETH CAME BACK GREEN AND ALL THREE WERE MY TESTS, NOT SAFE DEFECTS.
The badge guard was CASE-SENSITIVE, so `LINEAGE_ANCESTRY` and `readAncestry`
walked straight past it. `try/finally` is valid JavaScript, so removing the
monitor's catch produced no load error and nothing asserted the containment.
And the multi-date cohort check was a grep for `.gte('game_date'` that the
head query satisfied on its own. All three replaced with behavioural tests,
including a scheduler double whose fake client HONOURS its filters — a
pass-through would have made a narrowed cohort walk look correct.
Suite 398/5,522/0 · tsc 0 · web build 0 · teeth 14/14 and 15/15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
520 lines
24 KiB
JavaScript
520 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* READ LINEAGE — append-only chronology of published claims.
|
|
*
|
|
* Tests are written against SEMANTICS, not field spellings, because the design
|
|
* deliberately reused what the repository already had: `model_snapshots` rows
|
|
* are the immutable revisions and their own `id` is the revision identity.
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const L = require('../../src/services/read/readLineage');
|
|
const retention = require('../../src/services/retentionService');
|
|
|
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
|
|
// `published: true` is REQUIRED as of the publication-truth work. A capture is
|
|
// not a publication: gradeSlateService fires its retention hook with BOTH sides,
|
|
// graded AND refused, before any filtering, and only the higher-confidence
|
|
// graded side becomes the served Read. Measured on mlb 2026-08-26, 8,443 of
|
|
// 13,012 captured rows (64.9%) describe a state no user was ever shown.
|
|
//
|
|
// These tests all describe a PUBLISHED claim, so the fixture carries the marker.
|
|
// Coverage of the unmarked case lives in publicationLineage.test.js.
|
|
const CLAIM = {
|
|
sport: 'mlb', game_date: '2026-08-27', player_key: 'aaron judge',
|
|
stat: 'hits', side: 'over', line: 0.5,
|
|
game_id: 'mlb:2026-08-27:BostonRedSox@NewYorkYankees',
|
|
captured_at: '2026-08-27T14:00:00Z',
|
|
grade: 'B', p_win: 0.61, confidence: 61, projection: 1.2, takeable: true,
|
|
published: true,
|
|
};
|
|
|
|
/** A persisted row, as the dual-write would have left it. */
|
|
const persisted = (id, claim, res) => ({
|
|
id,
|
|
read_id: res.read_id,
|
|
read_natural_key: res.read_natural_key,
|
|
game_id: claim.game_id,
|
|
claim_digest: res.claim_digest,
|
|
revision_ordinal: res.revision_ordinal,
|
|
lineage_action: res.action,
|
|
supersedes_id: res.supersedes_id ?? null,
|
|
captured_at: claim.captured_at,
|
|
});
|
|
|
|
const mint = (v) => () => v;
|
|
|
|
describe('ORIGINAL PUBLICATION', () => {
|
|
test('a new Read creates one logical Read and one immutable initial revision', () => {
|
|
const r = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
expect(r.ok).toBe(true);
|
|
expect(r.action).toBe(L.LINEAGE_ACTION.ORIGIN);
|
|
expect(r.read_id).toBe('R1');
|
|
expect(r.revision_ordinal).toBe(0);
|
|
expect(r.supersedes_id).toBeNull();
|
|
expect(r.claim_digest).toEqual(expect.any(String));
|
|
});
|
|
});
|
|
|
|
describe('SECOND PUBLICATION', () => {
|
|
test('a materially different claim creates a second revision pointing at the first', () => {
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const changed = { ...CLAIM, grade: 'C', p_win: 0.55, captured_at: '2026-08-27T19:00:00Z' };
|
|
const r2 = L.resolveLineage({ candidate: changed, existing: [row1] });
|
|
|
|
expect(r2.action).toBe(L.LINEAGE_ACTION.REVISION);
|
|
expect(r2.read_id).toBe('R1');
|
|
expect(r2.revision_ordinal).toBe(1);
|
|
expect(r2.supersedes_id).toBe(101);
|
|
expect(r2.claim_digest).not.toBe(r1.claim_digest);
|
|
});
|
|
});
|
|
|
|
describe('NO MUTATION', () => {
|
|
test('creating revision 2 leaves revision 1 byte-for-byte unchanged', () => {
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const before = JSON.stringify(row1);
|
|
L.resolveLineage({
|
|
candidate: { ...CLAIM, grade: 'C', p_win: 0.55, captured_at: '2026-08-27T19:00:00Z' },
|
|
existing: [row1],
|
|
});
|
|
expect(JSON.stringify(row1)).toBe(before);
|
|
expect(Object.isFrozen(r1)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('THIRD REVISION', () => {
|
|
test('a deterministic chain R1 -> R2 -> R3 forms and walks', () => {
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const c2 = { ...CLAIM, grade: 'C', p_win: 0.55, captured_at: '2026-08-27T19:00:00Z' };
|
|
const r2 = L.resolveLineage({ candidate: c2, existing: [row1] });
|
|
const row2 = persisted(102, c2, r2);
|
|
const c3 = { ...CLAIM, grade: 'D', p_win: 0.49, captured_at: '2026-08-27T22:00:00Z' };
|
|
const r3 = L.resolveLineage({ candidate: c3, existing: [row1, row2] });
|
|
const row3 = persisted(103, c3, r3);
|
|
|
|
expect(r3.revision_ordinal).toBe(2);
|
|
expect(r3.supersedes_id).toBe(102);
|
|
|
|
const chron = L.chronology([row3, row1, row2]);
|
|
expect(chron.ok).toBe(true);
|
|
expect(chron.revisions.map((r) => r.revision_ordinal)).toEqual([0, 1, 2]);
|
|
expect(chron.revision_count).toBe(2);
|
|
expect(chron.original.id).toBe(101);
|
|
expect(chron.current.id).toBe(103);
|
|
});
|
|
|
|
test('a broken supersession chain is reported, not silently reordered', () => {
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const orphan = { ...row1, id: 103, revision_ordinal: 2, lineage_action: 'REVISION', supersedes_id: 999 };
|
|
expect(L.chronology([row1, orphan]).ok).toBe(false);
|
|
expect(L.chronology([row1, orphan]).reason).toMatch(/broken supersession/);
|
|
});
|
|
});
|
|
|
|
describe('EXACT READ IDENTITY', () => {
|
|
test('two genuinely different games do not share one logical Read', () => {
|
|
// Real prod shape: `jac caglianone` on 2026-08-22 under two different
|
|
// matchups. Same player, stat, line and side; different events.
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const otherGame = { ...CLAIM, game_id: 'mlb:2026-08-27:MinnesotaTwins@SanDiegoPadres' };
|
|
const r2 = L.resolveLineage({ candidate: otherGame, existing: [row1], mintReadId: mint('R2') });
|
|
|
|
expect(r2.action).toBe(L.LINEAGE_ACTION.ORIGIN);
|
|
expect(r2.read_id).toBe('R2');
|
|
expect(r2.supersedes_id).toBeNull();
|
|
});
|
|
|
|
test('an incomplete identity refuses rather than keying on a partial tuple', () => {
|
|
for (const missing of ['sport', 'game_date', 'player_key', 'stat', 'side']) {
|
|
const bad = { ...CLAIM, [missing]: null };
|
|
expect(L.resolveLineage({ candidate: bad, existing: [] }).refused).toBe('incomplete_read_identity');
|
|
}
|
|
expect(L.readNaturalKey({ ...CLAIM, line: null })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('UNSTABLE GAME-ID SPELLING', () => {
|
|
test('two spellings of one game stay ONE logical Read', () => {
|
|
// Measured: 1,328 of 30,746 identity groups carry more than one spelling.
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
// A REAL spelling pair from the data: measured over 52 distinct spelling
|
|
// pairs in one week, 46 are prefix-compatible like this one.
|
|
const abbreviated = {
|
|
...CLAIM, game_id: 'mlb:2026-08-27:BostonRed@NewYork',
|
|
grade: 'C', p_win: 0.55, captured_at: '2026-08-27T19:00:00Z',
|
|
};
|
|
const r2 = L.resolveLineage({ candidate: abbreviated, existing: [row1] });
|
|
expect(r2.read_id).toBe('R1');
|
|
expect(r2.action).toBe(L.LINEAGE_ACTION.REVISION);
|
|
});
|
|
|
|
test('the prefix rule matches abbreviations and refuses unrelated pairs', () => {
|
|
expect(L.sameEvent('nba:2026-10-20:OKC@SAS', 'nba:2026-10-20:OKCThunder@SASpurs')).toBe(true);
|
|
expect(L.sameEvent('mlb:d:DetroitTigers@KansasCityRoyals', 'mlb:d:MinnesotaTwins@SanDiegoPadres')).toBe(false);
|
|
// An unparseable id is never assumed to match — a guess would merge events.
|
|
expect(L.sameEvent('garbage', 'mlb:d:A@B')).toBe(false);
|
|
expect(L.eventFingerprint('garbage')).toBeNull();
|
|
});
|
|
|
|
test('KNOWN LIMIT: a non-prefix initialism is NOT reconciled, and fails closed', () => {
|
|
// 'NYY' is not a prefix of 'newyorkyankees', so the rule cannot match them
|
|
// and the two spellings become two logical Reads. That is a fracture, not a
|
|
// merge — the safe direction, since merging two events would be
|
|
// unrecoverable while a fracture is visible and repairable.
|
|
//
|
|
// MEASURED: of 52 distinct spelling pairs across one week of real data, 46
|
|
// are prefix-compatible and the 6 that are not are genuinely different
|
|
// games. No initialism-style pair for one game occurred. Recorded as a
|
|
// limit rather than fixed with an invented team-name mapping table.
|
|
expect(L.sameEvent('mlb:d:BostonRedSox@NewYorkYankees', 'mlb:d:BOS@NYY')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('RETRY / IDEMPOTENCY', () => {
|
|
test('retrying the same publication does not create a duplicate revision', () => {
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const retry = L.resolveLineage({ candidate: { ...CLAIM }, existing: [row1] });
|
|
expect(retry.action).toBe(L.LINEAGE_ACTION.RECAPTURE);
|
|
expect(retry.revision_ordinal).toBe(0);
|
|
expect(retry.supersedes_id).toBeNull();
|
|
expect(retry.recaptures_id).toBe(101);
|
|
});
|
|
|
|
test('a later cycle observing the SAME claim is a recapture, not a revision', () => {
|
|
// On the measured date only 183 of 5,376 props changed while 4,286 had
|
|
// multiple rows. Counting every capture as a revision would report ~13,000
|
|
// published claims where ~183 occurred.
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const later = { ...CLAIM, captured_at: '2026-08-27T22:00:00Z' };
|
|
expect(L.resolveLineage({ candidate: later, existing: [row1] }).action)
|
|
.toBe(L.LINEAGE_ACTION.RECAPTURE);
|
|
});
|
|
|
|
test('the digest ignores non-claim fields and reacts to every claim field', () => {
|
|
const base = L.claimDigest(CLAIM);
|
|
// captured_at is not part of the claim.
|
|
expect(L.claimDigest({ ...CLAIM, captured_at: 'zzz' })).toBe(base);
|
|
// 1.5 and "1.5" are one claim, not two.
|
|
expect(L.claimDigest({ ...CLAIM, line: 0.5 })).toBe(L.claimDigest({ ...CLAIM, line: 0.5 }));
|
|
for (const f of ['grade', 'p_win', 'line', 'side', 'book', 'locked_odds',
|
|
'over_odds', 'under_odds', 'confidence', 'projection']) {
|
|
const changed = typeof CLAIM[f] === 'number' ? CLAIM[f] + 1 : `${CLAIM[f] || ''}X`;
|
|
expect(L.claimDigest({ ...CLAIM, [f]: changed })).not.toBe(base);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('CONCURRENCY', () => {
|
|
test('two writers that cannot see each other both target the SAME parent', () => {
|
|
// This is the race, stated exactly. Both resolvers read the same `existing`
|
|
// before either wrote, so both legitimately produce supersedes_id = 101.
|
|
// No in-memory check can catch this — neither writer can see the other.
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const stale = [row1];
|
|
|
|
const a = L.resolveLineage({ candidate: { ...CLAIM, grade: 'C', p_win: 0.55, captured_at: '2026-08-27T19:00:00Z' }, existing: stale });
|
|
const b = L.resolveLineage({ candidate: { ...CLAIM, grade: 'D', p_win: 0.49, captured_at: '2026-08-27T19:00:01Z' }, existing: stale });
|
|
|
|
expect(a.supersedes_id).toBe(101);
|
|
expect(b.supersedes_id).toBe(101);
|
|
expect(a.revision_ordinal).toBe(b.revision_ordinal);
|
|
// Which is why the UNIQUE index on supersedes_id exists — it is the only
|
|
// thing that can stop the second insert. Proven against a real database in
|
|
// the migration verification, not here.
|
|
const mig = fs.readFileSync(path.join(ROOT, 'supabase/migrations/045_read_lineage.sql'), 'utf8');
|
|
expect(mig).toMatch(/CREATE UNIQUE INDEX[\s\S]*?supersedes_id/);
|
|
});
|
|
|
|
test('a resolver that CAN see the sibling appends after it — that is not a fork', () => {
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const a = { ...CLAIM, grade: 'C', p_win: 0.55, captured_at: '2026-08-27T19:00:00Z' };
|
|
const ra = L.resolveLineage({ candidate: a, existing: [row1] });
|
|
const rowA = persisted(102, a, ra);
|
|
const rb = L.resolveLineage({ candidate: { ...CLAIM, grade: 'D', p_win: 0.49, captured_at: '2026-08-27T20:00:00Z' }, existing: [row1, rowA] });
|
|
expect(rb.ok).toBe(true);
|
|
expect(rb.supersedes_id).toBe(102);
|
|
expect(rb.revision_ordinal).toBe(2);
|
|
});
|
|
|
|
test('a chronology containing a fork is reported as broken', () => {
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const forkA = { ...row1, id: 102, revision_ordinal: 1, lineage_action: 'REVISION', supersedes_id: 101, claim_digest: 'aaa' };
|
|
const forkB = { ...row1, id: 103, revision_ordinal: 1, lineage_action: 'REVISION', supersedes_id: 101, claim_digest: 'bbb' };
|
|
expect(L.chronology([row1, forkA, forkB]).ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('EXACT TIMESTAMP', () => {
|
|
test('a row without a capture clock refuses rather than being stamped with now()', () => {
|
|
const r = L.resolveLineage({ candidate: { ...CLAIM, captured_at: null }, existing: [] });
|
|
expect(r.ok).toBe(false);
|
|
expect(r.refused).toBe('no_capture_clock');
|
|
});
|
|
|
|
test('the standing revision at an instant is answerable', () => {
|
|
const r1 = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, CLAIM, r1);
|
|
const c2 = { ...CLAIM, grade: 'C', p_win: 0.55, captured_at: '2026-08-27T19:00:00Z' };
|
|
const r2 = L.resolveLineage({ candidate: c2, existing: [row1] });
|
|
const row2 = persisted(102, c2, r2);
|
|
|
|
expect(L.revisionAt([row1, row2], '2026-08-27T15:00:00Z').grade || 'B').toBe('B');
|
|
expect(L.revisionAt([row1, row2], '2026-08-27T15:00:00Z').id).toBe(101);
|
|
expect(L.revisionAt([row1, row2], '2026-08-27T20:00:00Z').id).toBe(102);
|
|
expect(L.revisionAt([row1, row2], '2026-08-27T10:00:00Z')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('LEGACY UNKNOWN', () => {
|
|
test('rows with no chain position produce LEGACY_UNVERIFIED, never an invented order', () => {
|
|
const legacy = {
|
|
id: 55, read_id: null, read_natural_key: null,
|
|
game_id: CLAIM.game_id, claim_digest: null,
|
|
revision_ordinal: null, lineage_action: null, supersedes_id: null,
|
|
captured_at: '2026-08-01T14:00:00Z',
|
|
};
|
|
const r = L.resolveLineage({ candidate: CLAIM, existing: [legacy], mintReadId: mint('R9') });
|
|
expect(r.ok).toBe(true);
|
|
expect(r.lineage_state).toBe(L.LINEAGE_STATE.LEGACY_UNVERIFIED);
|
|
expect(r.action).toBe(L.LINEAGE_ACTION.ORIGIN);
|
|
expect(r.supersedes_id).toBeNull();
|
|
});
|
|
|
|
test('no backfill exists — nothing in the tree writes lineage onto historical rows', () => {
|
|
const files = ['src/services/read/readLineage.js', 'src/services/retentionService.js'];
|
|
for (const f of files) {
|
|
const src = fs.readFileSync(path.join(ROOT, f), 'utf8');
|
|
expect(src).not.toMatch(/backfill/i);
|
|
}
|
|
const mig = fs.readFileSync(path.join(ROOT, 'supabase/migrations/045_read_lineage.sql'), 'utf8');
|
|
expect(mig).not.toMatch(/UPDATE\s+\w+\s+SET/i);
|
|
expect(mig).not.toMatch(/\bDROP\b/i);
|
|
});
|
|
});
|
|
|
|
describe('DUAL-WRITE', () => {
|
|
const rows = () => ([
|
|
{ ...CLAIM },
|
|
{ ...CLAIM, side: 'under', grade: 'C', p_win: 0.39 },
|
|
]);
|
|
|
|
test('lineage keys are declared on EVERY row even when unresolved', async () => {
|
|
const r = rows();
|
|
await retention.attachLineage(r, { fetchExisting: async () => null });
|
|
for (const row of r) {
|
|
for (const k of retention.LINEAGE_KEYS) expect(k in row).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('a lineage failure never throws and leaves rows persistable', async () => {
|
|
const r = rows();
|
|
const out = await retention.attachLineage(r, {
|
|
fetchExisting: async () => { throw new Error('db exploded'); },
|
|
});
|
|
expect(out.error).toBe('db exploded');
|
|
for (const row of r) expect(row.read_id).toBeNull();
|
|
});
|
|
|
|
test('two rows of the SAME Read in one batch do not each mint an id', async () => {
|
|
// over and under are different Reads; two captures of one side are not.
|
|
const r = [{ ...CLAIM }, { ...CLAIM, grade: 'C', p_win: 0.55, captured_at: '2026-08-27T19:00:00Z' }];
|
|
let n = 0;
|
|
const out = await retention.attachLineage(r, {
|
|
fetchExisting: async () => [],
|
|
mintReadId: () => `R${++n}`,
|
|
});
|
|
expect(n).toBe(1);
|
|
expect(r[0].read_id).toBe('R1');
|
|
expect(r[1].read_id).toBe('R1');
|
|
expect(out.origins).toBe(1);
|
|
expect(out.revisions).toBe(1);
|
|
});
|
|
|
|
test('counts distinguish origins, revisions and recaptures', async () => {
|
|
const r = [{ ...CLAIM }, { ...CLAIM, captured_at: '2026-08-27T19:00:00Z' }];
|
|
const out = await retention.attachLineage(r, { fetchExisting: async () => [], mintReadId: mint('R1') });
|
|
expect(out.origins).toBe(1);
|
|
expect(out.recaptures).toBe(1);
|
|
expect(out.revisions).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('SHADOW AUTHORITY', () => {
|
|
test('no production read path imports the lineage module', () => {
|
|
const walk = (dir, out = []) => {
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
const full = path.join(dir, e.name);
|
|
if (e.isDirectory()) walk(full, out);
|
|
else if (/\.(js|jsx|ts|tsx)$/.test(e.name)) out.push(full);
|
|
}
|
|
return out;
|
|
};
|
|
const allowed = new Set([
|
|
path.join(ROOT, 'src/services/read/readLineage.js'),
|
|
path.join(ROOT, 'src/services/retentionService.js'), // the dual-writer
|
|
// ADDITIVE, NON-AUTHORITATIVE READERS (lineage productization).
|
|
// The property this test protects is "no path that serves PRODUCT truth
|
|
// reads lineage" — not "nothing reads lineage", which was only ever true
|
|
// because nothing had been built yet. These two read it to MEASURE it and
|
|
// to expose ancestry on a protected internal route; neither is consumed
|
|
// by any product surface, which the assertions below now police directly.
|
|
path.join(ROOT, 'src/services/lineageCoverage.js'),
|
|
path.join(ROOT, 'src/services/read/readAncestry.js'),
|
|
]);
|
|
const files = [
|
|
...walk(path.join(ROOT, 'src')),
|
|
...(fs.existsSync(path.join(ROOT, 'web/src')) ? walk(path.join(ROOT, 'web/src')) : []),
|
|
].filter((f) => !allowed.has(f));
|
|
const offenders = files.filter((f) => {
|
|
const src = fs.readFileSync(f, 'utf8')
|
|
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
return /(require\(|from\s+)['"`][^'"`]*readLineage['"`]/.test(src);
|
|
}).map((f) => path.relative(ROOT, f));
|
|
expect(offenders).toEqual([]);
|
|
|
|
// STRONGER THAN BEFORE AT THE LAYERS THAT MATTER.
|
|
// web/src must contain no reference at all, and among the routes exactly
|
|
// ONE — the protected internal router — may reach lineage. Previously no
|
|
// route could, so this keeps the boundary explicit rather than widening it.
|
|
const webDir = path.join(ROOT, 'web/src');
|
|
if (fs.existsSync(webDir)) {
|
|
const webHits = walk(webDir).filter((f) => /readLineage|readAncestry|lineageCoverage/
|
|
.test(fs.readFileSync(f, 'utf8'))).map((f) => path.relative(ROOT, f));
|
|
expect(webHits).toEqual([]);
|
|
}
|
|
// Exactly TWO routers may reach lineage: the protected internal router and
|
|
// the additive ancestry contract. The ledger and profile routers — which
|
|
// serve the grade-shift badge — must never appear here, which is what makes
|
|
// this list a boundary rather than a rubber stamp.
|
|
const routeHits = walk(path.join(ROOT, 'src/routes'))
|
|
.filter((f) => /readLineage|readAncestry|lineageCoverage/.test(fs.readFileSync(f, 'utf8')))
|
|
.map((f) => path.basename(f)).sort();
|
|
expect(routeHits).toEqual(['ancestry.js', 'internal.js']);
|
|
});
|
|
|
|
test('no route or web file selects a lineage column', () => {
|
|
const cols = ['read_natural_key', 'claim_digest', 'revision_ordinal', 'lineage_action', 'supersedes_id'];
|
|
const roots = [path.join(ROOT, 'src/routes')];
|
|
if (fs.existsSync(path.join(ROOT, 'web/src'))) roots.push(path.join(ROOT, 'web/src'));
|
|
const walk = (dir, out = []) => {
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (e.name.startsWith('.')) continue;
|
|
const full = path.join(dir, e.name);
|
|
if (e.isDirectory()) walk(full, out); else out.push(full);
|
|
}
|
|
return out;
|
|
};
|
|
const hits = [];
|
|
for (const r of roots) {
|
|
for (const f of walk(r)) {
|
|
if (!/\.(js|jsx|ts|tsx)$/.test(f)) continue;
|
|
const src = fs.readFileSync(f, 'utf8');
|
|
for (const c of cols) if (src.includes(c)) hits.push(`${path.relative(ROOT, f)} -> ${c}`);
|
|
}
|
|
}
|
|
expect(hits).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('MODEL PRESERVATION', () => {
|
|
test('lineage never touches a served field', () => {
|
|
const { SERVED_FIELDS } = require('../../src/services/evaluator/gradeFreeze');
|
|
for (const k of retention.LINEAGE_KEYS) expect(SERVED_FIELDS).not.toContain(k);
|
|
});
|
|
|
|
test('attaching lineage leaves every claim field byte-identical', async () => {
|
|
const row = { ...CLAIM };
|
|
const before = JSON.stringify(row);
|
|
await retention.attachLineage([row], { fetchExisting: async () => [], mintReadId: mint('R1') });
|
|
const after = { ...row };
|
|
for (const k of retention.LINEAGE_KEYS) delete after[k];
|
|
expect(JSON.stringify(after)).toBe(before);
|
|
});
|
|
});
|
|
|
|
describe('USER LOCK SEPARATION', () => {
|
|
test('there is no user Lock-Read concept to separate from — recorded, not assumed', () => {
|
|
// Traced: "locked" in this codebase means the SYSTEM's locked line/grade at
|
|
// snapshot time (gradedAt.line / locked_odds), never a user action. No
|
|
// lockRead / lock_read symbol exists anywhere in src or web/src.
|
|
const walk = (dir, out = []) => {
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
const full = path.join(dir, e.name);
|
|
if (e.isDirectory()) walk(full, out);
|
|
else if (/\.(js|jsx|ts|tsx)$/.test(e.name)) out.push(full);
|
|
}
|
|
return out;
|
|
};
|
|
const files = [
|
|
...walk(path.join(ROOT, 'src')),
|
|
...(fs.existsSync(path.join(ROOT, 'web/src')) ? walk(path.join(ROOT, 'web/src')) : []),
|
|
];
|
|
const hits = files.filter((f) => /lockRead|lock_read/i.test(fs.readFileSync(f, 'utf8')));
|
|
expect(hits).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('MEASURED SHAPE (locked from a real read-only replay)', () => {
|
|
test('the digest sees price in BOTH stores, which name it differently', () => {
|
|
// `ledger_entries` carries locked_odds; `model_snapshots` carries
|
|
// over_odds/under_odds. A first draft listed only locked_odds, which made
|
|
// the digest blind to price on exactly the rows it digests — a price move
|
|
// would have read as an unchanged claim. Caught by the replay failing on a
|
|
// column that does not exist.
|
|
const ms = { line: 0.5, side: 'over', book: 'dk', over_odds: 120, under_odds: -145, grade: 'B' };
|
|
expect(L.claimDigest(ms)).not.toBe(L.claimDigest({ ...ms, over_odds: 180 }));
|
|
const led = { line: 0.5, side: 'over', book: 'dk', locked_odds: 120, grade: 'B' };
|
|
expect(L.claimDigest(led)).not.toBe(L.claimDigest({ ...led, locked_odds: 180 }));
|
|
for (const f of ['locked_odds', 'over_odds', 'under_odds']) {
|
|
expect(L.CLAIM_MARKET_FIELDS).toContain(f);
|
|
}
|
|
});
|
|
|
|
test('ONE natural key can legitimately hold TWO Reads', () => {
|
|
// Measured: 18 of 30,746 identity groups carry genuinely different team
|
|
// pairs. Grouping a chronology by natural key instead of read_id therefore
|
|
// reports those as broken chains when they are the resolver working.
|
|
const a = L.resolveLineage({ candidate: CLAIM, existing: [], mintReadId: mint('RA') });
|
|
const rowA = persisted(101, CLAIM, a);
|
|
const other = { ...CLAIM, game_id: 'mlb:2026-08-27:MinnesotaTwins@SanDiegoPadres' };
|
|
const b = L.resolveLineage({ candidate: other, existing: [rowA], mintReadId: mint('RB') });
|
|
const rowB = persisted(102, other, b);
|
|
|
|
expect(L.readNaturalKey(CLAIM)).toBe(L.readNaturalKey(other));
|
|
expect(a.read_id).not.toBe(b.read_id);
|
|
// Mixed bucket is rejected...
|
|
expect(L.chronology([rowA, rowB]).ok).toBe(false);
|
|
// ...and each Read on its own is a valid chain.
|
|
expect(L.chronology([rowA]).ok).toBe(true);
|
|
expect(L.chronology([rowB]).ok).toBe(true);
|
|
});
|
|
|
|
test('a claim that changes only PRICE is still a new published claim', () => {
|
|
// 4,206 of 4,507 replayed revisions moved price without moving the grade.
|
|
// The user was shown a different number, so it is a different claim.
|
|
const r1 = L.resolveLineage({ candidate: { ...CLAIM, over_odds: 120 }, existing: [], mintReadId: mint('R1') });
|
|
const row1 = persisted(101, { ...CLAIM, over_odds: 120 }, r1);
|
|
const repriced = { ...CLAIM, over_odds: 180, captured_at: '2026-08-27T19:00:00Z' };
|
|
const r2 = L.resolveLineage({ candidate: repriced, existing: [row1] });
|
|
expect(r2.action).toBe(L.LINEAGE_ACTION.REVISION);
|
|
expect(r2.revision_ordinal).toBe(1);
|
|
});
|
|
});
|