Description
Part of ADR-0053 Phase 2. Design: #1975 · Parent: #1928.
Slice 3 of 6 — compute-tz core. Make the time functions timezone-aware. Gated behind "reference tz unset → UTC".
Decision D1 (see #1975)
today()/daysFromNow()/daysAgo() return a Date at UTC-midnight of the reference-tz calendar day — new Date(Date.UTC(y, m, d)) with (y,m,d) computed in the reference tz. Not a date-only string; not local-midnight-as-instant (that trap re-introduces the silent-miss bug — cel-js's hydrateOverloadStrings rehydrates the field's date-only string to UTC midnight, so today() must match that).
Scope
- Add
timezone?: stringtoEvalContext—packages/formula/src/types.ts. registerStdLib(env, now)→registerStdLib(env, now, timezone);buildEnv(now)→buildEnv(now, ctx.timezone)—packages/formula/src/stdlib.ts:40-57,packages/formula/src/cel-engine.ts:35-42and:189-191.- Replace
startOfDayUtc/addDaysUtc(stdlib.ts:18-29) with tz-aware equivalents built on a sharedpartsInTz/calendarDayUtcutil extracted from the provenIntl.formatToPartspattern inpackages/services/service-messaging/src/preference-resolver.ts:347. No hand-rolled offset math (DST). - Side-effect: also fixes the "keeps wall-clock time" defect of
daysFromNow/daysAgo.
Acceptance criteria
- With reference tz
America/Los_Angeles, at2026-06-16T02:00Z,today()== the UTC-midnight Date of2026-06-15. -
record.due_date == today()and== daysFromNow(n)match the right calendar day across DST boundaries. - tz unset /
'UTC'→ byte-for-byte today's behavior.
Prerequisite
Confirm cel-js supports duration() + Timestamp arithmetic (the documented now() + duration("Nh") escape hatch). No duration usage exists in the formula package today.
Depends on
Slice 1 (timezone value); produces the shared partsInTz util reused by slice 5.
Activity
fix(objectql): thread execution context into read-time formula evalua…
feat(runtime): resolve a reference timezone onto ExecutionContext (#1993
feat(formula): timezone-aware today()/daysFromNow()/daysAgo() (#1980) (…
feat(formula,email): render datetime in reference timezone (ADR-0053 …
feat(settings): localization settings — platform timezone/language/fo…
os-zhuang commentedon Jul 18, 2026
Slice 3 delivered — closing as completed
The compute-tz core shipped across #1998 (the slice), #2001 (shared
calendarPartsInTzutil), and #2006 (localization settings making the reference tz configurable end-to-end). Acceptance tests are added in #3181.Scope ✔︎
EvalContext.timezoneadded —packages/formula/src/types.ts.registerStdLib(env, now, timezone)/buildEnv(now, ctx.timezone)—stdlib.ts,cel-engine.ts.startOfDayUtc/addDaysUtcreplaced by the DST-safepartsInTz/calendarDayUtc(Intl.formatToParts, no hand-rolled offset math). SharedcalendarPartsInTzextracted to@objectstack/core(reused by slice 5); formula keeps a private copy to stay a leaf.daysFromNow/daysAgofixed (now midnight-truncated).duration()prerequisite confirmed (cel-engine.test.ts).Acceptance criteria
today()at2026-06-16T02:00ZinAmerica/Los_Angeles→ UTC-midnight of2026-06-15.'UTC'→ byte-for-byte today's behavior (all three functions).Field.datetimeinstant compares equal todaysFromNow(n)across DST; aField.datestring matches via the hydration-safe idioms (date(...), ordering operators,daysBetween(...) == 0).record.due_date == today()wheredue_dateis aField.datestring silently returnsfalse. This is not a timezone problem: cel-js's equality hard-codesstring == Xto false (overloads.jsisEqual), so it can't be fixed in the formula operator layer, and it's cross-cutting (every CEL==on a date-string field). Split out as BaredateField == today()silently returns false — fix via AST temporal-comparison rewrite in the CEL engine #3183 for a type-aware hydration fix in the data layer. Workarounds documented there; aKNOWN GAPguard test in test(formula): lock ADR-0053 Phase 2 Slice 3 acceptance criteria (#1980) #3181 locks the current behavior until BaredateField == today()silently returns false — fix via AST temporal-comparison rewrite in the CEL engine #3183 lands.Closing as completed — Slice 3's deliverable (timezone-aware
today()/daysFromNow()/daysAgo()) is done and verified. The equality-hydration gap continues in #3183.Generated by Claude Code