Skip to content

feat(formula,lint): advisory type-soundness warnings for expressions (#1928 tier 4) - #3178

Merged
os-zhuang merged 2 commits into
mainfrom
claude/cel-js-arithmetic-null-2xtg4s
Jul 18, 2026
Merged

feat(formula,lint): advisory type-soundness warnings for expressions (#1928 tier 4)#3178
os-zhuang merged 2 commits into
mainfrom
claude/cel-js-arithmetic-null-2xtg4s

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What

Closes the last open guardrail from #1928. A Field.formula, record-scoped predicate, or flow/automation condition that uses a text or boolean field with an arithmetic (+ - * / %) or ordering (< > <= >=) operator against a number faults the runtime overload and silently evaluates to null (e.g. record.title * 2, record.is_active + 1, a flow condition status - 1 > 0). The build now surfaces this as a non-blocking warning naming the offending field and the fix.

This is Tier 4 from the issue's staged plan — deferred by the maintainer until a warning channel existed. It now does (shipped for tier 3 in #1933), so this lands as advisory warnings.

Soundness (the ADR-0032 design law — flag only what the runtime would also fail)

The build checker's type map deliberately mirrors runtime, verified empirically against the pinned cel-js (8.0.0):

  • Number / currency / percent / date / datetime → dyn, so the cases the runtime rescues never warn:
  • Equality (== / !=) is excluded — a heterogeneous equality is runtime-safe (evaluates to false), never a fault. The real example-crm formula record.status == "converted" || … is correctly not flagged.
  • In flow/automation conditions, flow variables stay dyn (unlisted → dyn) and are never flagged — only a typed field misused is.
  • Only genuinely free-text scalar types (text, textarea, email, url, phone, markdown, html, richtext) → string, and boolean/togglebool.

Changes

  • @objectstack/formula — new firstTypeMismatch(source, fieldCelTypes, scope) (builds a typed cel-js env — a record struct for record scope, bare top-level variables for flattened flow conditions — and reports arithmetic/ordering overload faults), plus an optional fieldTypes hint on validateExpression. A narrow spec-type → CEL-type map keeps false positives near-zero.
  • @objectstack/lintvalidateStackExpressions threads each object's field types into every checked site:
    • record-scoped (record.<field>) — formula fields, validation rules, action / hook / sharing predicates;
    • flattened flow / automation conditions (bare field).
      Warnings are advisory in objectstack build / validate (fatal only under --strict), matching the tier-3 channel.

Acceptance (#1928)

  • A text/boolean field misused in arithmetic/comparison is flagged at build with an actionable, field-named message.
  • The cases the runtime now handles (amount / 100, date-vs-today(), equality, string concat, null-guards, flow variables) are not flagged.
  • Covers both Field.formula / predicates and flow conditions.

Testing

  • @objectstack/formula219 green (+13: record + flattened cases; number/date/select/equality/concat/null-guard/flow-variable no-warn; scope gating).
  • @objectstack/lint232 green (+5 integration cases, incl. flow conditions).
  • Both suites green on the cel-js 7.6.1 → 8.0.0 bump; DTS builds type-check clean.

Not done (needs a decision, out of scope)

The issue also floated wiring this into an agent-callable validate_expression tool for authoring-time feedback. That tool does not exist in the codebase today, and the only runtime hook (validateFlowExpressions at registerFlow) has no object-schema access and only raises errors — so bringing tier-4 there is a runtime change (object-registry injection + a warning channel), not a small follow-up. Left for a separate, sign-off-gated PR.

Refs: #1928, ADR-0032. Runtime arithmetic fix#1930, tier-1 #1931, tier-3 #1933.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hnji7EEYR2mGt6pY53a8Bm

…1928 tier 4)
A `Field.formula` or record-scoped predicate that uses a text or boolean field
with an arithmetic (`+ - * / %`) or ordering (`< > <= >=`) operator against a
number faults the runtime overload and silently evaluates to null (e.g.
`record.title * 2`, `record.is_active + 1`). The build now surfaces this as a
NON-blocking warning naming the offending field.
Honours the ADR-0032 design law — flag only what the runtime would also fail:
number/currency/percent/date/datetime fields are declared `dyn`, so the cases
the runtime rescues never warn (`amount / 100` via registerOperator,
`due == today()` and numeric-string/ISO-date values via the string-hydration
retry, numeric-coded select options). Equality (`==`/`!=`) is excluded — a
heterogeneous equality is runtime-safe.
- formula: new `firstTypeMismatch` + optional `fieldTypes` hint on
`validateExpression`; a narrow spec-type -> CEL-type map (only free-text ->
string, boolean/toggle -> bool; everything else dyn).
- lint: `validateStackExpressions` threads each object's field types into every
record-scoped site (formula fields, validations, action/hook/sharing
predicates). Warnings are advisory in build/validate, fatal only under --strict.
Tests: formula 215 (+9), lint 230 (+3). Green on cel-js 8.0.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnji7EEYR2mGt6pY53a8Bm
@vercel

vercelBot commented Jul 18, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specCanceledCanceledJul 18, 2026 6:54am

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 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.

…nditions (#1928)
`firstTypeMismatch` gains a `scope` param: in addition to record-scoped sites
(`record.<field>`), it now covers bare-field flow/automation conditions
(`status - 1`, `is_active + 1`). The flattened env binds each field as a
top-level typed variable with `unlistedVariablesAreDyn: true`, so flow
variables stay `dyn` and are never flagged; equality stays excluded.
`validateExpression` runs the check in the flattened branch too, so the lint's
flow-condition validation surfaces the same advisory warning. Message uses the
bare `field` form in flattened scope, `record.field` in record scope.
Tests: formula 219 (+4 flattened cases), lint 232 (+2 flow-condition cases).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnji7EEYR2mGt6pY53a8Bm
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude