cc5bdf5797
`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