Skip to content

feat(formula): dateField == today() now matches — AST temporal-comparison rewrite (#3183)#3205

Merged
os-zhuang merged 1 commit into
mainfrom
claude/date-equality-runtime-hydration-3183
Jul 18, 2026
Merged

feat(formula): dateField == today() now matches — AST temporal-comparison rewrite (#3183)#3205
os-zhuang merged 1 commit into
mainfrom
claude/date-equality-runtime-hydration-3183

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3183. The runtime fix for the date-equality silent-miss the #3192 guardrail warned about.

The bug

A Field.date reads back as a YYYY-MM-DD string (ADR-0053 Phase 1), and cel-js's equality (overloads.js isEqual) treats a string and a timestamp as unequal without consulting any overload. So the most natural predicate —

record.due_date == today()      // silently false, even when due_date IS today
record.due_date != today()      // silently true for a same-day record

— compiled clean and silently returned the wrong answer. Ordering operators already worked (cel-js throws for them → the engine's existing string-hydration retry fires); only ==/!= failed silently.

The fix — AST temporal-comparison rewrite

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 (@marcbachmann/cel-js serialize) and evaluated. So record.due_date == today() runs as date(record.due_date) == today() — comparing two Timestamps.

  • Per-occurrence, not per-fieldrecord.d == "2026-06-20" || record.d == today() keeps the string-literal comparison intact while fixing the temporal one. No trade-off, no fail-safe skip.
  • Type-blind-safedate()/toDate degrades gracefully: an already-Date (Field.datetime) operand passes through; a non-date string or null → Invalid Date → the comparison stays false, exactly as before. No field-type info needed, and no currently-correct result is worsened.
  • Cheap → reserializes only when such a comparison is present (a plain-includes gate skips the rest) and memoizes source → rewritten source, so applyFormulaPlan's per-row × per-formula loop parses once per distinct source. AST-based — no regex on author input (no ReDoS).
  • Idempotentdate(record.d) is a call, not a field ref, so it's never re-wrapped.

Covers every interpreter site (read-time formulas, defaults, validation rules, hook conditions, flow conditions) through the single evaluate chokepoint. RLS/sharing conditions are unaffected — they compile via cel-to-filter, which already rejects function calls as a loud authoring error.

Supersedes the #3192 advisory lint

With the runtime fixed, the build-time warning (checkTemporalDateEquality + the temporalEqualityFields helper) would be a false alarm, so it's removed. Authors write the natural record.due_date == today() directly.

Tests

  • cel-engine.test.tsrewriteTemporalEquality unit tests (all four fns, both orders, idempotent, per-occurrence mixed case, adversarial input no-throw); end-to-end eval (string date field matches; != dual; literal unchanged; already-Date/non-date/null unaffected). Flips the test(formula): lock ADR-0053 Phase 2 Slice 3 acceptance criteria (#1980) #3181 KNOWN GAP test to assert the fix.
  • objectql/engine.test.ts — end-to-end: an is_due_today formula field on a record read via engine.find() (real driver YYYY-MM-DD round-trip) evaluates true.
  • Removed the obsolete feat(formula,lint): warn on date field == temporal-function silent-miss (#3183) #3192 lint tests.
  • Green: @objectstack/formula 233 · @objectstack/lint 230 · @objectstack/objectql 941; tsc + ESLint clean on all three. Changeset: @objectstack/formula minor (behavior change called out).

🤖 Generated with Claude Code


Generated by Claude Code

@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 Ready Ready Preview, Comment Jul 18, 2026 1:00pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/formula, @objectstack/lint, @objectstack/objectql.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/formula, packages/objectql)
  • content/docs/data-modeling/validation.mdx (via @objectstack/formula)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/formula, @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/formula)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)
  • content/docs/releases/v15.mdx (via @objectstack/formula)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

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.

…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
os-zhuang force-pushed the claude/date-equality-runtime-hydration-3183 branch from 143f3af to ffdc20d Compare July 18, 2026 12:24
@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 12:24
@os-zhuang
os-zhuang merged commit 6b51346 into main Jul 18, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the claude/date-equality-runtime-hydration-3183 branch July 18, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bare dateField == today() silently returns false — fix via AST temporal-comparison rewrite in the CEL engine

2 participants