# Scheduling Shared schedule time semantics for WP3 (publishing) and WP13 (multi-output schedules). Pure functions and types only — no app, database or Temporal imports, and no runtime dependency beyond Node's `Intl` tzdata. A schedule is a five-field cron expression plus an explicit IANA timezone (never server-local; `UTC` allowed). Occurrences are evaluated in that zone as nominal local date-times, then materialized to UTC fire instants. The occurrence key is the materialized UTC instant, so `UNIQUE (workspace_id, schedule_id, fire_at_utc)` holds by construction. ## Policies - **DST gap** (nonexistent local time): fires at the first valid instant after the gap (shift forward). Works for 1-hour gaps and Pacific/Apia's whole-day skip alike. - **DST fold** (ambiguous local time): fires ONCE, at its first (earlier-offset) instant. - **Collapse**: several nominal local times materializing onto one post-gap instant produce exactly one occurrence, labelled with the latest collapsed slot — the conservative value for the `last_fired_nominal_local` gate. - **Re-materialization** on tzdata or timezone change: deletes all unclaimed future occurrences, then regenerates only occurrences strictly after max(change instant, last claimed fire instant) whose nominal slot is strictly later than `last_fired_nominal_local`. A slot already fired under the old zone (daily 09:00 fired in New York, zone switched to Los Angeles) never fires again. The tzdata version is reported for recording. ## API ```ts import { parseCron, // five-field cron -> sets (rejects 6-field/`?`/`L`/`W`/`#`/macros) nextOccurrences, // (spec, from, count, {exclusive}) -> Occurrence[] materializeOccurrence, // ("2024-03-10T02:30", zone) -> epoch ms rematerialize, // (oldSpec, newSpec, storedRows, {changeInstant, ...}) canonicalTimezone, // validates + canonicalizes; rejects "local"/offsets currentTzdataVersion, // process.versions.tz } from "@tactical-media-broadcast-hub/scheduling"; ``` Errors: `SchedulingError` base, with `InvalidCronError`, `ImpossibleScheduleError`, `InvalidTimezoneError`, `InvalidNominalLocalError`, `InvalidInstantError`. Note `toInstantMs` rejects offset-less ISO datetimes that would otherwise parse in the server's local zone. ## Verification ``` pnpm exec tsc -p tsconfig.json --noEmit pnpm exec vitest run ``` `pnpm install` is required once so the workspace links this package's `vitest`/`typescript` devDependencies into `packages/scheduling/node_modules`.