Files
2026-05-27 23:29:53 +01:00

136 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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