Skip to content

Commit e6b7d8c

Browse files
fix(spec): rewrite the cross_field and script examples in validation.zod.ts in evaluable CEL, un-inverting two (#20252) (#20276)
Fixes #20252 Clause-②: no ## Rework round 1: what changed since the first revision The order of record is the seat's comment 5858340569 on #20252. - **File-surface amendment to claim 5858020673**, recorded by the seat in that comment rather than as a second claim. It adds three more sites in `packages/spec/src/data/validation.zod.ts`, text only: the header `script` example at :100, `cross_field` Example 2 at :257, and the `.describe()` on `CrossFieldValidationSchema.condition` at :294. - **Generated consequence:** `content/docs/references/data/validation.mdx`, regenerated by `pnpm --filter @objectstack/spec gen:docs`. It is generator output, not hand-edited. `check:docs` went stale because both the file-header docblock and the describe render into that page. - **Commits:** - round 0: `ec694f0b55` (Examples 1 and 3), unchanged; - round 1: `efc0b4aa15` (the three sites and the changeset text) and `08cb6583fe` (the regenerated page). - There was no rebase, amend or force-push. `origin/main` had not moved in `validation.zod.ts`, so there was no merge. - The round-0 rewrites of Examples 1 and 3 are kept byte for byte. ## What changed (every example predicate in `validation.zod.ts`) | site | before | after | |:--|:--|:--| | :100, header `script` example | `discount_percent > 0.40` (bare field) | `record.discount_percent > 0.40` | | :240, `cross_field` Example 1 | `MONTH(close_date) >= MONTH(TODAY()) AND YEAR(close_date) >= YEAR(TODAY())` (does not parse, and inverted) | `date(record.close_date) < addDays(today(), 1 - today().getDate())` | | :257, `cross_field` Example 2 | `discount > (amount * 0.40)` (bare fields) | `record.discount > (record.amount * 0.40)` | | :274, `cross_field` Example 3 | `products = null AND stage = "closed_won"` (does not parse) | `isBlank(record.products) && record.stage == "closed_won"` | | :294, `.describe()` on `CrossFieldValidationSchema.condition` | ``Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` `` (inverted) | ``Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date.`` | The Salesforce-formula side of every example is byte-for-byte unchanged. This is documentation text only: the TSDoc, one `.describe()` string and the regenerated reference page. There is no schema, behaviour or export change. Changeset: `@objectstack/spec` patch. ## Semantics: TRUE is the violation `evaluateRule` dispatches both `script` and `cross_field` to `checkPredicate` (`packages/objectql/src/validation/rule-validator.ts`), which returns a `rule_violation` only when `result.value === true` (line 4078 at `ec694f0b55`). So every example's condition must be TRUE on the record the rule rejects. - **Example 1 was inverted, and is un-inverted.** The old logic, with only its syntax repaired, is `true` for a 2026-09-15 close date and `false` for 2026-08-31 on a 2026-09-27 clock (transcript C). - **The `.describe()` example was inverted, and is un-inverted.** `record.end_date > record.start_date` evaluates to `true` on a valid end-after-start range and `false` on a real violation (transcript E). It becomes `record.end_date < record.start_date`, and the description now says a TRUE condition fails validation. The sibling `ScriptValidationSchema.condition` description already says this. - **Examples 2 and 3 and the header `script` example keep their direction.** Only their spelling changed. ## Functions: what the stdlib offers, and the choice made for Example 1 Measured in `packages/formula/src/stdlib.ts` (`registerStdLib`). The date functions are `now()`, `today()`, `daysFromNow(n)`, `daysAgo(n)`, `daysBetween(a, b)`, `addDays(d, n)`, `addMonths(d, n)`, `date(x)` and `datetime(x)`. The stdlib registers no `MONTH` / `YEAR` / `TODAY`. `today()` returns the reference-timezone calendar day as a UTC-midnight Timestamp (line 247). Month and year extraction exist as cel-js built-in Timestamp receiver methods (`@marcbachmann/cel-js` 8.0.0 `lib/functions.js`). `getDate()` is the 1-based day of the month, read in UTC. The platform accepts the receiver form. `validate.test.ts` keeps `record.created.getFullYear()` valid, and `validate.ts` documents the receiver-only names as resolving but ineligible for the bare-call catalog `CEL_STDLIB_FUNCTIONS`. So Example 1 **is expressible and was kept, not replaced**. `addDays(today(), 1 - today().getDate())` is the first day of the current month, and `date(record.close_date)` accepts both a `Field.date` string and a driver-hydrated `Date`. Example 3 uses the stdlib's `isBlank`, the stdlib's documented `ISBLANK()` analog, which is TRUE for null, `''` and `[]`. ## Evaluation transcript (`ExpressionEngine.evaluate`, `@objectstack/formula` dist built from the spec source at head `08cb6583fe`) **Method, the same one PR #20239 used.** - Each docblock `condition:` literal is extracted from `git show REF:packages/spec/src/data/validation.zod.ts` byte for byte, with the comment prefix stripped and the quotes kept. - It is then unescaped by Node's own parser, `(0, eval)(literal)`, to get exactly the runtime string a reader's paste produces. It is never hand-retyped. - For the `.describe()` site, the describe literal is unescaped the same way, which gives the published description text. The P-tagged example is then cut out of that text and handed to Node as the tagged template a reader would paste, with the spec's real `P` helper in scope. Its `source` is what gets evaluated. - `validateExpression('predicate', src, { scope: 'record', … })`, the verdict `@objectstack/lint` gives a validation-rule condition, is printed beside each. **Clock.** The evaluation clock is fixed through `EvalContext.now`: `2026-09-27T12:00:00Z`, plus `2027-01-15T12:00:00Z` for the year boundary. Two Example 1 rows use the real clock instead, with dates computed relative to the run date: the first day of the current UTC month, and the day before it. Timezone is the evaluator default, UTC, which is what `checkPredicate` passes. A row with a JS `Date` value (the driver-hydrated shape) renders as its ISO string. None of the round-1 sites depends on `today()`. ### A. Examples 1 and 3 at head `08cb6583fe` (unchanged since round 0) ```text ## Example 1 (close_date_future) — packages/spec/src/data/validation.zod.ts:240 raw docblock literal : 'date(record.close_date) < addDays(today(), 1 - today().getDate())' runtime string : "date(record.close_date) < addDays(today(), 1 - today().getDate())" validateExpression('predicate', …, scope:'record') : ok=true errors=[] warnings=0 violating now=2026-09-27T12:00:00.000Z record={"close_date":"2026-08-31"} -> {"ok":true,"value":true} OK valid now=2026-09-27T12:00:00.000Z record={"close_date":"2026-09-01"} -> {"ok":true,"value":false} OK valid now=2026-09-27T12:00:00.000Z record={"close_date":"2026-10-15"} -> {"ok":true,"value":false} OK valid now=2026-09-27T12:00:00.000Z record={"close_date":"2027-01-10"} -> {"ok":true,"value":false} OK violating now=2026-09-27T12:00:00.000Z record={"close_date":"2026-08-31T00:00:00.000Z"} -> {"ok":true,"value":true} OK violating now=2027-01-15T12:00:00.000Z record={"close_date":"2026-12-31"} -> {"ok":true,"value":true} OK valid now=2027-01-15T12:00:00.000Z record={"close_date":"2027-01-01"} -> {"ok":true,"value":false} OK violating now=REAL-CLOCK record={"close_date":"2026-08-31"} -> {"ok":true,"value":true} OK valid now=REAL-CLOCK record={"close_date":"2026-09-01"} -> {"ok":true,"value":false} OK ## Example 3 (products_required_for_won) — packages/spec/src/data/validation.zod.ts:274 raw docblock literal : 'isBlank(record.products) && record.stage == "closed_won"' runtime string : "isBlank(record.products) && record.stage == \"closed_won\"" validateExpression('predicate', …, scope:'record') : ok=true errors=[] warnings=0 violating record={"products":null,"stage":"closed_won"} -> {"ok":true,"value":true} OK violating record={"products":[],"stage":"closed_won"} -> {"ok":true,"value":true} OK valid record={"products":["prod_001"],"stage":"closed_won"} -> {"ok":true,"value":false} OK valid record={"products":null,"stage":"prospecting"} -> {"ok":true,"value":false} OK ``` Re-run at head `08cb6583fe`: 13 of 13 rows OK, 0 mismatches. ### B. Examples 1 and 3 before (BASE `a9fb83ef06`), same extraction ```text Example 1 raw literal: 'MONTH(close_date) >= MONTH(TODAY()) AND YEAR(close_date) >= YEAR(TODAY())' validateExpression: ok=false — invalid CEL predicate: Unexpected character: 'D' evaluate (both records): {"ok":false,"error":{"kind":"parse","message":"Unexpected character: 'D' …"}} Example 3 raw literal: 'products = null AND stage = "closed_won"' validateExpression: ok=false — invalid CEL predicate: Unexpected character: = evaluate (both records): {"ok":false,"error":{"kind":"parse","message":"Unexpected character: = …"}} ``` ### C. Example 1's inversion, shown on the old logic with only its syntax repaired ```text date(record.close_date).getMonth() >= today().getMonth() && date(record.close_date).getFullYear() >= today().getFullYear() now=2026-09-27T12:00:00.000Z close_date=2026-09-15 -> {"ok":true,"value":true} (a valid record, refused) now=2026-09-27T12:00:00.000Z close_date=2026-08-31 -> {"ok":true,"value":false} (an invalid record, accepted) ``` ### D. Round 1: the three new sites at head `08cb6583fe` ```text ## Header script example (discount_cannot_exceed_40_percent) — packages/spec/src/data/validation.zod.ts:100 raw docblock literal : 'record.discount_percent > 0.40' runtime string : "record.discount_percent > 0.40" validateExpression('predicate', …, scope:'record') : ok=true errors=[] warnings=0 violating record={"discount_percent":0.5} -> {"ok":true,"value":true} OK valid record={"discount_percent":0.1} -> {"ok":true,"value":false} OK valid record={"discount_percent":0.4} -> {"ok":true,"value":false} OK ## Example 2 (discount_limit) — packages/spec/src/data/validation.zod.ts:257 raw docblock literal : 'record.discount > (record.amount * 0.40)' runtime string : "record.discount > (record.amount * 0.40)" validateExpression('predicate', …, scope:'record') : ok=true errors=[] warnings=0 violating record={"discount":500,"amount":1000} -> {"ok":true,"value":true} OK valid record={"discount":100,"amount":1000} -> {"ok":true,"value":false} OK valid record={"discount":400,"amount":1000} -> {"ok":true,"value":false} OK ## CrossFieldValidationSchema.condition .describe() — packages/spec/src/data/validation.zod.ts:294 published text : "Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date." copied example : P`record.end_date < record.start_date` P`…` evaluates to : {"dialect":"cel","source":"record.end_date < record.start_date"} validateExpression('predicate', …, scope:'record') : ok=true errors=[] warnings=0 violating record={"start_date":"2026-09-10","end_date":"2026-09-01"} -> {"ok":true,"value":true} OK valid record={"start_date":"2026-09-01","end_date":"2026-09-10"} -> {"ok":true,"value":false} OK valid record={"start_date":"2026-09-01","end_date":"2026-09-01"} -> {"ok":true,"value":false} OK violating record={"start_date":"2026-09-10T00:00:00.000Z","end_date":"2026-09-01T00:00:00.000Z"} -> {"ok":true,"value":true} OK ``` 10 of 10 rows OK, 0 mismatches. ### E. Round 1: the same three sites before (round-0 head `ec694f0b55`), same extraction ```text :100 'discount_percent > 0.40' validateExpression: ok=false — "bare reference `discount_percent` … Write `record.discount_percent`." evaluate (all three records): {"ok":false,"error":{"kind":"type","message":"Unknown variable: discount_percent …"}} :257 'discount > (amount * 0.40)' validateExpression: ok=false — "bare reference `discount` … Write `record.discount`." evaluate (all three records): {"ok":false,"error":{"kind":"type","message":"Unknown variable: discount …"}} :294 published text "Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date`" validateExpression: ok=true (it parses; the defect is its direction) violating record={"start_date":"2026-09-10","end_date":"2026-09-01"} -> {"ok":true,"value":false} (a real violation, accepted) valid record={"start_date":"2026-09-01","end_date":"2026-09-10"} -> {"ok":true,"value":true} (a valid range, refused) valid record={"start_date":"2026-09-01","end_date":"2026-09-01"} -> {"ok":true,"value":false} violating Date-valued start 2026-09-10 / end 2026-09-01 -> {"ok":true,"value":false} (accepted) ``` ## Enumerating checks ### The extended check: every example predicate references fields only through `record.` The instrument is a script run against `git show REF:packages/spec/src/data/validation.zod.ts`. - **Population:** every docblock `condition:` / `when:` literal, plus every P-tagged example inside a `.describe()` string, each unescaped as a reader's paste would be. - **Verdict:** roots are read off the parsed CEL AST by `@objectstack/formula`'s `collectCelRootIdentifiers`, which never reports member or function names. A site is a hit when it has a root other than `record`, or when it does not parse. - **Coverage control:** any line that spells a P-tagged example or a docblock `condition` / `when` key but yielded no site is printed as UNREAD. There were 0 UNREAD lines. | ref | predicates read | hits | |:--|:--|:--| | head `08cb6583fe` | 26 | **0** | | round-0 head `ec694f0b55` (control) | 26 | 2: `:100 discount_percent`, `:257 discount, amount` | | BASE `a9fb83ef06` (control) | 26 | 4: `:100`, `:257`, and `:240` / `:274` do not parse | At head, the 26 sites are: - docblock literals at :100, :240, :257, :274, and 19 in the `ConditionalValidationSchema` docblock (:378–:532); - P-tagged `.describe()` examples at :180 (`record.amount < 0`), :294 (`record.end_date < record.start_date`) and :546 (`record.type == 'enterprise'`). Every one has roots `["record"]`. **No remaining hit.** A root check cannot see a direction error: the round-0 describe example was record-rooted and still inverted. Direction is covered by the evaluations above, not by this check. ### The card's original grep `grep -n -E "^\s*\*.*(condition|when)\s*:\s*['\"].*([^=!<>]=[^=]|\bAND\b|\bOR\b)"` over `validation.zod.ts`: - head `08cb6583fe`: **0 hits** (exit 1); - control leg, BASE `a9fb83ef06`: lines 240 and 274 (exit 0). It has no Salesforce-side hit either. The pattern anchors on `condition:` / `when:` followed by a quote, which no Salesforce-formula line carries. ## Where each string ships, and the dist check (spec built from the head `08cb6583fe` source) - **Examples 1, 2 and 3** ship in `packages/spec/dist/object.zod-Bjpk_moO.d.ts`. Each new string appears ×1 there, each old string ×0. - Untouched control: `condition: 'record.amount > 100000 && record.approval == null'` ×1. - **The describe** ships in the runtime bundles, not in a `.d.ts`. The new text appears ×1 in each of the 22 dist JS files that carry it, and in the gitignored `json-schema/` output (14 files). The old example ``P`record.end_date > record.start_date` `` appears ×0 across `dist/` and `json-schema/`. - In the generated `content/docs/references/data/validation.mdx`: new ×4, old ×0. There are four rows because the schema renders four times, the same count as before. - **The header `script` example** (:100) is in the file-level comment, which is attached to no declaration, so it ships in no `dist/` file, before or after. It ships in the generated `validation.mdx`: `condition: 'record.discount_percent > 0.40'` ×1, `condition: 'discount_percent > 0.40'` ×0. ## Fixtures No `validation.test.ts` fixture copies any of the five strings verbatim, so no test is touched. The fixture named `close_date_future` at `validation.test.ts:267` is a `script` rule with `close_date < TODAY()`, a different string. ## Local verification (all at head `08cb6583fe`) - **Builds.** - `pnpm --workspace-concurrency=2 --filter '@objectstack/formula...' build` (spec + formula): VERDICT command-exit 0. It ran at `efc0b4aa15`, whose `packages/` tree equals `08cb6583fe` (`git diff --stat efc0b4a 08cb658 -- packages/` is empty). - `@objectstack/sdui-parser` + `@objectstack/lint` build: exit 0. - **Tests.** `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: `Test Files 550 passed (550)`, `Tests 16239 passed | 1 todo (16240)`. - **Typecheck.** `pnpm --filter @objectstack/spec typecheck`: exit 0. The test layer is held at its ledger: "53 file(s) / 255 error(s) / 142 pinned signature(s)". - **Generated artifacts.** `pnpm --filter @objectstack/spec check:generated`: - first run: "1 of 15 artifact(s) stale: content/docs/references/**"; - after `gen:docs`: "All 15 generated artifacts are up to date". - **Gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list` gave "101 derived famil(ies) accounted for — 98 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)". All 98 that ran exited 0, 0 UNRUN. The union was re-derived for the new 3-path change set and re-run in full at this head. - **NOT MEASURED: `check:dual-build-cjs-loads` and `check:lean-entry-closure`.** Both refuse with PREREQUISITE NOT MET. They need the whole-workspace build and the `@objectstack/objectql` build closure. CI's lanes run them. - **NOT MEASURED: `check:skill-examples`.** It refuses with PREREQUISITE NOT MET because the `@objectstack/client-react` / `@objectstack/client` `.d.ts` build closure is absent. That closure is 36 workspace packages, out of this card's local scope. The gate reads only blocks carrying its `os:check` marker, and neither changed file carries one (grep count 0 in each). CI runs it. - **NOT MEASURED locally: the Clause-② level axis of `check-changeset-no-major`.** It is PR-scoped and needs a `pull_request` payload. Its no-major half is green. - **eslint, a proven narrowing** of the CI-owned `pnpm lint`: - Population, read from eslint's own config: `validation.zod.ts` is linted (`parserOptions.project` undefined). The `.changeset` file and the `.mdx` page are ignored ("no matching configuration"). - `--format json` over the three changed paths: 3 results. `validation.zod.ts` has 0 errors and 0 warnings. - Invariance: type-aware linting is not enabled anywhere in `eslint.config.mjs`, so this diff cannot move any untouched file's verdict. ## Acceptance notes - **The Salesforce formula beside Example 1 is kept as it is, per the card, but it is not a strict "before the current month" test.** `MONTH(CloseDate) < MONTH(TODAY()) || YEAR(CloseDate) < YEAR(TODAY())` also fires on a future-year date with a smaller month number, for example 2027-01-10 when today is 2026-09-27. The ObjectStack condition implements the example's stated intent, and that row reads `false` in transcript A. - **The parse-only `validation.test.ts` cross_field fixtures stay out of scope, per the order.** They still carry non-CEL strings such as `amount > 1000 AND discount_percent > 50` and `percent_a + percent_b + percent_c = 100`. They are schema-acceptance fixtures, since the schema does not compile CEL, and are not documented examples. - **Left as prose on purpose:** the "Use Cases" bullets in the `cross_field` docblock, `Date range validations (end_date > start_date)` and `Amount comparisons (discount < total)`. They state the requirement each rule enforces. They are not `condition:` / `when:` / describe predicates, so the extended check does not read them. - **The PR title still reads "the two cross_field … conditions".** This round's single write is a body-only edit, so the title is left for the seat. Authored by the `domain:spec` seat 2 dispatch, session `session_01QcAS3qiYYZNezaxZxaUdMV` (https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV). --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ae8e3ca commit e6b7d8c

3 files changed

Lines changed: 25 additions & 10 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Every example predicate in `packages/spec/src/data/validation.zod.ts` now reads fields through the `record.` root and describes the violation, so an author or agent who copies one gets a rule that evaluates and fires on the records it names as invalid. The fix covers the three `cross_field` "Salesforce Examples" in the `CrossFieldValidationSchema` TSDoc, the header `script` example, and the `.describe()` example on `CrossFieldValidationSchema.condition` (#20252).
6+
7+
Clause-②: no
8+
9+
- Example 1, "Close Date Must Be In Current or Future Month": `MONTH(close_date) >= MONTH(TODAY()) AND YEAR(close_date) >= YEAR(TODAY())` becomes `date(record.close_date) < addDays(today(), 1 - today().getDate())`. The old string did not parse (`AND` is not CEL, and `close_date` had no `record.` root). It was also inverted: a `cross_field` condition that evaluates TRUE is the violation, and the old condition was TRUE on the records the rule should accept. The new condition is TRUE when the close date falls before the first day of the current month. It uses only the formula stdlib's `date()`, `today()` and `addDays()` plus CEL's built-in `getDate()` timestamp accessor.
10+
- Example 2, "Discount Validation": `discount > (amount * 0.40)` becomes `record.discount > (record.amount * 0.40)`. The bare fields were unknown variables. The direction is unchanged.
11+
- Example 3, "Opportunity Must Have Products": `products = null AND stage = "closed_won"` becomes `isBlank(record.products) && record.stage == "closed_won"`. A lone `=` is a CEL parse error and `AND` is not CEL. The direction is unchanged.
12+
- The header `script` example: `discount_percent > 0.40` becomes `record.discount_percent > 0.40`. The bare field was an unknown variable. The direction is unchanged.
13+
- The `CrossFieldValidationSchema.condition` description: its example `record.end_date > record.start_date` refused every valid end-after-start range, because a TRUE condition is the violation. It becomes `record.end_date < record.start_date`, and the description now says that a TRUE condition fails validation.
14+
15+
The Salesforce-formula side of each example is unchanged. This changes documentation text only (TSDoc and one `.describe()` string, with the generated reference page regenerated to match). There is no schema, behaviour or export change.

‎content/docs/references/data/validation.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Equivalent ObjectStack rule:
9696
{
9797
type: 'script',
9898
name: 'discount_cannot_exceed_40_percent',
99-
condition: 'discount_percent > 0.40',
99+
condition: 'record.discount_percent > 0.40',
100100
message: 'Discount cannot exceed 40%',
101101
severity: 'error'
102102
}
@@ -239,7 +239,7 @@ const result = ConditionalValidationSchema.parse(data);
239239
| **_packageVersion** | `string` | optional | Owning package version. |
240240
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
241241
| **type** | `'cross_field'` | ✅ | |
242-
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
242+
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date. |
243243
| **fields** | `string[]` | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
244244

245245
### Nested Shape: `ConditionalValidation.then[type='json_schema']`
@@ -360,7 +360,7 @@ const result = ConditionalValidationSchema.parse(data);
360360
| **_packageVersion** | `string` | optional | Owning package version. |
361361
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
362362
| **type** | `'cross_field'` | ✅ | |
363-
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
363+
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date. |
364364
| **fields** | `string[]` | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
365365

366366
### Nested Shape: `ConditionalValidation.otherwise[type='json_schema']`
@@ -413,7 +413,7 @@ const result = ConditionalValidationSchema.parse(data);
413413
| **_packageVersion** | `string` | optional | Owning package version. |
414414
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
415415
| **type** | `'cross_field'` | ✅ | |
416-
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
416+
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date. |
417417
| **fields** | `string[]` | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
418418

419419

@@ -658,7 +658,7 @@ This schema accepts one of the following structures:
658658
| **_packageVersion** | `string` | optional | Owning package version. |
659659
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
660660
| **type** | `'cross_field'` | ✅ | |
661-
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
661+
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date. |
662662
| **fields** | `string[]` | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
663663

664664
---

‎packages/spec/src/data/validation.zod.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ import { MetadataProtectionFields } from '../kernel/metadata-protection.zod';
9797
* {
9898
* type: 'script',
9999
* name: 'discount_cannot_exceed_40_percent',
100-
* condition: 'discount_percent > 0.40',
100+
* condition: 'record.discount_percent > 0.40',
101101
* message: 'Discount cannot exceed 40%',
102102
* severity: 'error'
103103
* }
@@ -237,7 +237,7 @@ export const FormatValidationSchema = lazySchema(() => strictObject({
237237
* {
238238
* type: 'cross_field',
239239
* name: 'close_date_future',
240-
* condition: 'MONTH(close_date) >= MONTH(TODAY()) AND YEAR(close_date) >= YEAR(TODAY())',
240+
* condition: 'date(record.close_date) < addDays(today(), 1 - today().getDate())',
241241
* fields: ['close_date'],
242242
* message: 'Close Date must be in the current or a future month'
243243
* }
@@ -254,7 +254,7 @@ export const FormatValidationSchema = lazySchema(() => strictObject({
254254
* {
255255
* type: 'cross_field',
256256
* name: 'discount_limit',
257-
* condition: 'discount > (amount * 0.40)',
257+
* condition: 'record.discount > (record.amount * 0.40)',
258258
* fields: ['discount', 'amount'],
259259
* message: 'Discount cannot exceed 40% of the amount'
260260
* }
@@ -271,7 +271,7 @@ export const FormatValidationSchema = lazySchema(() => strictObject({
271271
* {
272272
* type: 'cross_field',
273273
* name: 'products_required_for_won',
274-
* condition: 'products = null AND stage = "closed_won"',
274+
* condition: 'isBlank(record.products) && record.stage == "closed_won"',
275275
* fields: ['products', 'stage'],
276276
* message: 'Opportunity must have products to be marked as Closed Won'
277277
* }
@@ -291,7 +291,7 @@ export const CrossFieldValidationSchema = lazySchema(() => strictObject({
291291
}, {
292292
...BASE_VALIDATION_SHAPE,
293293
type: z.literal('cross_field'),
294-
condition: EvaluatedExpressionInputSchema.describe('Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date`'),
294+
condition: EvaluatedExpressionInputSchema.describe('Predicate (CEL) comparing fields. If TRUE, validation fails — the condition describes the violation. e.g. P`record.end_date < record.start_date` refuses an end date before the start date.'),
295295
fields: z.array(z.string()).describe('Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path.'),
296296
}));
297297

0 commit comments

Comments
 (0)