Skip to content

feat(formula,lint): warn on date field == temporal-function silent-miss (#3183) - #3192

Merged
os-zhuang merged 1 commit into
mainfrom
claude/hydrate-date-fields-cel-equality-3183
Jul 18, 2026
Merged

feat(formula,lint): warn on date field == temporal-function silent-miss (#3183)#3192
os-zhuang merged 1 commit into
mainfrom
claude/hydrate-date-fields-cel-equality-3183

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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.date deserializes as a YYYY-MM-DDstring (ADR-0053 Phase 1), and cel-js's equality hard-codes string == <timestamp> to false (@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 —

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

— 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 Date in 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/formulaExprSchemaHint.dateFields; validateExpression flags a ==/!= between a named date field (record./previous./bare) and today()/daysFromNow()/daysAgo()/now(), with a message pointing at the working idioms: date(record.d) == today(), a range (>= … && <= …), or daysBetween(today(), record.d) == 0. Warning severity — never fails the build (the write/validation path may carry a real Date).
  • @objectstack/lintvalidateStackExpressions threads each object's date field names into the validator so the warning fires across every predicate site. Restricted to type: 'date' (unambiguously a string); datetime excluded to avoid false positives.
  • Ordering operators (>=/<=/</>) 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 when dateFields is 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 for datetime or 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

@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 8:23am

Request Review

@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 07:18
@os-zhuangos-zhuang reopened this Jul 18, 2026
@os-zhuang
os-zhuangforce-pushed the claude/hydrate-date-fields-cel-equality-3183 branch from 22edff2 to 572b914CompareJuly 18, 2026 07:39
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m and removed documentation Improvements or additions to documentation tests tooling labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

6 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/permissions/authorization.mdx(via @objectstack/lint)
  • 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.

Comment threadpackages/formula/src/validate.ts Fixed
Comment threadpackages/formula/src/validate.ts Fixed
…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
os-zhuangforce-pushed the claude/hydrate-date-fields-cel-equality-3183 branch from 572b914 to 346236dCompareJuly 18, 2026 07:54
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Jul 18, 2026
@os-zhuang
os-zhuang merged commit 80273c8 into mainJul 18, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the claude/hydrate-date-fields-cel-equality-3183 branch July 18, 2026 08:06
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 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.

3 participants

@os-zhuang@github-advanced-security@claude