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
This commit is contained in:
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* LINEAGE PRODUCTIZATION — persistent shadow writer, independent coverage
|
||||
* observer, additive ancestry surface.
|
||||
*
|
||||
* Three properties this suite exists to hold:
|
||||
* the product never depends on lineage;
|
||||
* the observer never trusts the writer;
|
||||
* the ancestry surface never invents history it does not have.
|
||||
*/
|
||||
|
||||
const svc = require('../../src/services/snapshotService');
|
||||
const mode = require('../../src/services/lineageWriteMode');
|
||||
const coverage = require('../../src/services/lineageCoverage');
|
||||
const ancestry = require('../../src/services/read/readAncestry');
|
||||
const retention = require('../../src/services/retentionService');
|
||||
|
||||
const ENV = mode.ENV_VAR;
|
||||
const setPersistent = (v) => { if (v === null) delete process.env[ENV]; else process.env[ENV] = v; };
|
||||
afterEach(() => setPersistent(null));
|
||||
|
||||
/* ─────────────────────────── WRITE MODE ─────────────────────────── */
|
||||
describe('write mode — three modes, one evaluator', () => {
|
||||
test('unset -> OFF, nothing enabled', () => {
|
||||
setPersistent(null);
|
||||
expect(mode.evaluate().mode).toBe(mode.WRITE_MODE.OFF);
|
||||
expect(mode.isEnabled('mlb')).toBe(false);
|
||||
});
|
||||
|
||||
test('PERSISTENT_SHADOW writes without any lease', () => {
|
||||
setPersistent('mlb');
|
||||
const e = mode.evaluate();
|
||||
expect(e.mode).toBe(mode.WRITE_MODE.PERSISTENT_SHADOW);
|
||||
expect(mode.isEnabled('mlb')).toBe(true);
|
||||
// No lease is involved, and none is required.
|
||||
expect(e.canary_sports).toEqual([]);
|
||||
expect(mode.modeFor('mlb')).toBe(mode.WRITE_MODE.PERSISTENT_SHADOW);
|
||||
});
|
||||
|
||||
test('a sport outside the allowlist fails closed, and takes the whole list with it', () => {
|
||||
setPersistent('wnba');
|
||||
expect(mode.evaluate().persistent_invalid_reason).toBe('INVALID_SPORT_NOT_PERSISTABLE');
|
||||
expect(mode.isEnabled('wnba')).toBe(false);
|
||||
setPersistent('mlb,wnba');
|
||||
// Keeping the valid half would enable a subset nobody asked for on its own.
|
||||
expect(mode.isEnabled('mlb')).toBe(false);
|
||||
});
|
||||
|
||||
test('the allowlist is the canary\'s, not a second one', () => {
|
||||
const canary = require('../../src/services/lineageCanaryConfig');
|
||||
expect(mode.PERSISTABLE_SPORTS).toBe(canary.LEASABLE_SPORTS);
|
||||
});
|
||||
|
||||
test('another sport cannot become enabled by accident', () => {
|
||||
setPersistent('mlb');
|
||||
for (const s of ['wnba', 'nba', 'soccer', '']) expect(mode.isEnabled(s)).toBe(false);
|
||||
});
|
||||
|
||||
test('status and gate come from the SAME evaluator', () => {
|
||||
setPersistent('mlb');
|
||||
const st = mode.state();
|
||||
expect(st.mode).toBe(mode.evaluate().mode);
|
||||
expect(st.effective_sports).toEqual(mode.evaluate().sports);
|
||||
expect(st.authority).toBe('NON_AUTHORITATIVE');
|
||||
});
|
||||
|
||||
test('rollback ON -> OFF disables future writes and touches nothing else', () => {
|
||||
setPersistent('mlb');
|
||||
expect(mode.isEnabled('mlb')).toBe(true);
|
||||
setPersistent(null);
|
||||
expect(mode.isEnabled('mlb')).toBe(false);
|
||||
expect(mode.evaluate().mode).toBe(mode.WRITE_MODE.OFF);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canary and persistent are DIFFERENT modes', () => {
|
||||
test('an ambiguous configuration fails closed rather than double-enabling', () => {
|
||||
// The two instructions disagree about WHEN WRITING STOPS. We cannot know
|
||||
// which the operator meant, so the sport writes nothing.
|
||||
const e = mode.evaluate.call(null);
|
||||
const fake = {
|
||||
lease_state: 'ACTIVE', effective_active_sports: ['mlb'],
|
||||
};
|
||||
jest.isolateModules(() => {
|
||||
jest.doMock('../../src/services/lineageCanaryConfig', () => ({
|
||||
LEASABLE_SPORTS: ['mlb'],
|
||||
evaluate: () => fake,
|
||||
}));
|
||||
const m2 = require('../../src/services/lineageWriteMode');
|
||||
process.env[m2.ENV_VAR] = 'mlb';
|
||||
const ev = m2.evaluate();
|
||||
expect(ev.conflict).toBe(m2.CONFLICT.SPORT_IN_BOTH_MODES);
|
||||
expect(ev.conflict_sports).toEqual(['mlb']);
|
||||
expect(ev.mode).toBe(m2.WRITE_MODE.OFF);
|
||||
expect(m2.isEnabled('mlb')).toBe(false);
|
||||
delete process.env[m2.ENV_VAR];
|
||||
});
|
||||
expect(e).toBeDefined();
|
||||
});
|
||||
|
||||
test('a lease alone still enables, with the canary contract untouched', () => {
|
||||
jest.isolateModules(() => {
|
||||
jest.doMock('../../src/services/lineageCanaryConfig', () => ({
|
||||
LEASABLE_SPORTS: ['mlb'],
|
||||
evaluate: () => ({ lease_state: 'ACTIVE', effective_active_sports: ['mlb'] }),
|
||||
}));
|
||||
const m2 = require('../../src/services/lineageWriteMode');
|
||||
delete process.env[m2.ENV_VAR];
|
||||
expect(m2.evaluate().mode).toBe(m2.WRITE_MODE.CANARY_LEASED);
|
||||
expect(m2.isEnabled('mlb')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('an EXPIRED lease enables nothing — dynamic expiry is preserved', () => {
|
||||
jest.isolateModules(() => {
|
||||
jest.doMock('../../src/services/lineageCanaryConfig', () => ({
|
||||
LEASABLE_SPORTS: ['mlb'],
|
||||
evaluate: () => ({ lease_state: 'EXPIRED', effective_active_sports: [] }),
|
||||
}));
|
||||
const m2 = require('../../src/services/lineageWriteMode');
|
||||
delete process.env[m2.ENV_VAR];
|
||||
expect(m2.isEnabled('mlb')).toBe(false);
|
||||
expect(m2.evaluate().mode).toBe(m2.WRITE_MODE.OFF);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* ────────────────── FAILURE ISOLATION (the product first) ────────────────── */
|
||||
describe('lineage failure can never reach authoritative product truth', () => {
|
||||
// HERMETIC. Injecting `retention` activates pipeline paths the other snapshot
|
||||
// tests never reach, and several resolve a real Supabase client when the
|
||||
// environment offers one. Blanking the credentials makes every database
|
||||
// dependency a no-op, so this measures the ISOLATION PROPERTY and not the
|
||||
// network. Restored afterwards so no other suite inherits it.
|
||||
const SUPA_KEYS = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_SERVICE_KEY', 'SUPABASE_ANON_KEY'];
|
||||
const saved = {};
|
||||
beforeAll(() => { for (const k of SUPA_KEYS) { saved[k] = process.env[k]; delete process.env[k]; } });
|
||||
afterAll(() => { for (const k of SUPA_KEYS) { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; } });
|
||||
|
||||
const memCache = () => { const store = {}; return { store, cacheGet: async (k) => (k in store ? store[k] : null), cacheSet: async (k, v) => { store[k] = v; } }; };
|
||||
const sampleProps = [{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, over_odds: -115, under_odds: -105, book: 'draftkings' }];
|
||||
const grades = [{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'A+', confidence: 80 }];
|
||||
const deps = (cache, extra = {}) => ({
|
||||
getOdds: async () => ({ sport: 'mlb', props: sampleProps, provider: 'propline' }),
|
||||
// Fires the retention hook the real grader fires. Without it the collector
|
||||
// stays empty, `persistedRows` stays null and the lineage gate is never
|
||||
// reached — which would make every assertion below vacuously true.
|
||||
gradeAndCacheSlate: async (_s, _p, opts) => {
|
||||
if (typeof opts.onGraded === 'function') {
|
||||
opts.onGraded(
|
||||
{
|
||||
player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, book: 'draftkings',
|
||||
game_time: '2026-08-31T23:05:00Z', game_date: '2026-08-31',
|
||||
canonical_event_id: 'mlb:gamepk:999001',
|
||||
},
|
||||
[
|
||||
{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'over', grade: 'A+', confidence: 80 },
|
||||
{ player: 'Aaron Judge', stat_type: 'total_bases', line: 1.5, direction: 'under', grade: 'D', confidence: 40 },
|
||||
],
|
||||
);
|
||||
}
|
||||
await opts.cacheSet('grades:x', { grades });
|
||||
return { written: true, count: 1 };
|
||||
},
|
||||
resolveStats: async () => ({ found: false }),
|
||||
classify: require('../../src/services/archetypeService').classify,
|
||||
cacheGet: cache.cacheGet, cacheSet: cache.cacheSet,
|
||||
now: () => '2026-08-31T01:00:00Z', nowMs: () => 1000,
|
||||
...extra,
|
||||
});
|
||||
|
||||
test('a throwing lineage commit leaves the slate published and the run ok', async () => {
|
||||
setPersistent('mlb');
|
||||
const cache = memCache();
|
||||
// The counter is the point. Without it this test passes when the lineage
|
||||
// path is never entered at all, which asserts nothing about isolation.
|
||||
let commits = 0;
|
||||
const r = await svc.runSnapshot('mlb', deps(cache, {
|
||||
retention: {
|
||||
persist: async () => ({ attempted: 1, written: 1 }),
|
||||
commitPublication: async () => { commits += 1; throw new Error('lineage exploded'); },
|
||||
rowsFromSides: retention.rowsFromSides,
|
||||
createCollector: retention.createCollector,
|
||||
newSnapshotId: retention.newSnapshotId,
|
||||
},
|
||||
}));
|
||||
expect(commits).toBe(1); // the failure really happened
|
||||
expect(r.status).toBe('ok'); // and the run still completed
|
||||
// The authoritative product write happened regardless...
|
||||
expect(cache.store['snapshot:mlb:latest']).toBeTruthy();
|
||||
// ...and so did every step AFTER the lineage block. This is what separates
|
||||
// "the slate was already written" from "the exception was contained".
|
||||
expect(cache.store['grades:mlb']).toBeTruthy();
|
||||
});
|
||||
|
||||
test('a throwing lineage commit does not stop the Ledger write', async () => {
|
||||
setPersistent('mlb');
|
||||
const cache = memCache();
|
||||
const ledgerCalls = [];
|
||||
let commits = 0;
|
||||
await svc.runSnapshot('mlb', deps(cache, {
|
||||
retention: {
|
||||
persist: async () => ({ attempted: 1, written: 1 }),
|
||||
commitPublication: async () => { commits += 1; throw new Error('lineage exploded'); },
|
||||
rowsFromSides: retention.rowsFromSides,
|
||||
createCollector: retention.createCollector,
|
||||
newSnapshotId: retention.newSnapshotId,
|
||||
},
|
||||
ledger: {
|
||||
recordPipelineGrades: async (...a) => { ledgerCalls.push(a); return { written: 1 }; },
|
||||
captureClosing: async () => ({}),
|
||||
},
|
||||
}));
|
||||
expect(commits).toBe(1);
|
||||
expect(ledgerCalls.length).toBe(1);
|
||||
// read_id linkage is absent, never guessed.
|
||||
expect(ledgerCalls[0][3].lineageIndex).toBeNull();
|
||||
});
|
||||
|
||||
test('with the mode OFF no lineage commit is attempted at all', async () => {
|
||||
setPersistent(null);
|
||||
const cache = memCache();
|
||||
let commits = 0;
|
||||
await svc.runSnapshot('mlb', deps(cache, {
|
||||
retention: {
|
||||
persist: async () => ({ attempted: 1, written: 1 }),
|
||||
commitPublication: async () => { commits += 1; return { published: 0, candidates: 0 }; },
|
||||
rowsFromSides: retention.rowsFromSides,
|
||||
createCollector: retention.createCollector,
|
||||
newSnapshotId: retention.newSnapshotId,
|
||||
},
|
||||
}));
|
||||
expect(commits).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
/* ─────────────── PARTIAL-LINEAGE ATOMICITY (no half actions) ─────────────── */
|
||||
describe('a failed lineage attempt writes no half action', () => {
|
||||
test('a throwing family lookup leaves every row completely blank', async () => {
|
||||
const rows = [
|
||||
{ sport: 'mlb', game_date: '2026-08-31', player_key: 'a b', stat: 'hits', side: 'over', line: 0.5, published: true },
|
||||
{ sport: 'mlb', game_date: '2026-08-31', player_key: 'c d', stat: 'hits', side: 'over', line: 0.5, published: true },
|
||||
];
|
||||
const out = await retention.attachLineage(rows, { fetchExisting: async () => { throw new Error('boom'); } });
|
||||
expect(out.error).toMatch(/boom/);
|
||||
for (const r of rows) {
|
||||
expect(retention.isValidLineageAction(r)).toBe(false);
|
||||
for (const k of retention.LINEAGE_KEYS) expect(r[k]).toBeNull();
|
||||
}
|
||||
// The natural key stamped before the lookup is cleared too — it is family
|
||||
// identity, and a failed attempt has no claim to it.
|
||||
expect(rows[0].read_natural_key).toBeNull();
|
||||
expect(out.incomplete_cleared).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
/* ───────────── INDEPENDENT COVERAGE OBSERVER (never trust the writer) ───────────── */
|
||||
describe('coverage observer', () => {
|
||||
const row = (o) => ({
|
||||
sport: 'mlb', game_date: '2026-08-31', player_key: 'aaron judge', stat: 'hits',
|
||||
side: 'over', line: 0.5, published: true, ...o,
|
||||
});
|
||||
const action = (o) => ({
|
||||
read_id: 'r1', read_natural_key: 'k', 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', ...o,
|
||||
});
|
||||
|
||||
test('THE DENOMINATOR IGNORES lineage_action — the property, not the spelling', () => {
|
||||
const base = [row({ player_key: 'a' }), row({ player_key: 'b' })];
|
||||
const withLineage = base.map((r) => ({ ...r, ...action({ read_natural_key: r.player_key }) }));
|
||||
const without = base.map((r) => ({ ...r }));
|
||||
// Same rows, opposite lineage state, identical expectation.
|
||||
expect(coverage.expectedKeys(withLineage).keys.size).toBe(coverage.expectedKeys(without).keys.size);
|
||||
expect([...coverage.expectedKeys(withLineage).keys].sort())
|
||||
.toEqual([...coverage.expectedKeys(without).keys].sort());
|
||||
});
|
||||
|
||||
test('unpublished rows are not expected to have lineage', () => {
|
||||
expect(coverage.expectedKeys([row({ published: false })]).keys.size).toBe(0);
|
||||
expect(coverage.expectedKeys([row({ published: null })]).keys.size).toBe(0);
|
||||
});
|
||||
|
||||
test('a row with no derivable identity is counted, not silently dropped', () => {
|
||||
const e = coverage.expectedKeys([row({ player_key: '' })]);
|
||||
expect(e.keys.size).toBe(0);
|
||||
expect(e.unkeyable_rows).toBe(1);
|
||||
});
|
||||
|
||||
test('WRITER NEVER RAN: full slate, zero lineage -> MISSING_COVERAGE', () => {
|
||||
const a = coverage.auditRows([row({ player_key: 'a' }), row({ player_key: 'b' })]);
|
||||
expect(a.expected_keys).toBe(2);
|
||||
expect(a.covered_keys).toBe(0);
|
||||
expect(a.health).toBe(coverage.HEALTH.MISSING_COVERAGE);
|
||||
});
|
||||
|
||||
test('zero expected and zero written are DIFFERENT answers', () => {
|
||||
const none = coverage.auditRows([row({ published: false })]);
|
||||
expect(none.health).toBe(coverage.HEALTH.NO_ELIGIBLE_CLAIMS);
|
||||
expect(none.health).not.toBe(coverage.HEALTH.MISSING_COVERAGE);
|
||||
});
|
||||
|
||||
test('partial coverage is detected', () => {
|
||||
const rows = [
|
||||
{ ...row({ player_key: 'a' }), ...action({ read_natural_key: 'mlb|2026-08-31|a|hits|over|0.500000' }) },
|
||||
row({ player_key: 'b' }),
|
||||
];
|
||||
const a = coverage.auditRows(rows);
|
||||
expect(a.health).toBe(coverage.HEALTH.PARTIAL_COVERAGE);
|
||||
expect(a.missing_keys).toBe(1);
|
||||
expect(a.coverage_pct).toBe(50);
|
||||
});
|
||||
|
||||
test('full coverage is HEALTHY', () => {
|
||||
const rows = ['a', 'b'].map((p) => ({
|
||||
...row({ player_key: p }),
|
||||
...action({ read_natural_key: `mlb|2026-08-31|${p}|hits|over|0.500000`, read_id: `r-${p}` }),
|
||||
}));
|
||||
const a = coverage.auditRows(rows);
|
||||
expect(a.health).toBe(coverage.HEALTH.HEALTHY);
|
||||
expect(a.coverage_pct).toBe(100);
|
||||
expect(a.missing_keys).toBe(0);
|
||||
expect(a.extra_keys).toBe(0);
|
||||
});
|
||||
|
||||
test('an invalid partial action is INVALID_GRAPH, never counted as covered', () => {
|
||||
const partial = { ...row({ player_key: 'a' }), read_natural_key: 'k', read_id: 'r' }; // 2 of 9
|
||||
const a = coverage.auditRows([partial]);
|
||||
expect(a.invalid_partial_actions).toBe(1);
|
||||
expect(a.covered_keys).toBe(0);
|
||||
expect(a.health).toBe(coverage.HEALTH.INVALID_GRAPH);
|
||||
});
|
||||
|
||||
test('extra lineage — covered but never expected — is a defect', () => {
|
||||
const rows = [{ ...row({ published: false, player_key: 'a' }), ...action({ read_natural_key: 'ghost' }) }];
|
||||
const a = coverage.auditRows(rows);
|
||||
expect(a.expected_keys).toBe(0);
|
||||
expect(a.extra_keys).toBe(1);
|
||||
expect(a.health).toBe(coverage.HEALTH.INVALID_GRAPH);
|
||||
});
|
||||
|
||||
test('a forked chain is a graph defect', () => {
|
||||
const k = 'mlb|2026-08-31|a|hits|over|0.500000';
|
||||
const rows = [
|
||||
{ ...row({ player_key: 'a' }), ...action({ read_natural_key: k, revision_ordinal: 0 }) },
|
||||
{ ...row({ player_key: 'a' }), ...action({ read_natural_key: k, revision_ordinal: 0 }) },
|
||||
];
|
||||
const a = coverage.auditRows(rows);
|
||||
expect(a.graph_defects).toBeGreaterThan(0);
|
||||
expect(a.health).toBe(coverage.HEALTH.INVALID_GRAPH);
|
||||
});
|
||||
|
||||
test('the observer\'s valid-action fields match the writer\'s, without importing it', () => {
|
||||
expect([...coverage.VALID_ACTION_FIELDS].sort())
|
||||
.toEqual([...retention.VALID_LINEAGE_ACTION_FIELDS].sort());
|
||||
});
|
||||
|
||||
test('an unavailable audit is not a coverage claim', () => {
|
||||
expect(coverage.classify({ audit_available: false })).toBe(coverage.HEALTH.AUDIT_UNAVAILABLE);
|
||||
});
|
||||
});
|
||||
|
||||
/* ───────────────────────── ADDITIVE ANCESTRY ───────────────────────── */
|
||||
describe('ancestry surface tells the truth about what it has', () => {
|
||||
const A = ancestry.ANCESTRY_STATE;
|
||||
const act = (o) => ({
|
||||
id: 1, read_id: 'r1', read_natural_key: 'k', lineage_action: 'ORIGIN', claim_digest: 'd0',
|
||||
revision_ordinal: 0, lineage_state: 'PUBLISHED', lineage_version: 'lin@1',
|
||||
claim_schema_version: 'claim@1', digest_algorithm_version: 'sha256-json-sorted@1',
|
||||
publication_id: 'p1', ...o,
|
||||
});
|
||||
|
||||
test('a row lineage never ran for is LEGACY_UNVERIFIED, and nothing is synthesized', () => {
|
||||
const out = ancestry.classifyAncestry([{ id: 1, publication_id: null, grade: 'B' }]);
|
||||
expect(out.state).toBe(A.LEGACY_UNVERIFIED);
|
||||
expect(out.revisions).toEqual([]);
|
||||
expect(out.read_id).toBeUndefined();
|
||||
});
|
||||
|
||||
test('a row lineage RAN for but did not complete is LINEAGE_UNAVAILABLE, not legacy', () => {
|
||||
// This is the shape of the 1,119 historical partial rows.
|
||||
const out = ancestry.classifyAncestry([{ id: 1, publication_id: 'p1', read_natural_key: 'k' }]);
|
||||
expect(out.state).toBe(A.LINEAGE_UNAVAILABLE);
|
||||
expect(out.state).not.toBe(A.LEGACY_UNVERIFIED);
|
||||
});
|
||||
|
||||
test('a partial row is never elected as an ancestry head', () => {
|
||||
const out = ancestry.classifyAncestry([
|
||||
{ id: 1, publication_id: 'p1', read_natural_key: 'k', read_id: 'r1' },
|
||||
]);
|
||||
expect(out.state).toBe(A.LINEAGE_UNAVAILABLE);
|
||||
expect(out.current).toBeUndefined();
|
||||
});
|
||||
|
||||
test('ORIGIN alone reads as a one-entry chronology', () => {
|
||||
const out = ancestry.classifyAncestry([act({})]);
|
||||
expect(out.state).toBe(A.LINEAGE_AVAILABLE);
|
||||
expect(out.revision_count).toBe(0);
|
||||
expect(out.original.action).toBe('ORIGIN');
|
||||
expect(out.current.action).toBe('ORIGIN');
|
||||
expect(out.authority).toBe('NON_AUTHORITATIVE');
|
||||
});
|
||||
|
||||
test('REVISION ancestry names the exact parent and order', () => {
|
||||
const out = ancestry.classifyAncestry([
|
||||
act({ id: 1, revision_ordinal: 0, lineage_action: 'ORIGIN' }),
|
||||
act({ id: 2, revision_ordinal: 1, lineage_action: 'REVISION', supersedes_id: 1, claim_digest: 'd1', change_type: 'MIXED_CHANGE' }),
|
||||
]);
|
||||
expect(out.state).toBe(A.LINEAGE_AVAILABLE);
|
||||
expect(out.revision_count).toBe(1);
|
||||
expect(out.current.revision_ordinal).toBe(1);
|
||||
expect(out.current.supersedes_row_id).toBe(1);
|
||||
expect(out.current.change_type).toBe('MIXED_CHANGE');
|
||||
});
|
||||
|
||||
test('RECAPTURE is reported as a recapture, never as a revision', () => {
|
||||
const out = ancestry.classifyAncestry([
|
||||
act({ id: 1, lineage_action: 'ORIGIN', revision_ordinal: 0 }),
|
||||
act({ id: 2, lineage_action: 'RECAPTURE', revision_ordinal: 0, recaptures_id: 1 }),
|
||||
]);
|
||||
expect(out.state).toBe(A.LINEAGE_AVAILABLE);
|
||||
expect(out.revision_count).toBe(0);
|
||||
expect(out.recapture_count).toBe(1);
|
||||
expect(out.recaptures[0].action).toBe('RECAPTURE');
|
||||
});
|
||||
|
||||
test('a broken chain REFUSES rather than electing a head', () => {
|
||||
const out = ancestry.classifyAncestry([
|
||||
act({ id: 1, lineage_action: 'ORIGIN', revision_ordinal: 0 }),
|
||||
act({ id: 2, lineage_action: 'REVISION', revision_ordinal: 1, supersedes_id: 999 }),
|
||||
]);
|
||||
expect(out.state).toBe(A.INVALID_LINEAGE);
|
||||
expect(out.revisions).toEqual([]);
|
||||
});
|
||||
|
||||
test('two read_ids under one natural key is INVALID_LINEAGE', () => {
|
||||
const out = ancestry.classifyAncestry([act({ id: 1, read_id: 'r1' }), act({ id: 2, read_id: 'r2' })]);
|
||||
expect(out.state).toBe(A.INVALID_LINEAGE);
|
||||
});
|
||||
|
||||
test('the projection carries only what lineage knows', () => {
|
||||
const p = ancestry.project({ ...act({}), p_win: 0.6, grade: 'A', locked_odds: '-110', outcome: 'hit' });
|
||||
for (const forbidden of ['p_win', 'grade', 'locked_odds', 'outcome', 'projection']) {
|
||||
expect(p[forbidden]).toBeUndefined();
|
||||
}
|
||||
expect(p.read_id).toBe('r1');
|
||||
expect(p.claim_digest).toBe('d0');
|
||||
});
|
||||
|
||||
test('nothing found is NOT_FOUND, which is not an absence of lineage', () => {
|
||||
expect(ancestry.classifyAncestry([]).state).toBe(A.NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
/* ────────────────────── PRESERVATION ────────────────────── */
|
||||
describe('this tranche changes nothing it was told not to', () => {
|
||||
const fs = require('fs'); const path = require('path');
|
||||
const ROOT = path.resolve(__dirname, '..', '..');
|
||||
|
||||
test('the grade-shift badge is untouched — no lineage anywhere near it', () => {
|
||||
for (const f of ['src/routes/ledger.js', 'src/routes/profiles.js']) {
|
||||
const src = fs.readFileSync(path.join(ROOT, f), 'utf8');
|
||||
expect(src).toMatch(/revised_from_grade/);
|
||||
expect(src).not.toMatch(/lineage|read_id/);
|
||||
}
|
||||
});
|
||||
|
||||
test('retention identity and its collision counter are unchanged', () => {
|
||||
expect(retention.RETENTION_CONFLICT.split(',')).toEqual(
|
||||
['snapshot_id', 'game_id', 'canonical_event_id', 'player_key', 'stat', 'line', 'side']);
|
||||
const dup = [
|
||||
{ snapshot_id: 's', game_id: 'g', canonical_event_id: 'e', player_key: 'a', stat: 'hits', line: 0.5, side: 'over' },
|
||||
{ snapshot_id: 's', game_id: 'g', canonical_event_id: 'e', player_key: 'a', stat: 'hits', line: 0.5, side: 'over' },
|
||||
];
|
||||
expect(retention.expectedMaterialization(dup).collision_count).toBe(1);
|
||||
});
|
||||
|
||||
test('participant identity guards are unchanged', () => {
|
||||
const pid = require('../../src/services/model/participantIdentity');
|
||||
const a = pid.auditParticipantIdentity([
|
||||
{ canonical_event_id: 'e', mlb_person_id: 1, player_key: 'x' },
|
||||
{ canonical_event_id: 'e', mlb_person_id: 1, player_key: 'y' },
|
||||
]);
|
||||
expect(a.participant_identity_split_count).toBe(1);
|
||||
expect(pid.semanticPlayerKey({ player: 'Mickey Gasper', mlb_person_id: 681508, canonical_player_name: 'Mickey Gasper' }).canonical).toBe(true);
|
||||
});
|
||||
|
||||
test('the ancestry surface writes nothing', () => {
|
||||
const src = fs.readFileSync(path.join(ROOT, 'src/services/read/readAncestry.js'), 'utf8')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
for (const verb of ['.insert(', '.update(', '.upsert(', '.delete(']) {
|
||||
expect(src).not.toContain(verb);
|
||||
}
|
||||
});
|
||||
|
||||
test('the coverage observer writes nothing', () => {
|
||||
const src = fs.readFileSync(path.join(ROOT, 'src/services/lineageCoverage.js'), 'utf8')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
for (const verb of ['.insert(', '.update(', '.upsert(', '.delete(']) {
|
||||
expect(src).not.toContain(verb);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user