feat(formula,lint): warn on date field == temporal-function silent-miss (#3183)#3192
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
7 tasks
os-zhuang
marked this pull request as ready for review
July 18, 2026 07:18
os-zhuang
force-pushed
the
claude/hydrate-date-fields-cel-equality-3183
branch
from
July 18, 2026 07:39
22edff2 to
572b914
Compare
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…3183) A `Field.date` deserializes as a "YYYY-MM-DD" string (ADR-0053 Phase 1), and cel-js's equality hard-codes `string == <timestamp>` to false, so `record.due_date == today()` compiles clean but silently never matches. cel-js offers no operator-layer hook to fix the comparison, so this adds a build-time advisory warning (the established ADR-0032 guardrail strategy) that turns the silent runtime miss into a loud, self-correcting build signal. `validateExpression` reuses the shared `ExprSchemaHint.fieldTypes` (the same per-field type map the #1928 tier-4 soundness check already threads through `@objectstack/lint`) to flag `==`/`!=` between a `date` field and today()/daysFromNow()/daysAgo()/now(), with a message pointing at the working idioms (date(...), a range, or daysBetween). Warning severity — the write/validation path may carry a real Date, so it never fails the build. Restricted to `type: 'date'` (unambiguously a string); `datetime` excluded to avoid false positives. Ordering operators already work (they fault→hydrate) and are not flagged. Complements the #1874 flow-scheduling lint, which targets a different root cause (record-change triggers firing only on the exact write day). A runtime fix (normalizing the peer of a temporal operand in the data layer) stays tracked in #3183 — a naive "hydrate date fields to Date" version would trade this silent-miss for another (breaking `dateField == "literal"`). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuiM565BZ3TR1VD3prMguB
os-zhuang
force-pushed
the
claude/hydrate-date-fields-cel-equality-3183
branch
from
July 18, 2026 07:54
572b914 to
346236d
Compare
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
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.
Part of #3183. A build-time guardrail for the cel-js date-equality silent-miss discovered while adding the ADR-0053 Slice 3 acceptance tests (#3181).
The bug
A
Field.datedeserializes as aYYYY-MM-DDstring (ADR-0053 Phase 1), and cel-js's equality hard-codesstring == <timestamp>tofalse(@marcbachmann/cel-jsoverloads.jsisEqual— returns false for a string left operand without consulting any overload, and refuses cross-type object equality). So the most natural predicate an author (or AI) writes —— compiles clean, throws nothing, and silently never matches. Same silent-miss family as #1928; timezone-independent (fails identically at UTC); cross-cutting (formulas, validation, RLS, flow/action/sharing/hook predicates).
Why a lint, not a runtime fix
cel-js gives no operator-layer hook (equality short-circuits before overload lookup). A naive runtime fix — hydrating date fields to
Datein the data layer — would trade this silent-miss for another:dateField == "2026-06-20"(string literal) would start returning false. Verified empirically. So the runtime fix needs its own design and stays tracked in #3183; this PR ships the safe, established guardrail: turn the silent runtime miss into a loud, self-correcting build warning.Changes
@objectstack/formula—ExprSchemaHint.dateFields;validateExpressionflags a==/!=between a nameddatefield (record./previous./bare) andtoday()/daysFromNow()/daysAgo()/now(), with a message pointing at the working idioms:date(record.d) == today(), a range (>= … && <= …), ordaysBetween(today(), record.d) == 0. Warning severity — never fails the build (the write/validation path may carry a realDate).@objectstack/lint—validateStackExpressionsthreads each object'sdatefield names into the validator so the warning fires across every predicate site. Restricted totype: 'date'(unambiguously a string);datetimeexcluded to avoid false positives.>=/<=/</>) already work (cel-js throws → the engine's existing string-hydration retry fires), so they are not flagged.Tests
packages/formula/src/validate.test.ts— warns on both operand orders + all four temporal fns + bare-field (flattened) scope; does NOT warn on the working idioms, ordering ops, non-date fields, or whendateFieldsis absent; de-dups repeated refs. (formula suite: 220 pass)packages/lint/src/validate-expressions.test.ts— end-to-end on formula/validation/flow sites; advisory (never fails build); no warning fordatetimeor the working idioms. (lint suite: 230 pass)tsc --noEmit+ ESLint clean on all changed files. Changeset included (minor).🤖 Generated with Claude Code
Generated by Claude Code