Skip to content

fix(formula): null-guarded numeric formulas + floor/ceil + reject date arithmetic (#3306)#3348

Merged
os-zhuang merged 2 commits into
mainfrom
claude/formula-3306-null-guard-floor-date-arith
Jul 20, 2026
Merged

fix(formula): null-guarded numeric formulas + floor/ceil + reject date arithmetic (#3306)#3348
os-zhuang merged 2 commits into
mainfrom
claude/formula-3306-null-guard-floor-date-arith

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #3306.

Shipped template Field.formula fields silently evaluated to null on the pinned runtime — no parse error, no build warning, no runtime error. Root-caused to three independent CEL-engine gaps (full ground-truth diagnosis in #3306); this closes all three.

The three gaps

# Gap Before After
cond ? <number> : null (the null-guard idiom) cel-js ternary unifier rejects intnull — even true ? 5 : null faulted → whole formula nulls compiles and evaluates
floor / ceil unregistered floor(...)no matching overload → null registered + in the catalog
date arithmetic (end - start + 1, today() + 30) compiles clean (dyn), nulls at runtime — the silent gate hole build ERRORdaysBetween/daysFromNow/addDays

How

  • ① Null-guard — AST pre-pass wraps the non-null branch of cond ? x : null in dyn(...) (value-preserving, null-branch-only, idempotent), so a concrete branch unifies with null. Mirrors the merged Bare dateField == today() silently returns false — fix via AST temporal-comparison rewrite in the CEL engine #3183 rewriteTemporalEquality; applied in compile(), evaluate(), and the build soundness check.
  • ② floor/ceil — registered in stdlib (return int, round toward −∞/+∞ — not trunc, so floor(-1.2) == -2), added to CEL_STDLIB_FUNCTIONS + objectstack-formula SKILL.md (the anti-drift skill-catalog-sync gate enforces this).
  • ③ Date-arith gate — the soundness check types date/datetime fields as google.protobuf.Timestamp and flags date/duration arithmetic against a number as a hard error. Sound by construction — only arithmetic-against-a-number is flagged; ordering (date < today(), date < "2026-01-01" string-lex), equality (Bare dateField == today() silently returns false — fix via AST temporal-comparison rewrite in the CEL engine #3183), and concatenation ("Due: " + date) are runtime-tolerated and stay clean. A != null guard no longer masks the inner fault (== null no-op overloads registered in the check-only env).

Result

The two fields from the issue now compute:

tenure_years: record.hire_date == null ? null : daysBetween(record.hire_date, today()) / 365   → 6
days:         ... ? daysBetween(record.start_date, record.end_date) + 1 : null                  → 5

Tests

  • null-guard: compile + eval (incl. both hr templates, null branch → null), genuine mismatch still errors.
  • floor/ceil: semantics incl. floor(-1.2) == -2, double-arg coercion, catalog drift-guard.
  • date-arith: RED on the shipped time_off.days bug and behind a != null guard; GREEN on all 6 runtime-tolerated date uses; stack-level validateStackExpressions RED-on-broken / GREEN-on-rewrite.

242 formula tests + 239 lint tests green; @objectstack/formula typecheck + DTS clean.

Downstream heads-up: ③ adds a new build-time error. A formula/predicate doing arithmetic on a date/datetime field that previously built (and nulled at runtime) will now fail objectstack build — this only fires for expressions that were already returning null. Corrective message names the fix.

The template-side rewrites (hr_employee.tenure_years, hr_time_off_request.days) ship in a companion PR against the templates repo, plus wiring validateStackExpressions into templates CI.

🤖 Generated with Claude Code

os-zhuang and others added 2 commits July 20, 2026 09:20
fix(list): route remaining system-field groupings through shared classifier (#2706)

objectui@3b2e4d98d904d695a8372c394d46b81673011270
…e arithmetic (#3306)

Shipped template `Field.formula` fields (hr_employee.tenure_years,
hr_time_off_request.days) silently evaluated to null on the pinned runtime.
Three independent CEL-engine gaps, now closed:

1. `cond ? <value> : null` — cel-js's ternary unifier rejects a concrete
   int/double/string branch against null (even `true ? 5 : null` faulted),
   so the blessed null-guard idiom nulled. AST pre-pass wraps the non-null
   branch in dyn() (value-preserving, null-branch-only, idempotent) so it
   compiles AND evaluates. Applied in compile(), evaluate(), and the build
   soundness check. Mirrors the #3183 rewriteTemporalEquality pattern.

2. floor/ceil were unregistered — `floor(...)` faulted and nulled. Registered
   (return int, round toward -inf/+inf, NOT trunc) + added to the catalog
   (CEL_STDLIB_FUNCTIONS + objectstack-formula SKILL.md).

3. Date arithmetic (`date - date + 1`, `today() + 30`) compiled clean (dyn
   operands) but always nulls at runtime. The soundness check now types
   date/datetime fields as Timestamp and flags date/duration ARITHMETIC
   against a number as a hard build ERROR pointing at daysBetween/daysFromNow/
   addDays. Sound by construction: ordering, equality, and string concat of a
   date field stay clean (runtime-tolerated); a `!= null` guard no longer
   masks the inner fault (== null no-op overloads in the check-only env).

Tests: null-guard idiom (compile+eval, incl. the two hr templates), floor/ceil
semantics, date-arith RED on the shipped bug (incl. behind a null guard) and
GREEN on every runtime-tolerated date use, stack-level validateStackExpressions.

Fixes #3306.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 20, 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 20, 2026 5:35am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

7 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)
  • content/docs/releases/v16.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.

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

1 participant