From 4036ac3ca131898fe0693a033436ce1390ecd1c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 17:07:47 +0000 Subject: [PATCH] docs(skills): correct the invariant freeze semantics and name the "required when X" trap The data skill's "Choose by intent" bullet said a transition gate written as an invariant "bricks existing data". Measured against the engine, that is wrong in the direction that matters: a `validations[]` `script` / `cross_field` rule is judged on the merged record on every write with no pre-state exemption, so a row that already violates it is refused on any edit until a repairing write lands. Frozen, not bricked. The bullet also never named the trap the tool choice exists for: "required when X" reads like an invariant and is not one. Both deltas are traded inside the bullet's own length, and the Script Validation section of rules/validation.md gains the tool-choice pointer it carried nowhere: script and cross_field are the invariant tools, requiredWhen and field bounds judge the write, not the stored row. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox --- skills/objectstack-data/SKILL.md | 13 +++++++------ skills/objectstack-data/rules/validation.md | 9 +++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) 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';