You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4f9d0cf
Browse filesBrowse the repository at this point in the historyBrowse files
chore(spec): changeset + regenerate the reference docs for the narrowed predicate
`gen:docs` was the one artifact `check:generated` proved stale; the published
reference now states `source` as required on `Predicate` / `PredicateInput`.
Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
**BREAKING (accept-set narrows)** — a predicate slot now accepts only what the expression engine can actually run. `PredicateSchema` / `PredicateInputSchema` compose the EVALUATED rule instead of the persistence contract, and the `FieldSchema` field-rule triad — `visibleWhen`, `readonlyWhen`, `requiredWhen` — binds them (#17778, ADR-0136 D1).
6
+
7
+
Clause-②: yes (narrowing)
8
+
9
+
**FROM → TO.** Two spellings stop parsing on those three slots, and on any slot composing the `Predicate*` aliases:
10
+
11
+
-`{ dialect: 'cel', ast: … }` with no `source` → **TO**`{ dialect: 'cel', source: "record.status == 'paid'" }`
12
+
-`{ dialect: 'cel', source: ' ' }`, or the bare-string shorthand `' '` → **TO** the same non-blank envelope, or the `P` tagged template the examples use
13
+
14
+
**The one-line fix:** author the predicate's `source`, or REMOVE the key if the field was meant to carry no rule. Removal is behaviour-preserving here — all three slots resolved both refused spellings to a no-op already — so it is a safe default, but it records that the rule never ran. Authoring the `source` is the repair.
15
+
16
+
**Why this is a break worth taking.** The CEL engine evaluates `source` alone, so both spellings parsed, registered, passed `objectstack validate`, and then produced a rule that silently did nothing — by three *different* mechanisms, each measured at its own end: a field-level `visibleWhen` is never evaluated server-side at all; `isReadonlyWhenLocked` returns LOCKED only for an unbound-ROOT fault and logs `change allowed through` for every other one, which is the arm an unevaluable envelope takes, silently waiving the declared lock; `requiredWhen` logs and SKIPS the check. One unauthored predicate therefore made a form show more, lock less and demand less at once, with no state saying the rule had not run.
17
+
18
+
**`ExpressionSchema` / `ExpressionInputSchema` are NOT narrowed** — they remain the persistence contract (`source` OR `ast`), and an `ast` BESIDE a string `source` stays admitted everywhere. Absence is untouched: "no rule" was never a malformed rule.
19
+
20
+
**Declared beside the narrowing, delivered by consumers.** ADR-0136 also records the fault semantics the renderer and submit path are held to: a faulting field-rule predicate refuses the SUBMIT loudly, naming the field and the rule (D2), while visibility stays fail-OPEN at RENDER so a rule that could not run never hides a control and lets the form write `null` over a column the user never saw (D3) — a pair, neither safe alone. `packages/spec` carries no business logic, so those land in the ObjectUI half. The evaluation helper's fallback stays freely specifiable (D5): fault-to-flag and fault-to-throw both exist only because it is a parameter.
21
+
22
+
**Repo census.** Zero field-rule predicates of either refused spelling across `examples/`, `packages/`, `content/` and `skills/` at `03b7b8187`, against two live lit controls (15 files with non-blank tagged-template predicates, 14 with envelope-form ones). Nothing in this repository needed rewriting.
|**trackHistory**|`boolean`| optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
104
104
|**group**|`string`| optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
105
-
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'`|
106
-
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'`|
107
-
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]``script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
105
+
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is shown only when TRUE (else hidden). Needs a non-blank `source`: an evaluated slot is held to what the engine can run (ADR-0136 D1). A fault at RENDER is fail-open (the field shows) and refuses the SUBMIT, naming the field and this rule (D2/D3). e.g. P`record.type == 'invoice'`|
106
+
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is read-only when TRUE. Needs a non-blank `source` (ADR-0136 D1); a fault refuses the submit and names the field and this rule (D2). e.g. P`record.status == 'paid'`|
107
+
| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. Needs a non-blank `source` (ADR-0136 D1); a fault refuses the submit and names the field and this rule (D2). A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]` `script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
108
108
|**conditionalRequired**|`never`| optional |[REMOVED]`conditionalRequired` was removed in @objectstack/spec 17 — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
109
109
|**widget**|`string`| optional | Form widget override — names a registered field component (resolved as `field:<widget>`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". |
110
110
|**hidden**|`boolean`| optional (default: `false`) | Hidden from default UI |
|**trackHistory**|`boolean`| optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
266
266
|**group**|`string`| optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
267
-
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'`|
268
-
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'`|
269
-
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]``script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
267
+
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is shown only when TRUE (else hidden). Needs a non-blank `source`: an evaluated slot is held to what the engine can run (ADR-0136 D1). A fault at RENDER is fail-open (the field shows) and refuses the SUBMIT, naming the field and this rule (D2/D3). e.g. P`record.type == 'invoice'`|
268
+
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is read-only when TRUE. Needs a non-blank `source` (ADR-0136 D1); a fault refuses the submit and names the field and this rule (D2). e.g. P`record.status == 'paid'`|
269
+
| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. Needs a non-blank `source` (ADR-0136 D1); a fault refuses the submit and names the field and this rule (D2). A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]` `script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
270
270
|**conditionalRequired**|`never`| optional |[REMOVED]`conditionalRequired` was removed in @objectstack/spec 17 — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
271
271
|**widget**|`string`| optional | Form widget override — names a registered field component (resolved as `field:<widget>`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". |
272
272
|**hidden**|`boolean`| optional (default: `false`) | Hidden from default UI |
@@ -597,9 +597,9 @@ const result = ApiMethod.parse(data);
|**trackHistory**|`boolean`| optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. |
599
599
|**group**|`string`| optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
600
-
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'`|
601
-
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'`|
602
-
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]``script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
600
+
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is shown only when TRUE (else hidden). Needs a non-blank `source`: an evaluated slot is held to what the engine can run (ADR-0136 D1). A fault at RENDER is fail-open (the field shows) and refuses the SUBMIT, naming the field and this rule (D2/D3). e.g. P`record.type == 'invoice'`|
601
+
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is read-only when TRUE. Needs a non-blank `source` (ADR-0136 D1); a fault refuses the submit and names the field and this rule (D2). e.g. P`record.status == 'paid'`|
602
+
| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. Needs a non-blank `source` (ADR-0136 D1); a fault refuses the submit and names the field and this rule (D2). A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]` `script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
603
603
|**conditionalRequired**|`never`| optional |[REMOVED]`conditionalRequired` was removed in @objectstack/spec 17 — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
604
604
|**widget**|`string`| optional | Form widget override — names a registered field component (resolved as `field:<widget>`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". |
605
605
|**hidden**|`boolean`| optional (default: `false`) | Hidden from default UI |
0 commit comments