feat(adapters): recurrence pre-pass expanding RRULEs into engine inputs (TIN-1996 slice 1)#104
Merged
Merged
Conversation
…ts (TIN-1996) Add src/adapters/recurrence.ts: RecurringHoursRule / RecurringBlock types and expandRecurrence(), which expands RFC 5545 recurrence rules into the availability engine's existing input types (HoursOverride, OccupiedBlock). Engine signatures untouched; recurrence stays a pure pre-pass. RRULE parsing is delegated to the new optional peer dependency @tummycrypt/tinyland-calendar (^0.2.3) via RecurrenceEngine.parseRRule, loaded lazily with the same dynamic-import posture as the HomegrownAdapter auth-pg schema fallback (actionable RecurrencePeerUnavailableError when absent). The occurrence walk is local: at 0.2.3 the upstream walk ignores BYDAY and mis-parses compact UNTIL, so the kit walks a validated RFC 5545 subset and throws UnsupportedRecurrenceError for anything it cannot expand faithfully instead of shipping wrong recurrence math. Adds the ./recurrence subpath export mirroring ./capabilities wiring. Refs TIN-1996 (slice 1 of 3)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Slice 1 of TIN-1996 (design spike): RFC 5545 recurrence enters the availability substrate as a pure pre-pass. New
src/adapters/recurrence.tsexposesRecurringHoursRule/RecurringBlockandexpandRecurrence(rules, range), which emits the engine's existing input types (HoursOverride,OccupiedBlock).getAvailableSlots/isSlotAvailable/getDatesWithAvailabilitysignatures are untouched.@tummycrypt/tinyland-calendar^0.2.3(peerDependenciesMeta optional; also a devDependency for tests). Loaded lazily via dynamic import insideexpandRecurrence, mirroring the HomegrownAdapter auth-pg legacy-fallback pattern. Absent peer → typed, actionableRecurrencePeerUnavailableError; empty rule sets never touch the peer../recurrence→dist/adapters/recurrence.{js,d.ts}, mirroring./capabilitieswiring. Also re-exported from./adapters. No BUILD.bazel change needed: theadaptersts_projectglobssrc/adapters/**/*.ts, which picks up the new module (full MODULE.bazelbazel_depwiring is slice 3 per the spike).expandRecurrencereturns aPromise(the spike sketched it sync) because the optional peer is loaded via dynamic import on first use — same posture asloadLegacyAuthPgSchemas.Upstream fidelity gaps (spike risk #1 — confirmed, worked around honestly)
Verified against the published
tinyland-calendar@0.2.3tarball (dist/RecurrenceEngine.js):expandPattern(the private walk behindgenerateOccurrences) ignoresBYDAYentirely —FREQ=WEEKLY;BYDAY=MO,WE,FRsteps 7 days from DTSTART and emits one occurrence/week on DTSTART's weekday.parseRRuleparsesUNTILwithnew Date('YYYYMMDDTHHMMSSZ')→ Invalid Date for the RFC 5545 compact form, after whichUNTILis silently never enforced.parseRRulemaps missing/unknownFREQtodailyand silently drops unknown tokens.Per the spike's contingency, the kit uses the peer's
RecurrenceEngine.parseRRulefor tokenization but runs a local occurrence walk over a validated subset:FREQ=DAILY|WEEKLY|MONTHLY|YEARLY,INTERVAL,COUNT,UNTIL(compact + ISO, normalized locally), non-ordinalBYDAYfor DAILY (filter) / WEEKLY (expansion), positiveBYMONTHDAYfor MONTHLY,WKST=MO, plusexdates. Anything outside the subset (BYSETPOS, BYMONTH, ordinal BYDAY, BYDAY with MONTHLY/YEARLY, sub-daily FREQ, non-MO WKST, unknown tokens) throws a typedUnsupportedRecurrenceError— no silent wrong recurrence math. Once upstream exposes a correctexpandPattern(pattern, dtstart, rangeStart, rangeEnd)(upstream ask tracked in TIN-1996), the local walk can be swapped out.Timezone posture (spike risk #2): hours rules expand in pure calendar-date space (TZ remains the engine's job);
RecurringBlocktakes an optionaltimezone— when set, occurrences preserve DTSTART's wall-clock time across DST (RFC TZID semantics, reusing the engine's exportedparseTimeInTz); when unset, fixed UTC cadence. Both behaviors are pinned by tests across the 2026-03-08 US DST boundary.Tests (32 new, in
src/adapters/__tests__/recurrence.test.ts)BYDAYexpansion incl. mid-week DTSTART andINTERVAL=2week-bucketingCOUNTconsumed from DTSTART (pre-range occurrences count), post-BYDAY semanticsUNTIL(the exact forms upstream mis-parses), instant-precision UNTIL for blocksexdatesfor hours (date) and blocks (exact instant + date)BYMONTHDAY, monthly-on-the-31st month skipping, YEARLYValidation
pnpm check— 0 errors / 0 warnings (1955 files)pnpm lint— cleanpnpm test:unit— 762 passed (27 files)pnpm build—dist/adapters/recurrence.{js,d.ts}emittedpublint— "All good!"Review-panel self-check
availability-engine.ts/homegrown.ts; rebases trivially over the 0.9.0 release PR (only potential skew is the package.jsonversionfield, which this diff does not touch).expandRecurrencewith rules.Roadmap (per spike phasing)
AvailabilitySource/BusySnapshotin core types;externalBusySourceswiring intoloadOccupied();createCalDAVBusySource(sync-token + staleness semantics) behind a./caldavsubpath; mocked-CalendarClientintegration tests.bazel_depwiring, recurring-appointment write path (Acuity parity), upstream PRs (correctexpandPattern, per-instancecalendarPath, caldav-client peerDep metadata).Refs TIN-1996