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

#GapBeforeAfter
cond ? <number> : null (the null-guard idiom)cel-js ternary unifier rejects intnull — even true ? 5 : null faulted → whole formula nullscompiles and evaluates
floor / ceil unregisteredfloor(...)no matching overload → nullregistered + in the catalog
date arithmetic (end - start + 1, today() + 30)compiles clean (dyn), nulls at runtime — the silent gate holebuild 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 #3183rewriteTemporalEquality; 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-zhuangand 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

vercelBot commented Jul 20, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 20, 2026 5:35am

Request Review

@github-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

1 participant

@os-zhuang