export interface SchedulePhase { state: T; durationSeconds: number; } export interface StateScheduleConfig { phases: readonly SchedulePhase[]; /** Repeat the phase list forever (default); `false` clamps to the last phase after one pass. */ loop?: boolean; /** Shifts the whole timeline so `t = 0` lands `offsetSeconds` into the cycle — staggers identical schedules. */ offsetSeconds?: number; } export interface ScheduleSample { state: T; /** Index into `phases`. */ index: number; elapsedSeconds: number; /** Seconds until this phase ends; `Infinity` on the clamped final phase of a non-looping schedule. */ remainingSeconds: number; /** Elapsed fraction of the phase in `[0, 1]`; `1` on the clamped final phase. */ fraction: number; } export interface ScheduleWindow { start: number; end: number; } /** * A deterministic timeline of states — position-in-schedule is a pure function of absolute time, * so a forecast preview and the live state can never disagree. One primitive behind zoned weather * shifts, hazard cycles, and timetabled machinery: `stateAt`/`sampleAt` for the live state, * `nextTransitionAt` for countdowns, `windowsOf`/`nextWindow` for "when is it safe" forecasts. */ export interface StateSchedule { /** Sum of all phase durations — one full cycle. */ readonly cycleSeconds: number; stateAt(t: number): T; sampleAt(t: number): ScheduleSample; /** Absolute time of the next phase boundary after `t`; `Infinity` once a non-looping schedule ends. */ nextTransitionAt(t: number): number; /** All windows within `[fromT, fromT + horizonSeconds]` where the state matches. */ windowsOf(match: (state: T) => boolean, fromT: number, horizonSeconds: number): ScheduleWindow[]; /** The first matching window at or after `fromT` (looking ahead `horizonSeconds`, default one full cycle). */ nextWindow(match: (state: T) => boolean, fromT: number, horizonSeconds?: number): ScheduleWindow | null; } export declare function createStateSchedule(config: StateScheduleConfig): StateSchedule; export interface ClearWindowScan { fromSeconds: number; horizonSeconds: number; /** Sampling resolution; smaller catches shorter gaps. */ stepSeconds: number; /** Reject windows shorter than this; default `0`. */ minDurationSeconds?: number; } /** * Forward-scan any predicate-of-time — a timetable mover's "is the crossing clear at `t`" — for the * next open window. Sampling-based: pick `stepSeconds` at or below half the shortest gap that matters. */ export declare function nextClearWindow(isClear: (t: number) => boolean, scan: ClearWindowScan): ScheduleWindow | null;