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-DDstring (ADR-0053 Phase 1), and cel-js's equality (overloads.jsisEqual) 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-jsserialize) 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) #3181KNOWN 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

vercelBot commented Jul 18, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 18, 2026 1:00pm

Request Review

@github-actionsgithub-actionsBot 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-zhuangforce-pushed the claude/date-equality-runtime-hydration-3183 branch from 143f3af to ffdc20dCompareJuly 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 mainJul 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@os-zhuang@claude