# OpenSpec Change: enterprise-datetime - **Change ID**: enterprise-datetime - **Package**: `@suaveplan/datetime` - **Category**: universal - **Target path**: `packages/universal/datetime` - **Status**: IN PROGRESS - **Date**: 2026-04-18 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/datetime.md` ## Summary Greenfield implementation of `@suaveplan/datetime` for the genesis branch. The existing main-branch datetime package provides the baseline public-API surface to preserve; this change delivers timezone-aware date/time utilities with business-day arithmetic, calendar math, recurrence rules (RRule subset), relative time formatting, injectable clock, structured errors, property-based tests, OTel instrumentation, and benchmarks. ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. ## Available workspace deps on genesis | Package | Available | |---------|-----------| | `@suaveplan/error` | ✅ | | `@suaveplan/types` | ✅ | | `@suaveplan/events` | ✅ | | `@suaveplan/timing` | ❌ not yet built — no dependency taken | | `@suaveplan/streams` | ❌ not yet built — not required | | `@suaveplan/opentelemetry` | ❌ not yet built — use `@opentelemetry/api` directly | | `@suaveplan/feature-flags` | ❌ not yet built — skip | | `@suaveplan/schema-registry` | ❌ not yet built — export Zod schemas directly | External deps (via catalog): - `zod` → `catalog:forms` - `@opentelemetry/api` → `catalog:observability` - `tslib` → `catalog:build` Dev deps (via catalog): - `vitest`, `@vitest/coverage-istanbul` → `catalog:testing` - `fast-check` → `catalog:testing` - `mitata` → `catalog:testing` ## Preserved public API surface (from main branch) All of the following must be exported from the genesis build: ### Functions — Business Days - `addBusinessDays(date, n, opts?)` → `Date` - `businessDaysBetween(start, end, opts?)` → `number` - `isBusinessDay(date, opts?)` → `boolean` - `nextBusinessDay(date, opts?)` → `Date` - `previousBusinessDay(date, opts?)` → `Date` - `subtractBusinessDays(date, n, opts?)` → `Date` ### Functions — Calendar - `addDuration(date, duration)` → `Date` - `daysInMonth(year, month)` → `number` - `endOf(date, unit)` → `Date` - `getCalendarMonth(year, month)` → `CalendarMonth` - `isLeapYear(year)` → `boolean` - `isSameDay(a, b)` → `boolean` - `isSameMonth(a, b)` → `boolean` - `startOf(date, unit)` → `Date` - `subtractDuration(date, duration)` → `Date` - `toDate(input)` → `Date` ### Functions — Recurrence - `expandRecurrence(rule, opts)` → `Date[]` - `formatRRule(rule)` → `string` - `nextOccurrence(rule, from)` → `Date | null` - `parseRRule(input)` → `RecurrenceRule` ### Functions — Relative - `diffInUnits(a, b, unit)` → `number` - `formatRelative(date, opts?)` → `string` - `timeAgo(date, opts?)` → `string` - `timeUntil(date, opts?)` → `string` ### Types - `BusinessDayOptions` — `{ holidays?: Date[]; weekendDays?: Weekday[]; clock?: Clock }` - `CalendarDay` — `{ date: Date; dayOfWeek: number; isWeekend: boolean; isToday: boolean }` - `CalendarMonth` — `{ year: number; month: number; days: CalendarDay[] }` - `DateInput` — `Date | string | number` - `Duration` — `{ years?: number; months?: number; weeks?: number; days?: number; hours?: number; minutes?: number; seconds?: number; milliseconds?: number }` - `DurationUnit` — union of duration field names - `RelativeTimeOptions` — `{ locale?: string; clock?: Clock; precision?: DurationUnit }` - `Timezone` — IANA timezone string branded type - `Weekday` — 0–6 (Sunday–Saturday) - `DayAbbreviation` — 'Sun' | 'Mon' | … | 'Sat' - `Frequency` — 'daily' | 'weekly' | 'monthly' | 'yearly' - `RecurrenceOptions` — options for `expandRecurrence` - `RecurrenceRule` — `{ freq: Frequency; interval?: number; count?: number; until?: Date; byDay?: Weekday[]; byMonthDay?: number[] }` ## New additions for genesis ### Structured errors - `DatetimeError` extends `SuaveplanError` with codes under `DATETIME.*` namespace - Codes: `DATETIME.PARSE.INVALID_INPUT`, `DATETIME.RECURRENCE.INFINITE_EXPANSION`, `DATETIME.TIMEZONE.UNKNOWN` ### Zod schemas - `DateInputSchema`, `DurationSchema`, `RecurrenceRuleSchema`, `TimezoneSchema` — all public types with Zod schemas in `*.schemas.ts` siblings ### Injectable clock - All functions that read current time accept optional `{ clock?: Clock }` — zero-config default uses `Date.now()` ### OTel instrumentation - `@opentelemetry/api` counters and histograms on `expandRecurrence`, `formatRelative`, and business-day batch functions ### Property-based tests (fast-check) - Round-trip: `parseRRule → formatRRule → parseRRule` identity - Inverse pairs: `addDuration + subtractDuration`, `addBusinessDays + subtractBusinessDays` - Boundary invariants: `startOf(date, 'day')` ≤ `date` ≤ `endOf(date, 'day')` ### Benchmarks (mitata) - `expandRecurrence` 1 000-occurrence daily rule - `getCalendarMonth` 12-month batch - `businessDaysBetween` 10-year span ## Impact - Adds one new package: `packages/universal/datetime` - Adds one new spec capability: `datetime` - No existing package consumers on genesis yet