Skip to content

test(formula): lock ADR-0053 Phase 2 Slice 3 acceptance criteria (#1980)#3181

Merged
os-zhuang merged 1 commit into
mainfrom
claude/timezone-aware-date-functions-9regff
Jul 18, 2026
Merged

test(formula): lock ADR-0053 Phase 2 Slice 3 acceptance criteria (#1980)#3181
os-zhuang merged 1 commit into
mainfrom
claude/timezone-aware-date-functions-9regff

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #1980 (ADR-0053 Phase 2 · Slice 3 — compute-tz core). Design: #1975 · Parent: #1928.

The implementation shipped across #1998 / #2001 / #2006. This PR adds the acceptance tests that lock the issue's criteria and pin the DST-boundary + equality behavior D1 promises. Test-only — no functional change, no changeset.

What the tests cover

All in packages/formula/src/cel-engine.test.ts, in a new ADR-0053 Phase 2 · Slice 3 acceptance criteria (#1980) block:

  • AC1today() at 2026-06-16T02:00Z with reference tz America/Los_Angeles → the UTC-midnight Date of 2026-06-15 (plus daysFromNow(1)/daysAgo(1)).
  • AC3 — reference tz unset vs 'UTC' is byte-for-byte identical for all three functions (today's behavior preserved).
  • AC2 — calendar days land correctly across both 2026 US DST transitions (spring-forward Mar 8, fall-back Nov 1); a Field.datetime instant compares equal to daysFromNow(n) across DST; a Field.date string matches today()/daysAgo() via the hydration-safe idioms (ordering operators, date(...), daysBetween(...) == 0).

Verification

  • @objectstack/formula213 tests pass (7 new), tsc --noEmit clean.

⚠️ Documented gap (separate follow-up)

AC2 lists record.due_date == today(). A Field.date reads back as a YYYY-MM-DD string (ADR-0053 Phase 1, #1968), and cel-js's equality hard-codes string == X to false (overloads.js isEqual returns false for a string left operand without ever consulting a registered overload — and refuses cross-type object equality). So a bare date-string == today() silently returns false:

  • It is timezone-independent (fails identically at UTC) and cross-cutting (affects every CEL site comparing a date field with ==/!=: formulas, validation, RLS, flow conditions, action predicates) — so it is out of scope for this slice and cannot be fixed in the formula operator layer.
  • The fix belongs in the data layer, hydrating date fields to Date before evaluation where field-type metadata is known.
  • Until then the working author idioms are date(record.d) == today(), ordering operators, or daysBetween(today(), record.d) == 0.

A characterization guard test locks this known behavior (clearly marked KNOWN GAP, not an endorsement) so it can't silently change. Recommend tracking the fix as its own issue.

🤖 Generated with Claude Code


Generated by Claude Code

Add acceptance tests for the timezone-aware today()/daysFromNow()/daysAgo()
functions (compute-tz core of ADR-0053 Phase 2, decision D1). The
implementation already shipped (#1998/#2001/#2006); these lock the issue's
criteria and pin the DST-boundary + equality behavior:

- AC1: today() at 2026-06-16T02:00Z in America/Los_Angeles == UTC-midnight of
  2026-06-15.
- AC3: reference tz unset vs 'UTC' is byte-for-byte the pre-Phase-2 behavior
  for all three functions.
- AC2: calendar days are correct across both 2026 US DST transitions
  (spring-forward Mar 8, fall-back Nov 1); a Field.datetime instant compares
  equal to daysFromNow(n) across DST; a Field.date string matches via the
  hydration-safe idioms (ordering operators, date(), daysBetween()).
- A characterization guard documents the known cel-js equality limitation:
  a bare `date-string == today()` silently returns false because cel-js's
  isEqual hard-codes `string == X` to false. This is timezone-independent and
  cross-cutting; the fix belongs in the data layer (hydrate date fields to Date
  where field types are known) and is tracked as a separate follow-up.

Test-only; no changeset (no functional change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuiM565BZ3TR1VD3prMguB
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jul 18, 2026 6:35am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/formula.

5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx (via @objectstack/formula)
  • content/docs/data-modeling/validation.mdx (via @objectstack/formula)
  • content/docs/plugins/packages.mdx (via @objectstack/formula)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/formula)
  • content/docs/releases/v15.mdx (via @objectstack/formula)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 06:51
@os-zhuang
os-zhuang merged commit 5c0de05 into main Jul 18, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the claude/timezone-aware-date-functions-9regff branch July 18, 2026 06:52
os-zhuang pushed a commit that referenced this pull request Jul 18, 2026
…arison rewrite (#3183)

A `Field.date` reads back as a "YYYY-MM-DD" string (ADR-0053 Phase 1), and
cel-js's equality treats a string and a timestamp as unequal without consulting
any overload, so `record.due_date == today()` silently returned false (and
`!= today()` silently true) even for a same-day record.

celEngine.evaluate now rewrites the parsed AST: for each `==`/`!=` whose one
operand is today()/daysFromNow()/daysAgo()/now(), the field operand is wrapped in
date(...) (the stdlib coercion), then serialized and evaluated — so
`record.due_date == today()` runs as `date(record.due_date) == today()`.

- Per-occurrence: a mixed `d == "literal" || d == today()` keeps the literal
  comparison intact while fixing the temporal one.
- Type-blind-safe: date() degrades gracefully (already-Date passes through;
  non-date string / null → Invalid Date → stays false), so no field types are
  needed and no currently-correct result is worsened.
- Cheap: reserializes only when such a comparison is present (plain-includes
  gate) and memoizes source → rewritten source. AST-based, no regex on input.

Covers every interpreter site (formulas, defaults, validation, hooks, flow
conditions) via the single evaluate chokepoint. RLS/sharing unaffected
(cel-to-filter rejects function calls loudly).

Supersedes the #3192 advisory lint (checkTemporalDateEquality + the
temporalEqualityFields helper), now removed — with the runtime fixed it would be
a false alarm. Flips the #3181 KNOWN GAP characterization test to assert the fix,
and adds an objectql end-to-end test (a date formula field read via find()).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuiM565BZ3TR1VD3prMguB
os-zhuang added a commit that referenced this pull request Jul 18, 2026
…arison rewrite (#3183) (#3205)

A `Field.date` reads back as a "YYYY-MM-DD" string (ADR-0053 Phase 1), and
cel-js's equality treats a string and a timestamp as unequal without consulting
any overload, so `record.due_date == today()` silently returned false (and
`!= today()` silently true) even for a same-day record.

celEngine.evaluate now rewrites the parsed AST: for each `==`/`!=` whose one
operand is today()/daysFromNow()/daysAgo()/now(), the field operand is wrapped in
date(...) (the stdlib coercion), then serialized and evaluated — so
`record.due_date == today()` runs as `date(record.due_date) == today()`.

- Per-occurrence: a mixed `d == "literal" || d == today()` keeps the literal
  comparison intact while fixing the temporal one.
- Type-blind-safe: date() degrades gracefully (already-Date passes through;
  non-date string / null → Invalid Date → stays false), so no field types are
  needed and no currently-correct result is worsened.
- Cheap: reserializes only when such a comparison is present (plain-includes
  gate) and memoizes source → rewritten source. AST-based, no regex on input.

Covers every interpreter site (formulas, defaults, validation, hooks, flow
conditions) via the single evaluate chokepoint. RLS/sharing unaffected
(cel-to-filter rejects function calls loudly).

Supersedes the #3192 advisory lint (checkTemporalDateEquality + the
temporalEqualityFields helper), now removed — with the runtime fixed it would be
a false alarm. Flips the #3181 KNOWN GAP characterization test to assert the fix,
and adds an objectql end-to-end test (a date formula field read via find()).


Claude-Session: https://claude.ai/code/session_01SuiM565BZ3TR1VD3prMguB

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Jul 20, 2026
…arison rewrite (objectstack-ai#3183)

A `Field.date` reads back as a "YYYY-MM-DD" string (ADR-0053 Phase 1), and
cel-js's equality treats a string and a timestamp as unequal without consulting
any overload, so `record.due_date == today()` silently returned false (and
`!= today()` silently true) even for a same-day record.

celEngine.evaluate now rewrites the parsed AST: for each `==`/`!=` whose one
operand is today()/daysFromNow()/daysAgo()/now(), the field operand is wrapped in
date(...) (the stdlib coercion), then serialized and evaluated — so
`record.due_date == today()` runs as `date(record.due_date) == today()`.

- Per-occurrence: a mixed `d == "literal" || d == today()` keeps the literal
  comparison intact while fixing the temporal one.
- Type-blind-safe: date() degrades gracefully (already-Date passes through;
  non-date string / null → Invalid Date → stays false), so no field types are
  needed and no currently-correct result is worsened.
- Cheap: reserializes only when such a comparison is present (plain-includes
  gate) and memoizes source → rewritten source. AST-based, no regex on input.

Covers every interpreter site (formulas, defaults, validation, hooks, flow
conditions) via the single evaluate chokepoint. RLS/sharing unaffected
(cel-to-filter rejects function calls loudly).

Supersedes the objectstack-ai#3192 advisory lint (checkTemporalDateEquality + the
temporalEqualityFields helper), now removed — with the runtime fixed it would be
a false alarm. Flips the objectstack-ai#3181 KNOWN GAP characterization test to assert the fix,
and adds an objectql end-to-end test (a date formula field read via find()).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SuiM565BZ3TR1VD3prMguB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants