diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 182d366fcc..bfe1c3037c 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -282,12 +282,13 @@ export const Invoice = ObjectSchema.create({ - Use `requiredWhen` for conditional requiredness; the ObjectQL validator enforces it on submit. The `conditionalRequired` alias was REMOVED in protocol 17 — emitting it is a parse error. -- **Choose by intent — invariant or transition gate.** A fact that must hold for - *every stored record* is an invariant: express it in `validations[]`. A - condition on a *transition* ("required once the record reaches `paid`") is - `requiredWhen` / field bounds, which let already-stored rows through. A - transition gate written as an invariant bricks existing data; an invariant - written as a transition gate never enforces itself. +- **Choose by intent — invariant or transition gate.** A fact true of *every + stored record* is an invariant: a `validations[]` `script` rule. A row that + violates it is refused on any edit until repaired. A *transition* condition + ("required once the record reaches `paid`") is `requiredWhen` / field bounds, + which judge the write, not the stored row. "Required when X" reads like an + invariant and is not one; an invariant written as a gate never enforces + itself. - For inline `master_detail` grids, predicates are evaluated row-by-row against the child row's `record`, so line-item rules should live on child fields. - For complex predicates, load **objectstack-formula** and emit CEL via diff --git a/skills/objectstack-data/rules/validation.md b/skills/objectstack-data/rules/validation.md index 180e6c0f8a..930d790dd7 100644 --- a/skills/objectstack-data/rules/validation.md +++ b/skills/objectstack-data/rules/validation.md @@ -36,6 +36,15 @@ the **failure** condition — validation **fails** when it evaluates to `true`. ## Script Validation +**Tool choice — this is the invariant tool.** `script` and `cross_field` +are judged against the *merged* record on every write, with no pre-state +exemption, so a row that already violates the rule is refused on any edit until +a repairing write lands (frozen, not bricked). A condition that must hold only +from a transition onward is not this tool: use `requiredWhen` or a field bound +(`min` / `max` / `minLength` / `maxLength`), which judge the write, not the +stored row (see **Choose by intent — invariant or transition gate** in +`SKILL.md`). + ```typescript import { P } from '@objectstack/spec';