Files
vyndr/tests/unit/readLineage.test.js
T
builtbykev 930d526b01 Lineage productization: a live graph, an observer that doesn't trust it, and a surface that owns only what it knows
The authority review found there was nothing to switch: lineage is a
write-only graph with no living permanent writer and no product consumer.
Authority theater would have been a switch on a consumer that does not exist,
reading a store that is not being written. So: make the graph live, measure it
from outside, and expose the one category it alone owns.

WRITE-PATH FAILURE SEMANTICS, TRACED FIRST. persist(:857) -> the authoritative
cacheSet snapshot:latest(:1404) -> the lineage gate(:1426) -> ledger(:1473).
Lineage failure cannot fail base retention (earlier, separate upsert), cannot
fail publication (the product write precedes the gate), cannot fail the Ledger
(lineageIndex defaults null; the ledger has its own guard), and cannot create a
new partial row (blankLineage() first, atomicity sweep after the catch). The
isolation this tranche needed already existed; only a mode was missing.

THREE MODES, ONE EVALUATOR. `lineageWriteMode` distinguishes OFF /
CANARY_LEASED / PERSISTENT_SHADOW, and the write gate and the status surface
both read it, so they cannot disagree. The canary is CONSULTED, never
converted: its <=4h absolute expiry, its dynamic evaluation and its
fail-closed parse are untouched.

AN AMBIGUOUS CONFIGURATION FAILS CLOSED. If a sport is named by both persistent
mode and an active lease, the two instructions disagree about WHEN WRITING
STOPS — the lease says 22:45, persistent says never. The dangerous reading is
the quiet one: an operator sets a bounded lease believing writing will stop
while persistent keeps it going. We cannot know which they meant, so that sport
writes nothing until the configuration says one thing. The sport allowlist is
the canary's own, so persistent mode can never widen past it.

THE OBSERVER MAY NOT ASK THE WRITER HOW IT DID. settleLedger returned
{settled:0,pending:0} — byte-identical to a healthy "nothing to settle" — while
1,444 rows sat unprocessed, and the watchdog believed it. So `lineageCoverage`
reads durable retained state only, and THE DENOMINATOR MAY NOT CONSULT
lineage_action: eligibility is "the row was published AND a natural key is
derivable from its own identity columns", neither of which the lineage path
writes. If expectation were derived from whether lineage exists, coverage would
be 100% by construction and the metric would be decoration. Zero-expected and
zero-written are kept as different answers.

THE LEGACY BOUNDARY IS OBSERVED, NOT DECLARED. `publication_id` is stamped only
by commitPublication, so the row itself says whether lineage ran. Verified on
production: 5,353 rows carry it — 4,234 complete actions plus exactly the 1,119
historical partial rows — and zero actions exist without one. No epoch constant
is invented; a date would have been a guess about when the writer was on.
publication_id NULL -> LEGACY_UNVERIFIED. Stamped but incomplete ->
LINEAGE_UNAVAILABLE, which is the honest answer for the 1,119 and is never
quietly rewritten as legacy.

THE GRADE-SHIFT BADGE IS UNTOUCHED. revised_from_grade answers "did the letter
change"; lineage answers "which published claim superseded which". Different
questions, and a test now fails if either route learns the word lineage.

Suite 397/5,496/0 · tsc 0 · 15/15 teeth.

TWO OF MY OWN TESTS WERE VACUOUS AND A TOOTH FOUND IT. Tooth 2 came back green
because the isolation tests asserted the slate was published — true whether or
not the exception propagated — while never reaching the lineage gate at all:
the fake grader never fired `onGraded`, so the collector stayed empty and
persistedRows stayed null. Fixed by firing the hook and counting the commit.
A green teeth run means the test is missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
2026-08-30 22:10:14 -04:00

516 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([]);
}
const routeHits = walk(path.join(ROOT, 'src/routes'))
.filter((f) => /readLineage|readAncestry|lineageCoverage/.test(fs.readFileSync(f, 'utf8')))
.map((f) => path.basename(f));
expect(routeHits).toEqual(['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);
});
});