A canary is temporary: bound activation with a fail-closed lease

`LINEAGE_CANARY_SPORTS=mlb` was parsed once at module load and frozen, so an
enabled canary stayed writable for the whole process lifetime. On 2026-08-29 it
was left set and the 22:00Z, 01:00Z and 03:00Z scheduled snapshots each wrote
lineage overnight with nobody watching. That history happened to be correct.
Lineage is append-only evidence, so a defective canary would have written
irreversible WRONG evidence exactly as quietly. Correct history was luck, not a
safety property.

NEW CONTRACT:  LINEAGE_CANARY_SPORTS=mlb@2026-08-30T18:00:00Z

Absolute UTC instant only -- no duration, no local timezone. A relative "4h"
would silently restart on every redeploy, which is the defect being removed.

LEGACY `mlb` NO LONGER ACTIVATES ANYTHING. It is INVALID_MISSING_EXPIRY. Leaving
it working would have left the defect in place behind a nicer-looking
alternative, so this is the load-bearing half of the repair.

EXPIRY IS EVALUATED AT THE WRITE GATE, not at startup. `isEnabled(sport, now)`
re-reads the clock on every call and `lineageCanaryEnabled` threads it through,
so a lease turns itself off with no operator, no restart, no Redis and no
network. A design where an expired canary keeps writing until someone restarts
is the same failure in a different hat.

MAX_LEASE is FOUR HOURS, and the bound is proven rather than chosen. MLB ticks
are [14,19,22,1,3] UTC; exhaustively over a minute grid across UTC date
boundaries, the shortest span enclosing THREE consecutive ticks is
22:00 -> 01:00 -> 03:00 = five hours. Four hours encloses at most two, with an
hour of margin. (An earlier note claimed six hours admitted two. It admits
three; that claim was false and the test now pins the arithmetic.) The bound is
a function of the scheduler, so a test reads the hours from sportCadence and
fails if a cadence change invalidates the proof.

Everything fails closed: absent, empty, bare sport, comma list, two leases,
duplicate @, non-leasable sport, missing timezone, numeric offset, malformed,
over-long, already-expired, expiry-equals-now, and an unreadable clock.
Configured-but-EXPIRED stays distinguishable from never-configured so automatic
containment is auditable.

No Redis, no Supabase, no counter, no network in the lease path -- an
unreachable dependency must never decide whether lineage may write.

Lineage algorithms, cache-date, participant and retention identity untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
Kev
2026-08-30 14:58:11 -04:00
parent 836b8c73d5
commit cc5bdf5797
5 changed files with 529 additions and 93 deletions
+20 -9
View File
@@ -443,25 +443,36 @@ describe('CANARY SCOPE — bounded and reversible', () => {
}
});
test('MLB is enabled only by explicit configuration', () => {
const m = load('mlb');
expect(m.lineageCanaryEnabled('mlb')).toBe(true);
test('MLB is enabled only by an explicit BOUNDED LEASE', () => {
// Plain `mlb` is no longer an activation path — it means "forever", which
// is precisely the containment defect. Only sport@absolute-UTC-expiry works.
const now = new Date('2026-08-30T12:00:00Z');
const legacy = load('mlb');
expect(legacy.lineageCanaryEnabled('mlb', now)).toBe(false);
const m = load('mlb@2026-08-30T13:00:00Z');
expect(m.lineageCanaryEnabled('mlb', now)).toBe(true);
for (const sp of ['wnba', 'nba', 'soccer']) {
expect(m.lineageCanaryEnabled(sp)).toBe(false);
expect(m.lineageCanaryEnabled(sp, now)).toBe(false);
}
// And it turns itself off when the clock crosses, with no restart.
expect(m.lineageCanaryEnabled('mlb', new Date('2026-08-30T13:00:01Z'))).toBe(false);
});
test('an empty value is a kill switch needing no migration revert', () => {
const m = load('');
expect(m.LINEAGE_CANARY_SPORTS).toEqual([]);
expect(m.lineageCanaryEnabled('mlb')).toBe(false);
expect(m.lineageCanaryEnabled('mlb', new Date('2026-08-30T12:00:00Z'))).toBe(false);
});
test('a sport without a verified event resolver cannot be canaried into safety', () => {
// Widening the flag does NOT create identity: eventIdentity still refuses,
// so a widened sport records UNSUPPORTED_SPORT and its lineage stays honest.
const m = load('mlb,wnba');
expect(m.lineageCanaryEnabled('wnba')).toBe(true);
// Two defences now. The lease refuses to widen at all (one sport, and only
// a leasable one), AND eventIdentity still refuses to invent identity, so a
// widened sport would record UNSUPPORTED_SPORT and stay honest regardless.
const now = new Date('2026-08-30T12:00:00Z');
const m = load('mlb@2026-08-30T13:00:00Z,wnba@2026-08-30T13:00:00Z');
expect(m.lineageCanaryEnabled('wnba', now)).toBe(false);
expect(m.lineageCanaryEnabled('mlb', now)).toBe(false); // the WHOLE config fails closed
const r = E.resolveEvent('wnba', dhProp('2026-08-17T17:40:00Z'), DH_GAMES);
expect(r.canonical_event_id).toBeNull();
expect(r.event_identity_method).toBe(E.IDENTITY_METHOD.UNSUPPORTED_SPORT);