From c4ac2efcfba329c3146dea0965ae66adf757d859 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 09:01:20 +0000 Subject: [PATCH] docs(data-modeling): document build-time expression validation guardrails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The formula/flow guardrail series (#1928) added build-time checks that were never documented. Add a "Build-time validation" section to the formulas guide covering: qualify field references (bare-ref → null), field existence, unknown functions, and the text/boolean-in-arithmetic type-soundness warning — with their severities (error vs advisory warning), the note that integer-literal arithmetic (amount / 100) works at runtime, and how flow conditions differ (bare fields, did-you-mean typo warnings). Add a companion note + cross-link to the validation guide. Docs-only; verified against the shipped behavior in @objectstack/formula and @objectstack/lint. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Hnji7EEYR2mGt6pY53a8Bm --- content/docs/data-modeling/formulas.mdx | 31 +++++++++++++++++++++++ content/docs/data-modeling/validation.mdx | 6 +++++ 2 files changed, 37 insertions(+) diff --git a/content/docs/data-modeling/formulas.mdx b/content/docs/data-modeling/formulas.mdx index d769a84e56..5bca743cc0 100644 --- a/content/docs/data-modeling/formulas.mdx +++ b/content/docs/data-modeling/formulas.mdx @@ -196,6 +196,37 @@ Keep them pure, dependency-free, and AI-readable. --- +## Build-time validation + +`objectstack build` type-checks every expression against the object schema, so +a mistake that would silently evaluate to `null` at runtime is caught before it +ships. The same shared validator also runs when a flow is registered +(`registerFlow`), so a flow authored dynamically gets the same verdict. Findings +come at two severities: **errors** fail the build; **warnings** are advisory +(surfaced, not fatal — promote them with `--strict`). + +| Check | Example | Severity | +|:---|:---|:---| +| **Qualify field references** — a formula / validation / predicate binds only the `record` namespace, so a bare `amount` resolves to nothing and evaluates to `null`. Write `record.amount`. | `amount > 100` → `record.amount > 100` | error | +| **Field must exist** — a typo'd `record.` is flagged with a did-you-mean. | `record.amont` → `record.amount` | error | +| **Unknown function** — a misspelled or nonexistent stdlib call. | `isBlnk(record.x)` | error | +| **Type soundness** — a text or boolean field used with an arithmetic (`+ - * / %`) or ordering (`< > <= >=`) operator against a number faults at runtime and evaluates to `null`. Store it as a number field, or drop the arithmetic. | `record.title * 2` | warning | + +> **Integer literals in arithmetic are fine.** `record.amount / 100`, +> `record.price * 2`, `record.total - fee` all work — the engine handles mixed +> `double × int` arithmetic, so you never need the `100.0` / `2.0` float-literal +> workaround. Equality against any type is also safe (`record.stage == 5` simply +> returns `false`), so only arithmetic/ordering on a genuinely text/boolean +> field is flagged. + +In **flow and automation conditions** the record's fields are bound *bare* +(`stage == "won"`, not `record.stage`), so a bare reference is correct there. +Instead, a bare identifier that is a near-miss of a real field gets a +did-you-mean **warning** (a genuine flow variable is left alone), and the same +type-soundness check applies. + +--- + ## Cookbook ### Computed full name (handle nullable parts) diff --git a/content/docs/data-modeling/validation.mdx b/content/docs/data-modeling/validation.mdx index 510bf9734b..ba71ad0c27 100644 --- a/content/docs/data-modeling/validation.mdx +++ b/content/docs/data-modeling/validation.mdx @@ -337,6 +337,12 @@ functions available include: To compare against a field's previous value, reference `previous.` (e.g. `record.stage != previous.stage`). +> **Qualify your references.** A validation predicate binds only the `record` +> namespace, so a bare `status == "won"` resolves to nothing and the rule +> silently never fires — write `record.status == "won"`. `objectstack build` +> flags bare references, unknown fields, and text/boolean fields misused in +> arithmetic — see [Build-time validation](/docs/data-modeling/formulas#build-time-validation). + ## Best Practices ✅ **DO:**