export interface Clock { now(): Date } export const systemClock: Clock = { now: () => new Date(), } /** Deterministic clock for tests and reproducible jobs. */ export function fixedClock(iso: string): Clock { const at = new Date(iso) return { now: () => new Date(at.getTime()) } } export function nowIso(clock: Clock = systemClock): string { return clock.now().toISOString() } /** UTC hour key like `2026-09-12T03` used for at-most-once heartbeat claims. */ export function utcHourKey(at: Date): string { return at.toISOString().slice(0, 13) } /** ms until the next UTC hour boundary. */ export function msUntilNextHour(at: Date): number { const next = Date.UTC( at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate(), at.getUTCHours() + 1, ) return next - at.getTime() } /** Day key `YYYY-MM-DD` (UTC) used for budget rollover. */ export function utcDayKey(at: Date): string { return at.toISOString().slice(0, 10) } export function addDays(at: Date, days: number): Date { return new Date(at.getTime() + days * 86_400_000) } export function isoToDate(iso: string): Date { const d = new Date(iso) if (Number.isNaN(d.getTime())) throw new Error(`invalid iso timestamp: ${iso}`) return d }