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 215e662
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec,lint): a date or datetime field declares its deadline: dueLike and settledWhen (#22503)
Fixes#22227
Clause-②: yes
The protocol half of objectstack-ai/objectui#11815's ruling D
(declared-only overdue). The maintainer's words there:
「每个状态不是可以自己配置颜色吗?done 配置为红色不是元数据的 bug 吗?为什么平台要兜一圈」, then 「同意」 to option
D.
## What
On the `date` and `datetime` field definitions
(`packages/spec/src/data/field.zod.ts`):
- **`dueLike: boolean`** (optional, no default) declares the date a
deadline. Absent means not a deadline, and nothing is inferred from the
field's name.
- **`settledWhen`** (optional, `EvaluatedExpressionInputSchema`, the
same schema as `visibleWhen` / `readonlyWhen` / `requiredWhen`) is a
per-record CEL predicate. While it holds, the deadline is settled.
Declared = enforced (ADR-0049), at two seams:
| Door | What it refuses | Where |
|:--|:--|:--|
| parse (`FieldSchema` superRefine, the `maxLength` / `valueDomain` /
`rows` template) | `dueLike` on any type outside `DEADLINE_FIELD_TYPES`
(`date`, `datetime`), `settledWhen` on any such type, and `settledWhen`
unless `dueLike: true` | `field.zod.ts` |
| authoring CEL gate (`os build`, `os validate`, the object save door) |
a `settledWhen` that does not parse, reads a bare field, names an
undeclared field, or reads a root the field-rule family does not bind |
`packages/lint/src/validate-expressions.ts`, the field-rule pass |
No select option carries a terminal marker. Nothing changes in objectui
here: objectui#11815 is `Blocked-by:` this card and reads the keys after
this spec publishes.
## Mechanism hypotheses, measured
- **H1: is there a legacy spelling to convert?** No. `git grep -n
"dueLike\|settledWhen"` over this tree at `dee7692f0b` finds 3 hits, all
`CHANGELOG.md` prose (`packages/console`, `packages/platform-objects`,
`packages/spec`), and no source. The view schemas refuse the key:
`ListColumnSchema.safeParse({ field: 'due_date', dueLike: true })`
answers `unrecognized_keys` with keys `dueLike`, while the control `{
field: 'due_date' }` parses. So no spec-validated source can spell it,
and **no ADR-0087 entry** is added. The changeset declares no break, and
`check-adr-0087-registration` judges only breaking declarations
(`breakingDeclaration`: "`widening` adds NO signal"). It exits 0.
- **H2: the CEL scope.** The field-rule family binds exactly
`FIELD_RULE_BOUND_ROOTS` = `record`, `previous`, `parent`. `today()` is
a stdlib function in every CEL env, not a scope name, so it applies.
`value` is not a root in `SCOPE_ROOTS`, so it reads as a bare field
reference and is refused. `settledWhen` joins the same loop as
`visibleWhen` and gets exactly that verdict. No new scope name is added.
The pins: the three bound roots are admitted, `current_user` gets the
family's root verdict naming `settledWhen`, `today()` passes, and
`value` is refused. Like `visibleWhen`, the slot is display only. The
server-side gates further down (the `parent` gate, the null guard, the
traversal refusal) stay with `requiredWhen` / `readonlyWhen`.
- **H3: type scoping idiom.** `field.zod.ts` scopes a type-specific key
with a superRefine over a `ReadonlySet` of types
(`BOUNDED_STRING_FIELD_TYPES`, `VALUE_DOMAIN_FIELD_TYPES`,
`MULTILINE_EDITOR_FIELD_TYPES`), not with a discriminated variant.
`DEADLINE_FIELD_TYPES` follows it, so `dueLike` on a `text` field is a
located `custom` issue at `[dueLike]`.
- **H4: the example objects' real status values.** showcase
`showcase_task`: status options include `done`, so `settledWhen:
record.status == 'done'`. app-todo `todo_task`: `completed` (also the
app's own documented overdue definition, `due_date` past AND `status`
not `completed`). crm `crm_activity`: `completed`. All three follow the
card.
## Files beyond the claim's declared surface (each one demanded by a
gate)
The claim fenced the surface to `field.zod.ts` and its tests, the lint
field-rule gate and its tests, generated baselines and forms, the docs
page, the three example objects and one changeset. A new `FieldSchema`
key and a new CEL slot are also counted by these classification
ratchets. Each one is red until the key is classified:
- `packages/spec/liveness/field.json` (`check:liveness` UNCLASSIFIED)
and its generated `state-counts/field.md`. `dueLike` is `live`
cross-repo: objectui `resolveDueLike` reads it, and the detail surfaces
copy it through `ENRICHED_FIELD_METADATA_KEYS`, at main `55e90fd` and at
the `.objectui-sha` pin `f0268ad78`. `settledWhen` is `planned`: no
reader in either repo yet, with no `authorWarn`.
- `packages/qa/dogfood/test/expression-conformance.ledger.ts`: a new
`settledWhen` CEL slot is unclassified otherwise. The new row is
`experimental` and `unevaluated`, and it says NO EVALUATOR FOUND.
- `packages/metadata-core/src/object-schema-fls-references.ts`:
`FIELD_REFERENCE_POSITIONS` is pinned against the live `FieldSchema`
shape. `dueLike` is `keep` and `settledWhen` is `expression`.
- `packages/lint/src/validate-predicate-path-refs.test.ts`: the
shipped-form predicate census moves 85 → 87 for the two new form rows.
-
`packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts`:
generator output plus six hand-authored leaves, in two commits. The
`*.source-hashes.generated.ts` files are byte-identical to base.
`object-lifecycle-panel-echo-decisions.test.ts` moves its
translated-label control 665 → 667.
- Patch round 1, four more, each named by the REWORK:
`packages/drivers/driver-sql/src/builtin-column-collision.ts` (the tenth
ratchet),
`packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts`
(the eleventh),
`packages/lint/scripts/check-doc-formula-expressions.mjs` and
`packages/lint/src/validate-field-consumers.ts` (the field-rule family
lists).
None of them is touched by any of the 12 open PRs (file lists read while
this PR was being built).
## Patch round 1 (HEAD `c73e779f9a`; REWORK `6084352738` on #22227,
contract review FAIL `6084330967`, then PASS `6085939799`)
- Two key-shape ratchets the first head missed, both red in CI at
`a520b4eea5` and fixed: `driver-sql` `FIELD_KEY_STORAGE_CLASS`
classifies `dueLike` and `settledWhen` as `presentation` (`c064f4b335`);
`metadata-protocol` `protocol.meta-types-degenerate-derivation.test.ts`
serves 77 `field` properties, up from 75 (`d0af72c6f8`).
- `settledWhen` joins the field-rule family lists: `FIELD_RULE_SLOTS` /
`FIELD_RULE_TEXT_RE` in `check-doc-formula-expressions.mjs` (three
self-test cases; ablation turns exactly those red), `BEHAVIOUR_SEGMENTS`
in `validate-field-consumers.ts`, and the #6585 user-alias table.
- `origin/main` `2e10c9abe0` merged (`23c192bfad`), clean; no
regeneration owed. PR #22215 (protocol 18) landed after it; this PR adds
no ADR-0087 entry, so no re-sync is owed.
- At `c73e779f9a`: metadata-protocol 223 files / 28,347; driver-sql 218
/ 3,635; lint 130 / 5,958; metadata-core 17 / 387; service-messaging 48
/ 534; platform-objects 68 / 1,078; spec local 631 / 18,829, repo 54 /
915; objectql local 391 / 7,716; cli unit 274 / 4,053; the three example
apps' suites and `validate` green. 118 derived gate families run, 0 not
measured. CI: 35 runs, 33 success, 2 expected skips.
## Tests, round 0 (HEAD `a520b4eea5`, all locked runs through
`os-verify-lock`)
- `@objectstack/spec`, `--project local`: 631 files, 18824 passed, 1
todo. `--project repo`: 54 files, 915 passed. Both ran at `832743b836`;
the one later commit touches only a `platform-objects` test.
- `@objectstack/lint` 130 files / 5956 · `@objectstack/metadata-core` 17
/ 421 · `@objectstack/objectql` (`--project local`) 390 / 7698 ·
`@objectstack/cli` (`--project unit`) 274 / 4037 · example-crm 5 / 45 ·
example-todo 7 / 238 · example-showcase 33 / 408 · dogfood
`expression-conformance.test.ts` 7 / 7. All green at `832743b836`.
- `@objectstack/platform-objects` 68 files / 1078 passed at
`a520b4eea5`.
- `objectstack validate` passes for all three example apps.
- `typecheck` is green for spec (including `check:test-typecheck`, which
caught one error in the new pin before this head), lint, metadata-core,
platform-objects, dogfood and the three example apps, at `a520b4eea5`.
- `@objectstack/cli` integration tier: not run locally, declared to CI.
The diff touches no spawn entry.
**Ablation, three runs, each committed first and mutated through
`scripts/ablation-replace.mjs`.** Each anchor hit once, the blob
changed, and each restore was proven with blob == HEAD and an empty `git
diff HEAD`. The subjects resolve to `src` through relative imports, so
no `dist` leg applies.
| Mutation | Result |
|:--|:--|
| `settledWhen` dropped from the lint field-rule loop | 7 red (parse,
bare reference, unknown field, `current_user`, `value`, object door,
slot list), 386 green |
| the `dueLike !== true` branch → `false` | 4 red (absent or `false`, on
`date` and `datetime`) |
| the `dueLike` type refusal → `false` | 9 red (8 types, plus `dueLike:
false` off-type) |
All three turned red, which was the expected direction.
**Gates:** `dispatch-gates --commands` re-derived on the actual diff
(`--repo objectstack-ai/objectstack`) is unioned with the dispatched
list: 119 commands, all exit 0 at `a520b4eea5`. The re-derivation added
10 families: `check:generated`, `check:engine-double-contract`,
`check:i18n`, `check:i18n-stale-fill`, `check:objectql-double-limit`,
`check:platform-checklist`, `check:quick-reference-counts`,
`check:type-check-coverage`, `check:type-check-debt` and
`check:where-matcher`. `dispatch-gates --ran` reports 114 derived
families, 114 run, 0 NOT-MEASURED, all with a recorded exit code.
## Acceptance notes
- **`crm_activity` has a second finished state.** `cancelled` exists
beside `completed`. Under the ruled predicate a cancelled activity past
due keeps the overdue affordance. `record.status in ['completed',
'cancelled']` is a one-line change if that is wanted; the card's text
was followed. app-todo's `deferred` is in the same position, and the
app's own overdue definition counts only `completed`.
- **`previous` / `parent` on a display predicate.** The family's bound
roots are admitted for `settledWhen` as they are for `visibleWhen`. A
display-row evaluator may have neither bound. That is objectui#11815's
to honour or a later narrowing card's to decide (carrier:
objectui#11815).
- **`dueLike` reaches the cell only on the detail surfaces today.**
objectui's grid copy set does not carry it, and objectui still also
guesses due-likeness from the field name. Both retire with
objectui#11815 (carrier: objectui#11815).
- **`settledWhen` is a 35th evaluated-expression position.**
`packages/spec/src/shared/evaluated-slot-population.test.ts` hand-lists
34 (pinned `toBe(34)`), and `evaluated-slot-union.ts:10` says 34. No
gate is red, and the three-spelling refusal holds by composition.
Carrier: #22228, which adds the next evaluated position on the field
definition and adds both rows (contract review `6085939799` ③).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
A `date` or `datetime` field can declare that it is a deadline, and when that deadline is settled: `dueLike` and `settledWhen`
7
+
8
+
Clause-②: yes
9
+
10
+
-**`dueLike: boolean`** (optional, no default) declares the date a deadline. A renderer may then show relative overdue wording and an overdue colour once the date has passed. Absent means not a deadline: nothing is inferred from the field's name.
11
+
-**`settledWhen`** (optional) is a per-record CEL predicate in the `visibleWhen` / `readonlyWhen` / `requiredWhen` family. While it holds, the deadline is settled and no overdue wording or colour applies to that record, for example `record.status == 'done'`.
12
+
-**The doors that judge them.**`FieldSchema` refuses either key on any type other than `date` / `datetime`, and refuses `settledWhen` unless the field also declares `dueLike: true`. The CEL is judged by `@objectstack/lint`'s field-rule pass, the one `visibleWhen` goes through, at `objectstack build`, `objectstack validate` and the object save: it must parse, read fields as `record.<field>`, name declared fields, and read no root but `record` (plus `previous`, and `parent` on a master-detail line item).
13
+
-**Display only.** Nothing on the write path reads either key. Every metadata that parsed before parses unchanged; a field without the keys behaves exactly as before.
14
+
-**Who reads them.** objectui's date and datetime cells read `dueLike` on the record detail surfaces today. Reading `settledWhen`, and retiring the field-name guess, follows in objectui once this spec version is published.
15
+
-**Where to author them.** The field designer offers both on date and datetime fields, `settledWhen` only once the field is a deadline. The example apps declare them on the showcase and todo `task` and the CRM `activity`.
A date is a **deadline** only when its field says so. `dueLike: true` on a
105
+
`date` or `datetime` field declares one: once the date has passed, a renderer
106
+
may show it as overdue, with relative wording ("Overdue 3d") and an overdue
107
+
colour. Without the key the field is not a deadline, and nothing is inferred
108
+
from its name — a `due_date` that is not declared is just a date.
109
+
110
+
A deadline usually stops mattering when the record is finished, and only the
111
+
object knows what finished means. `settledWhen` says it: a CEL predicate over
112
+
the record, in the same family as `visibleWhen`, and while it is `TRUE` no
113
+
overdue wording or colour applies to that record.
114
+
115
+
```typescript
116
+
import { P } from'@objectstack/spec';
117
+
import { Field } from'@objectstack/spec/data';
118
+
119
+
due_date: Field.date({
120
+
label: 'Due Date',
121
+
dueLike: true,
122
+
settledWhen: P`record.status == 'done'`,
123
+
}),
124
+
```
125
+
126
+
Both keys are judged when the metadata is authored, not when a view renders it:
127
+
128
+
- Either key on any other field type is refused, and so is `settledWhen`
129
+
without `dueLike: true` — a settle condition on a date that is no deadline
130
+
would change nothing.
131
+
-`settledWhen` is checked exactly like `visibleWhen`: it must parse, read
132
+
fields as `record.<field>`, name fields the object declares, and read no root
133
+
but `record` (plus `previous`, and `parent` on a master-detail line item).
134
+
`objectstack validate`, `objectstack build` and saving the object all refuse
135
+
anything else, naming the field.
136
+
137
+
Both are display semantics: nothing on the write path reads them, and a past
138
+
deadline never blocks a save.
139
+
102
140
### Boolean Type
103
141
104
142
```typescript
@@ -352,6 +390,7 @@ For values that must be encrypted at rest (API keys, tokens, DB passwords), use
352
390
|`visibleWhen`|`string \| Expression`| CEL predicate; field is shown only when `TRUE`|
353
391
|`readonlyWhen`|`string \| Expression`| CEL predicate; field is read-only when `TRUE`|
354
392
|`requiredWhen`|`string \| Expression`| CEL predicate; field is required when `TRUE`|
393
+
|`settledWhen`|`string \| Expression`| CEL predicate on a `dueLike` date; the deadline is settled when `TRUE` — see [Deadlines](#deadlines-duelike-and-settledwhen)|
Copy file name to clipboardExpand all lines: content/docs/references/data/field.mdx
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -107,6 +107,8 @@ const result = CurrencyConfigSchema.parse(data);
107
107
|**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'`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every update that writes the field — `objectstack validate` refuses it. Put such a check in a `validations[]``script` rule, whose `condition` is read one hop through a reference. |
108
108
| **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`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every write that reaches it — `objectstack validate` refuses it; put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
109
109
|**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; `--write` applies the ones it can prove, and you apply the rest by hand. |
110
+
|**dueLike**|`boolean`| optional | Deadline semantic (`date` / `datetime` only): TRUE declares this date a deadline, so a renderer may show relative overdue wording ("Overdue 3d") and an overdue colour once it has passed. Absent or FALSE: not a deadline — nothing is inferred from the field's name. Pair with `settledWhen` to say when the deadline is settled for a record. Refused on any other type. Display only: nothing on the write path reads it. |
111
+
|**settledWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the deadline is SETTLED while TRUE: no overdue wording or colour applies to this record, e.g. P`record.status == 'done'`. Evaluated per record like `visibleWhen`, reading the record's own columns as `record`. Requires `dueLike: true` on a `date` / `datetime` field; refused otherwise. Display only: nothing on the write path reads it. |
110
112
|**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". |
111
113
|**hidden**|`boolean`| optional (default: `false`) | Hidden from default UI |
112
114
|**internal**|`boolean`| optional | Never return this field's value on the generic data path — the engine OMITS the key from `find`/`findOne` results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in `?select=`. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on `text` columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a `required` column. |
Copy file name to clipboardExpand all lines: content/docs/references/data/object.mdx
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -272,6 +272,8 @@ const result = ApiMethod.parse(data);
272
272
|**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'`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every update that writes the field — `objectstack validate` refuses it. Put such a check in a `validations[]``script` rule, whose `condition` is read one hop through a reference. |
273
273
| **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`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every write that reaches it — `objectstack validate` refuses it; put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
274
274
|**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; `--write` applies the ones it can prove, and you apply the rest by hand. |
275
+
|**dueLike**|`boolean`| optional | Deadline semantic (`date` / `datetime` only): TRUE declares this date a deadline, so a renderer may show relative overdue wording ("Overdue 3d") and an overdue colour once it has passed. Absent or FALSE: not a deadline — nothing is inferred from the field's name. Pair with `settledWhen` to say when the deadline is settled for a record. Refused on any other type. Display only: nothing on the write path reads it. |
276
+
|**settledWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the deadline is SETTLED while TRUE: no overdue wording or colour applies to this record, e.g. P`record.status == 'done'`. Evaluated per record like `visibleWhen`, reading the record's own columns as `record`. Requires `dueLike: true` on a `date` / `datetime` field; refused otherwise. Display only: nothing on the write path reads it. |
275
277
|**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". |
276
278
|**hidden**|`boolean`| optional (default: `false`) | Hidden from default UI |
277
279
|**internal**|`boolean`| optional | Never return this field's value on the generic data path — the engine OMITS the key from `find`/`findOne` results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in `?select=`. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on `text` columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a `required` column. |
@@ -607,6 +609,8 @@ const result = ApiMethod.parse(data);
607
609
|**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'`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every update that writes the field — `objectstack validate` refuses it. Put such a check in a `validations[]``script` rule, whose `condition` is read one hop through a reference. |
608
610
| **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`. Reads the bound record's OWN columns: the field level never reads a related record, so a read THROUGH a reference field (`record.account.tier`) faults on every row and refuses every write that reaches it — `objectstack validate` refuses it; put such a check in a `validations[]` `script` rule, whose `condition` is read one hop through a reference. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
609
611
|**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; `--write` applies the ones it can prove, and you apply the rest by hand. |
612
+
|**dueLike**|`boolean`| optional | Deadline semantic (`date` / `datetime` only): TRUE declares this date a deadline, so a renderer may show relative overdue wording ("Overdue 3d") and an overdue colour once it has passed. Absent or FALSE: not a deadline — nothing is inferred from the field's name. Pair with `settledWhen` to say when the deadline is settled for a record. Refused on any other type. Display only: nothing on the write path reads it. |
613
+
|**settledWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the deadline is SETTLED while TRUE: no overdue wording or colour applies to this record, e.g. P`record.status == 'done'`. Evaluated per record like `visibleWhen`, reading the record's own columns as `record`. Requires `dueLike: true` on a `date` / `datetime` field; refused otherwise. Display only: nothing on the write path reads it. |
610
614
|**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". |
611
615
|**hidden**|`boolean`| optional (default: `false`) | Hidden from default UI |
612
616
|**internal**|`boolean`| optional | Never return this field's value on the generic data path — the engine OMITS the key from `find`/`findOne` results, the 201 create body and the by-id update body, on the default projection AND when a client names the field in `?select=`. Storage, filtering and indexing are untouched, so a server-side verifier can still match on the column and a purpose-built mint route can still return the value once at creation. The read protection for ADR-0100's third credential channel (auth-subsystem one-way hashes on `text` columns). Omission, not masking: a mask signals 'a value is set', which carries no information on a `required` column. |
0 commit comments