Skip to content

Commit e0f17a3

Browse files
fix(spec): rewrite ConditionalValidationSchema TSDoc examples in evaluable CEL (#20026) (#20239)
Fixes #20026 Clause-②: no ## REWORK round 1 — what changed since the last revision Per the contract-review FAIL on this PR (comment 5856311938 on this PR, order of record 5856314403 on #20026), three things were wrong and are fixed in this round: 1. **The `matches()` example did not parse when copied.** The docblock's `"^\d{2}-\d{7}$"` reads fine as raw comment text (comments are not JS-escape-processed), but a reader who pastes that literal into a real `.ts` string literal gets ONE level of JS unescaping the comment text never went through: `\\d` in the comment becomes `\d` at runtime, and cel-js refuses `\d` as an invalid escape sequence (it only accepts `\\ ? " ' \` a b f n r t v`, `\u`, `\x`, octal). Rewritten to the escape-free character class `"^[0-9]{2}-[0-9]{7}$"`, which needs no escaping at any copy depth. **The previous revision's transcript row for this predicate, and its "20/20 …" claim, were not measured on the copied literal** — see the corrected measurement methodology below. 2. **Two changeset sentences were false at the reviewed head.** "`@marcbachmann/cel-js` parses `=` as the start of an assignment" → cel-js has no assignment operator; it rejects a lone `=` as an unexpected character, and CEL equality is `==`. The "both branches evaluate as expected for all 19" claim and the `matches` table row are now true, after item 1. 3. **Ten more `validation.test.ts` fixtures were left behind under the already-widened scope.** Each one's surrounding test name and message text verbatim-copy a Use Case 4/5/6/7 docblock predicate the same way the original seven did, so the PR's own stated criterion for moving a fixture applies to them too: `country = "US"` / `state = "CA"` / `tax_id = null` (`nested_validation`, Use Case 5 — `tax_id` now the full escape-free `record.tax_id == null || !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")`), `is_taxable = true` / `tax_code = null` (`tax_validation`, Use Case 6 — `tax_code` now the full `record.tax_code == null || record.tax_code == ''`), `region = "EU"` / `gdpr_consent_given = false` / `tos_accepted = false` (`regional_validation`, Use Case 4), `user_role = "manager"` / `approval_amount > 50000` (`role_based_validation`, Use Case 7). Nothing else changed: the other 18 predicates, the Salesforce-formula side of the comparison, and the `validation.zod.ts`-TSDoc-only / `patch` / `Clause-②: no` shape all passed review and are untouched this round. ## What changed overall (both rounds) Every example predicate in the `ConditionalValidationSchema` TSDoc docblock (`packages/spec/src/data/validation.zod.ts`, ~lines 362-537) is rewritten in the CEL the evaluator actually accepts: - 17 of 19 used `=` for comparison, or `AND`/`NOT`/`REGEX(...)` word-form operators — cel-js rejects a lone `=` as an unexpected character (CEL equality is `==`), and CEL has no `AND`/`OR`/`NOT` keyword form. - 2 (`order_total > 10000`, `approval_amount > 50000`) named a bare field with no `record.` root — an unknown-variable type error. - `REGEX(tax_id, "…")` is rewritten to the evaluator's registered stdlib function `matches(record.tax_id, "…")` (`packages/formula/src/stdlib.ts:361`), using the escape-free character class `[0-9]` rather than `\d` for the reason in item 1 above. - The **Salesforce Approach** half of the "Salesforce Pattern Comparison" block (`IF(ISPICKVAL(...), AND(...), FALSE)`) is deliberately **untouched** — it documents Salesforce's own formula syntax, not CEL. `packages/spec/src/data/validation.test.ts`'s `ConditionalValidationSchema` fixtures whose surrounding test name/message text verbatim-copy a docblock predicate move with it (parse-only — `ValidationRuleSchema.parse(...).not.toThrow()`, no CEL evaluation, so no behaviour changes): the six exact copies of Use Cases 1-3's original seven strings; `manager_approval = null` → `record.manager_approval == null` (`order_value_validation`, Use Case 3, one field-name spelling apart); and, from this round, the ten listed above. **17 fixture strings moved in total.** No schema, behaviour, or public export changes — TSDoc text only, in the same two files the strings already lived in, plus the changeset. ## Evaluation transcript — corrected methodology (`ExpressionEngine.evaluate`, `@objectstack/formula`) **The previous transcript was measured on hand-typed strings that were logically equivalent to the CEL source but never simulated what a reader actually gets by pasting the TSDoc text into real code.** This transcript is measured differently: each `when:`/`condition:` literal is extracted from `validation.zod.ts` byte for byte (comment prefix stripped, quote characters kept), then handed to Node's own parser (`eval`) to apply the exact JS string-literal unescaping a reader's paste would trigger — never hand-retyped. That runtime string is what gets passed to `ExpressionEngine.evaluate`. | # | key | raw docblock literal (as written) | runtime string after JS unescape | true-record | true result | false-record | false result | |--:|:--|:--|:--|:--|:--|:--|:--| | 1 | when | `"record.account_type == 'enterprise'"` | `record.account_type == 'enterprise'` | `{"account_type":"enterprise"}` | `{"ok":true,"value":true}` OK | `{"account_type":"smb"}` | `{"ok":true,"value":false}` OK | | 2 | condition | `'record.approval_status == null'` | `record.approval_status == null` | `{"approval_status":null}` | `{"ok":true,"value":true}` OK | `{"approval_status":"approved"}` | `{"ok":true,"value":false}` OK | | 3 | when | `'record.requires_shipping == true'` | `record.requires_shipping == true` | `{"requires_shipping":true}` | `{"ok":true,"value":true}` OK | `{"requires_shipping":false}` | `{"ok":true,"value":false}` OK | | 4 | condition | `"record.shipping_address == null \|\| record.shipping_address == ''"` | `record.shipping_address == null \|\| record.shipping_address == ''` | `{"shipping_address":null}` | `{"ok":true,"value":true}` OK | `{"shipping_address":"123 Main St"}` | `{"ok":true,"value":false}` OK | | 5 | when | `'record.order_total > 10000'` | `record.order_total > 10000` | `{"order_total":15000}` | `{"ok":true,"value":true}` OK | `{"order_total":5000}` | `{"ok":true,"value":false}` OK | | 6 | condition | `'record.manager_approval_id == null'` | `record.manager_approval_id == null` | `{"manager_approval_id":null}` | `{"ok":true,"value":true}` OK | `{"manager_approval_id":"user_123"}` | `{"ok":true,"value":false}` OK | | 7 | condition | `'record.payment_method == null'` | `record.payment_method == null` | `{"payment_method":null}` | `{"ok":true,"value":true}` OK | `{"payment_method":"credit_card"}` | `{"ok":true,"value":false}` OK | | 8 | when | `"record.region == 'EU'"` | `record.region == 'EU'` | `{"region":"EU"}` | `{"ok":true,"value":true}` OK | `{"region":"US"}` | `{"ok":true,"value":false}` OK | | 9 | condition | `'record.gdpr_consent_given == false'` | `record.gdpr_consent_given == false` | `{"gdpr_consent_given":false}` | `{"ok":true,"value":true}` OK | `{"gdpr_consent_given":true}` | `{"ok":true,"value":false}` OK | | 10 | condition | `'record.tos_accepted == false'` | `record.tos_accepted == false` | `{"tos_accepted":false}` | `{"ok":true,"value":true}` OK | `{"tos_accepted":true}` | `{"ok":true,"value":false}` OK | | 11 | when | `"record.country == 'US'"` | `record.country == 'US'` | `{"country":"US"}` | `{"ok":true,"value":true}` OK | `{"country":"CA"}` | `{"ok":true,"value":false}` OK | | 12 | when | `"record.state == 'CA'"` | `record.state == 'CA'` | `{"state":"CA"}` | `{"ok":true,"value":true}` OK | `{"state":"NY"}` | `{"ok":true,"value":false}` OK | | 13 | condition | `'record.tax_id == null \|\| !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")'` | `record.tax_id == null \|\| !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")` | `{"tax_id":null}` | `{"ok":true,"value":true}` OK | `{"tax_id":"12-3456789"}` | `{"ok":true,"value":false}` OK | | 14 | when | `'record.is_taxable == true'` | `record.is_taxable == true` | `{"is_taxable":true}` | `{"ok":true,"value":true}` OK | `{"is_taxable":false}` | `{"ok":true,"value":false}` OK | | 15 | condition | `"record.tax_code == null \|\| record.tax_code == ''"` | `record.tax_code == null \|\| record.tax_code == ''` | `{"tax_code":null}` | `{"ok":true,"value":true}` OK | `{"tax_code":"VAT-1"}` | `{"ok":true,"value":false}` OK | | 16 | when | `"record.user_role == 'manager'"` | `record.user_role == 'manager'` | `{"user_role":"manager"}` | `{"ok":true,"value":true}` OK | `{"user_role":"rep"}` | `{"ok":true,"value":false}` OK | | 17 | condition | `'record.approval_amount > 50000'` | `record.approval_amount > 50000` | `{"approval_amount":75000}` | `{"ok":true,"value":true}` OK | `{"approval_amount":10000}` | `{"ok":true,"value":false}` OK | | 18 | when | `"record.type == 'enterprise'"` | `record.type == 'enterprise'` | `{"type":"enterprise"}` | `{"ok":true,"value":true}` OK | `{"type":"smb"}` | `{"ok":true,"value":false}` OK | | 19 | condition | `'record.amount > 100000 && record.approval == null'` | `record.amount > 100000 && record.approval == null` | `{"amount":150000,"approval":null}` | `{"ok":true,"value":true}` OK | `{"amount":150000,"approval":"user_1"}` | `{"ok":true,"value":false}` OK | **All 19 predicates, extracted from the source and evaluated as the runtime string a copy-paste produces: true-branch and false-branch both evaluate as expected.** ### The `matches()` site specifically — old (broken) literal vs. new (fixed) literal, same simulation ``` old docblock literal (as it was before this round): 'record.tax_id == null || !matches(record.tax_id, "^\\d{2}-\\d{7}$")' -> runtime string after JS unescape: "record.tax_id == null || !matches(record.tax_id, \"^\\d{2}-\\d{7}$\")" -> ExpressionEngine.evaluate: {"ok":false,"error":{"kind":"parse","message":"Invalid escape sequence: \\d ..."}} new docblock literal (this round): 'record.tax_id == null || !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")' -> runtime string after JS unescape: "record.tax_id == null || !matches(record.tax_id, \"^[0-9]{2}-[0-9]{7}$\")" -> ExpressionEngine.evaluate (true record {tax_id:null}): {"ok":true,"value":true} -> ExpressionEngine.evaluate (false record {tax_id:'12-3456789'}): {"ok":true,"value":false} -> ExpressionEngine.evaluate (wrong-shape record {tax_id:'1234'}): {"ok":true,"value":true} -> ExpressionEngine.evaluate (wrong-shape {tax_id:'not-a-tax-id'}): {"ok":true,"value":true} ``` The old literal reproduces, exactly, the `Invalid escape sequence: \d` failure the review measured; the new literal evaluates correctly on all four probes. ## dist confirmation (H2) Rebuilt `@objectstack/spec` after this round's edit. `grep -c` on `packages/spec/dist/object.zod-*.d.ts`: every one of the 19 new strings present exactly once; every one of the 19 original broken strings absent (checked both `\d` escape variants of the `matches()` predicate — `\\d` and `\d` — zero occurrences of either). ## Local verification - `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/formula build` — both build clean. - `pnpm --filter @objectstack/spec check:generated` — 15/15 generated artifacts up to date. - `pnpm --filter @objectstack/spec exec vitest run src/data/validation.test.ts` — 72/72 tests pass. - `pnpm --filter @objectstack/spec run typecheck` — clean; the 255 pre-existing debt errors are the shrink-only ledger, untouched by this diff. - The three changeset gates (`check-empty-changeset`, `check-adr-0087-registration`, `check-changeset-no-major`) — green against the corrected changeset. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list`: **82 derived families, 79 run (all green), 3 NOT-MEASURED** (`check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt` — each refuses its own prerequisite without the full workspace build closure, out of this card's local scope, left to CI's Build Core lane). 0 unrun. ## Acceptance notes - `content/docs/protocol/objectql/index.mdx:186` (`condition: "account_type = 'Enterprise' AND contract_value = null"`) carries the same non-CEL spelling in an unrelated docs page; already recorded, unresolved, in `docs/audits/2026-06-handwritten-docs-accuracy-followups.md`. Confirmed still out of scope by this round's review; left untouched. --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5983d11 commit e0f17a3

3 files changed

Lines changed: 75 additions & 40 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Every example predicate in the `ConditionalValidationSchema` TSDoc docblock (all seven Use Cases, plus the ObjectStack side of the "Salesforce Pattern Comparison" block, above the schema in `packages/spec/src/data/validation.zod.ts`) is rewritten in the CEL the evaluator actually accepts, so an author or agent who copies a documented example gets a rule that evaluates instead of one that refuses every write it guards (#20026).
6+
7+
Clause-②: no
8+
9+
17 of the 19 predicates used `=` for comparison (or `AND`/`NOT`/`REGEX(...)` word-form operators) — a CEL parse error, since `@marcbachmann/cel-js` rejects a lone `=` as an unexpected character; CEL equality is `==`, and CEL has no `AND`/`OR`/`NOT` keyword form — and the other 2 (`order_total > 10000`, `approval_amount > 50000`) named a bare field with no `record.` root, an unknown-variable type error. `REGEX(tax_id, "…")` is rewritten to the evaluator's registered `matches(record.tax_id, "…")` stdlib function — the CEL form it accepts, not a literal transliteration — with the escape-free character class `[0-9]` rather than `\d`: a `\d` inside the CEL string literal parses fine as written in this TSDoc comment (comments are not JS-escape-processed), but a reader who pastes the same text into a real `.ts` string literal gets ONE level of JS unescaping the comment never applied, so `\\d` in the comment becomes `\d` at runtime and cel-js refuses it (`Invalid escape sequence: \d` — this was this round's own rework: a prior revision of this changeset claimed a passing result for that predicate without measuring it on the copied literal). The Salesforce-formula half of the comparison (`IF(ISPICKVAL(...), AND(...), FALSE)`) is deliberately untouched: it documents Salesforce's own syntax, not CEL.
10+
11+
Measured through `ExpressionEngine.evaluate` (`@objectstack/formula`), taking each predicate as the RUNTIME STRING a reader gets by pasting the docblock's literal into real `.ts` source — the literal is extracted from the source file byte for byte and handed to Node's own parser to unescape, never hand-retyped — against a record that makes each rewritten predicate true and one that makes it false; both branches evaluate as expected for all 19 (plus one extra disjunct check on the regex branch) — transcript in the PR body. The strings ship in the published `dist/object.zod-*.d.ts`, confirmed before and after this change with a lit/dark control: every rewritten string is present exactly once and every original broken string is absent.
12+
13+
| old (fails) | new (evaluates) |
14+
|:--|:--|
15+
| `account_type = "enterprise"` | `record.account_type == 'enterprise'` |
16+
| `approval_status = null` | `record.approval_status == null` |
17+
| `requires_shipping = true` | `record.requires_shipping == true` |
18+
| `shipping_address = null OR shipping_address = ""` | `record.shipping_address == null \|\| record.shipping_address == ''` |
19+
| `order_total > 10000` | `record.order_total > 10000` |
20+
| `manager_approval_id = null` | `record.manager_approval_id == null` |
21+
| `payment_method = null` | `record.payment_method == null` |
22+
| `region = "EU"` | `record.region == 'EU'` |
23+
| `gdpr_consent_given = false` | `record.gdpr_consent_given == false` |
24+
| `tos_accepted = false` | `record.tos_accepted == false` |
25+
| `country = "US"` | `record.country == 'US'` |
26+
| `state = "CA"` | `record.state == 'CA'` |
27+
| `tax_id = null OR NOT(REGEX(tax_id, "^\d{2}-\d{7}$"))` | `record.tax_id == null \|\| !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")` |
28+
| `is_taxable = true` | `record.is_taxable == true` |
29+
| `tax_code = null OR tax_code = ""` | `record.tax_code == null \|\| record.tax_code == ''` |
30+
| `user_role = "manager"` | `record.user_role == 'manager'` |
31+
| `approval_amount > 50000` | `record.approval_amount > 50000` |
32+
| `type = "enterprise"` (Salesforce comparison) | `record.type == 'enterprise'` |
33+
| `amount > 100000 AND approval = null` (Salesforce comparison) | `record.amount > 100000 && record.approval == null` |
34+
35+
`packages/spec/src/data/validation.test.ts`'s `ConditionalValidationSchema` fixtures move with the docblock (parse-only — `ValidationRuleSchema.parse(...).not.toThrow()`, no CEL evaluation, so no behaviour change): the six exact copies of Use Cases 1-3's original seven strings; `manager_approval = null` (the `order_value_validation` fixture whose message text and structure mirror Use Case 3's manager-approval example one field-name spelling apart), now `record.manager_approval == null`; and, under this round's rework, ten more fixtures whose surrounding test name and message text are verbatim copies of Use Cases 4, 5, 6 and 7's docblock text — `is_taxable = true` / `tax_code = null` (`tax_validation`, Use Case 6, the `tax_code` fixture now the full `record.tax_code == null || record.tax_code == ''`), `region = "EU"` / `gdpr_consent_given = false` / `tos_accepted = false` (`regional_validation`, Use Case 4), `user_role = "manager"` / `approval_amount > 50000` (`role_based_validation`, Use Case 7), and `country = "US"` / `state = "CA"` / `tax_id = null` (`nested_validation`, Use Case 5 — the `tax_id` fixture now the full `record.tax_id == null || !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")`, escape-free for the same copy-paste reason as the docblock). 17 fixture strings moved in total. No schema, behaviour, or public export changes — TSDoc text only, in the same two files the strings already lived in.

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

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -511,12 +511,12 @@ describe('ValidationRuleSchema (Discriminated Union)', () => {
511511
type: 'conditional' as const,
512512
name: 'enterprise_validation',
513513
message: 'Enterprise accounts require approval',
514-
when: 'account_type = "enterprise"',
514+
when: "record.account_type == 'enterprise'",
515515
then: {
516516
type: 'script' as const,
517517
name: 'require_approval',
518518
message: 'Approval required for enterprise accounts',
519-
condition: 'approval_status = null',
519+
condition: 'record.approval_status == null',
520520
},
521521
};
522522

@@ -551,17 +551,17 @@ describe('ValidationRuleSchema (Discriminated Union)', () => {
551551
type: 'conditional' as const,
552552
name: 'nested_validation',
553553
message: 'Complex conditional validation',
554-
when: 'country = "US"',
554+
when: "record.country == 'US'",
555555
then: {
556556
type: 'conditional' as const,
557557
name: 'state_validation',
558558
message: 'State-specific validation',
559-
when: 'state = "CA"',
559+
when: "record.state == 'CA'",
560560
then: {
561561
type: 'script' as const,
562562
name: 'ca_tax',
563563
message: 'California requires tax ID',
564-
condition: 'tax_id = null',
564+
condition: 'record.tax_id == null || !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")',
565565
},
566566
},
567567
};
@@ -592,12 +592,12 @@ describe('ValidationRuleSchema (Discriminated Union)', () => {
592592
type: 'conditional' as const,
593593
name: 'shipping_validation',
594594
message: 'Shipping validation',
595-
when: 'requires_shipping = true',
595+
when: 'record.requires_shipping == true',
596596
then: {
597597
type: 'script' as const,
598598
name: 'shipping_address_required',
599599
message: 'Shipping address is required',
600-
condition: 'shipping_address = null OR shipping_address = ""',
600+
condition: "record.shipping_address == null || record.shipping_address == ''",
601601
},
602602
};
603603

@@ -609,18 +609,18 @@ describe('ValidationRuleSchema (Discriminated Union)', () => {
609609
type: 'conditional' as const,
610610
name: 'order_value_validation',
611611
message: 'Order value validation',
612-
when: 'order_total > 10000',
612+
when: 'record.order_total > 10000',
613613
then: {
614614
type: 'script' as const,
615615
name: 'high_value_approval',
616616
message: 'Orders over $10,000 require manager approval',
617-
condition: 'manager_approval = null',
617+
condition: 'record.manager_approval == null',
618618
},
619619
otherwise: {
620620
type: 'script' as const,
621621
name: 'standard_validation',
622622
message: 'Payment method required',
623-
condition: 'payment_method = null',
623+
condition: 'record.payment_method == null',
624624
},
625625
};
626626

@@ -632,12 +632,12 @@ describe('ValidationRuleSchema (Discriminated Union)', () => {
632632
type: 'conditional' as const,
633633
name: 'tax_validation',
634634
message: 'Tax validation',
635-
when: 'is_taxable = true',
635+
when: 'record.is_taxable == true',
636636
then: {
637637
type: 'script' as const,
638638
name: 'tax_code_required',
639639
message: 'Tax code is required for taxable items',
640-
condition: 'tax_code = null',
640+
condition: "record.tax_code == null || record.tax_code == ''",
641641
},
642642
};
643643

@@ -701,18 +701,18 @@ describe('ValidationRuleSchema (Discriminated Union)', () => {
701701
type: 'conditional' as const,
702702
name: 'regional_validation',
703703
message: 'Regional compliance validation',
704-
when: 'region = "EU"',
704+
when: "record.region == 'EU'",
705705
then: {
706706
type: 'script' as const,
707707
name: 'gdpr_consent',
708708
message: 'GDPR consent required for EU customers',
709-
condition: 'gdpr_consent_given = false',
709+
condition: 'record.gdpr_consent_given == false',
710710
},
711711
otherwise: {
712712
type: 'script' as const,
713713
name: 'tos_acceptance',
714714
message: 'Terms of Service acceptance required',
715-
condition: 'tos_accepted = false',
715+
condition: 'record.tos_accepted == false',
716716
},
717717
};
718718

@@ -724,12 +724,12 @@ describe('ValidationRuleSchema (Discriminated Union)', () => {
724724
type: 'conditional' as const,
725725
name: 'role_based_validation',
726726
message: 'Role-based validation',
727-
when: 'user_role = "manager"',
727+
when: "record.user_role == 'manager'",
728728
then: {
729729
type: 'script' as const,
730730
name: 'manager_approval_limit',
731731
message: 'Managers can approve up to $50,000',
732-
condition: 'approval_amount > 50000',
732+
condition: 'record.approval_amount > 50000',
733733
},
734734
};
735735

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

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -375,53 +375,53 @@ export const ValidationRuleSchema: z.ZodType<BaseValidationRuleShape, BaseValida
375375
* {
376376
* type: 'conditional',
377377
* name: 'enterprise_approval_required',
378-
* when: 'account_type = "enterprise"',
378+
* when: "record.account_type == 'enterprise'",
379379
* message: 'Enterprise validation',
380380
* then: {
381381
* type: 'script',
382382
* name: 'require_approval',
383383
* message: 'Enterprise accounts require manager approval',
384-
* condition: 'approval_status = null'
384+
* condition: 'record.approval_status == null'
385385
* }
386386
* }
387387
* ```
388-
*
388+
*
389389
* ### 2. Conditional Field Requirements
390390
* Require certain fields only when specific conditions are met.
391391
* ```typescript
392392
* {
393393
* type: 'conditional',
394394
* name: 'shipping_address_when_required',
395-
* when: 'requires_shipping = true',
395+
* when: 'record.requires_shipping == true',
396396
* message: 'Shipping validation',
397397
* then: {
398398
* type: 'script',
399399
* name: 'shipping_address_required',
400400
* message: 'Shipping address is required for physical products',
401-
* condition: 'shipping_address = null OR shipping_address = ""'
401+
* condition: "record.shipping_address == null || record.shipping_address == ''"
402402
* }
403403
* }
404404
* ```
405-
*
405+
*
406406
* ### 3. Amount-Based Validation
407407
* Apply different rules based on transaction amount.
408408
* ```typescript
409409
* {
410410
* type: 'conditional',
411411
* name: 'high_value_approval',
412-
* when: 'order_total > 10000',
412+
* when: 'record.order_total > 10000',
413413
* message: 'High value order validation',
414414
* then: {
415415
* type: 'script',
416416
* name: 'manager_approval_required',
417417
* message: 'Orders over $10,000 require manager approval',
418-
* condition: 'manager_approval_id = null'
418+
* condition: 'record.manager_approval_id == null'
419419
* },
420420
* otherwise: {
421421
* type: 'script',
422422
* name: 'standard_validation',
423423
* message: 'Payment method is required',
424-
* condition: 'payment_method = null'
424+
* condition: 'record.payment_method == null'
425425
* }
426426
* }
427427
* ```
@@ -432,19 +432,19 @@ export const ValidationRuleSchema: z.ZodType<BaseValidationRuleShape, BaseValida
432432
* {
433433
* type: 'conditional',
434434
* name: 'regional_compliance',
435-
* when: 'region = "EU"',
435+
* when: "record.region == 'EU'",
436436
* message: 'EU compliance validation',
437437
* then: {
438438
* type: 'script',
439439
* name: 'gdpr_consent',
440440
* message: 'GDPR consent is required for EU customers',
441-
* condition: 'gdpr_consent_given = false'
441+
* condition: 'record.gdpr_consent_given == false'
442442
* },
443443
* otherwise: {
444444
* type: 'script',
445445
* name: 'tos_acceptance',
446446
* message: 'Terms of Service acceptance required',
447-
* condition: 'tos_accepted = false'
447+
* condition: 'record.tos_accepted == false'
448448
* }
449449
* }
450450
* ```
@@ -455,53 +455,53 @@ export const ValidationRuleSchema: z.ZodType<BaseValidationRuleShape, BaseValida
455455
* {
456456
* type: 'conditional',
457457
* name: 'country_state_validation',
458-
* when: 'country = "US"',
458+
* when: "record.country == 'US'",
459459
* message: 'US-specific validation',
460460
* then: {
461461
* type: 'conditional',
462462
* name: 'california_validation',
463-
* when: 'state = "CA"',
463+
* when: "record.state == 'CA'",
464464
* message: 'California-specific validation',
465465
* then: {
466466
* type: 'script',
467467
* name: 'ca_tax_id_required',
468468
* message: 'California requires a valid tax ID',
469-
* condition: 'tax_id = null OR NOT(REGEX(tax_id, "^\\d{2}-\\d{7}$"))'
469+
* condition: 'record.tax_id == null || !matches(record.tax_id, "^[0-9]{2}-[0-9]{7}$")'
470470
* }
471471
* }
472472
* }
473473
* ```
474-
*
474+
*
475475
* ### 6. Tax Validation for Taxable Items
476476
* Only validate tax fields when the item is taxable.
477477
* ```typescript
478478
* {
479479
* type: 'conditional',
480480
* name: 'tax_field_validation',
481-
* when: 'is_taxable = true',
481+
* when: 'record.is_taxable == true',
482482
* message: 'Tax validation',
483483
* then: {
484484
* type: 'script',
485485
* name: 'tax_code_required',
486486
* message: 'Tax code is required for taxable items',
487-
* condition: 'tax_code = null OR tax_code = ""'
487+
* condition: "record.tax_code == null || record.tax_code == ''"
488488
* }
489489
* }
490490
* ```
491-
*
491+
*
492492
* ### 7. Role-Based Validation
493493
* Apply validation based on user role.
494494
* ```typescript
495495
* {
496496
* type: 'conditional',
497497
* name: 'role_based_approval_limit',
498-
* when: 'user_role = "manager"',
498+
* when: "record.user_role == 'manager'",
499499
* message: 'Manager approval limits',
500500
* then: {
501501
* type: 'script',
502502
* name: 'manager_limit',
503503
* message: 'Managers can approve up to $50,000',
504-
* condition: 'approval_amount > 50000'
504+
* condition: 'record.approval_amount > 50000'
505505
* }
506506
* }
507507
* ```
@@ -525,11 +525,11 @@ export const ValidationRuleSchema: z.ZodType<BaseValidationRuleShape, BaseValida
525525
* {
526526
* type: 'conditional',
527527
* name: 'enterprise_high_value',
528-
* when: 'type = "enterprise"',
528+
* when: "record.type == 'enterprise'",
529529
* then: {
530530
* type: 'cross_field',
531531
* name: 'amount_approval',
532-
* condition: 'amount > 100000 AND approval = null',
532+
* condition: 'record.amount > 100000 && record.approval == null',
533533
* fields: ['amount', 'approval']
534534
* }
535535
* }

0 commit comments

Comments
 (0)