Skip to content

Commit 215e662

Browse 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>
1 parent da989bb commit 215e662

28 files changed

Lines changed: 608 additions & 15 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
---
5+
6+
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`.

‎content/docs/data-modeling/fields.mdx‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,44 @@ start_date: Field.date({ label: 'Start Date', required: true }),
9999
created_at: Field.datetime({ label: 'Created At', readonly: true }),
100100
```
101101

102+
#### Deadlines: `dueLike` and `settledWhen`
103+
104+
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+
102140
### Boolean Type
103141

104142
```typescript
@@ -352,6 +390,7 @@ For values that must be encrypted at rest (API keys, tokens, DB passwords), use
352390
| `visibleWhen` | `string \| Expression` | CEL predicate; field is shown only when `TRUE` |
353391
| `readonlyWhen` | `string \| Expression` | CEL predicate; field is read-only when `TRUE` |
354392
| `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) |
355394
| `multiple` | `boolean` | Allow multiple values (for select, lookup, file) |
356395

357396
```typescript

‎content/docs/references/data/field.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ const result = CurrencyConfigSchema.parse(data);
107107
| **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. |
108108
| **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. |
109109
| **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. |
110112
| **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". |
111113
| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI |
112114
| **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. |

‎content/docs/references/data/object.mdx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ const result = ApiMethod.parse(data);
272272
| **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. |
273273
| **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. |
274274
| **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. |
275277
| **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". |
276278
| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI |
277279
| **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);
607609
| **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. |
608610
| **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. |
609611
| **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. |
610614
| **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". |
611615
| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI |
612616
| **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

Comments
 (0)