Skip to content

Commit 3d886ee

Browse files
feat(spec,lint): a field declares conditionalFormatting cell rules over value and record, in the list view's own rule element (#22546)
Fixes #22228 Clause-②: yes (widening) The maintainer-directed field-level formatting block from the decision discussion on objectstack-ai/objectui#11815 (the director seat's proposal, answered 「同意」). Spec and authoring gate only; objectui's cell renderer is its own card after the spec publishes. ## What - **`FieldSchema.conditionalFormatting`** (optional): an ordered list of `{ condition, style }` rules. The first rule whose CEL `condition` holds applies its CSS `style` map to that field's cell, wherever the field renders. A list view's row rules still style the row, and both may apply. - **One rule element, no second copy.** The `{ condition, style }` element was declared inline in `ListViewSchema`. A field definition cannot import it from there (`ui/view.zod.ts` imports `data/field.zod.ts`, so the reverse import is a cycle and a data-on-UI dependency). It moved to `packages/spec/src/shared/conditional-formatting.zod.ts` as `ConditionalFormattingRuleSchema`, and both `ListViewSchema.conditionalFormatting` and `FieldSchema.conditionalFormatting` mount it. The `object-grid` / `object-kanban` blocks still reuse the list view member by identity, so they mount it too. Not exported from the `shared` barrel: no public export is added. - **The scope is `value` and `record`, nothing else.** `@objectstack/lint`'s field walk (`validate-expressions.ts`) judges each `conditionalFormatting[i].condition` at `os build`, `os validate` and the object save door: it must parse, read fields as `record.FIELD` (never bare), name declared fields, and read no root but `value` and `record`. `value` reaches the shared validator through `ExprSchemaHint.roots` for this one call; `SCOPE_ROOTS` is not widened. A new root verdict, `fieldFormattingRootIssue`, refuses `previous`, `parent`, the user roots and every other judged root, in its own words (the field-rule family's message names `previous` / `parent` as bound, and here they are not). - **No new style vocabulary.** The `style` map is the row block's, by the identity above. ## Zone 2 mechanism assumptions, measured 1. **Where the rule item schema lives on `ee8751d41e`:** inline in `ListViewSchema` (`ui/view.zod.ts`, a `strictObject` inside `z.array`), with `ListViewSchema.shape.conditionalFormatting` the single definition the grid and kanban blocks reuse by identity. `data/field.zod.ts` cannot import `ui/view.zod.ts` (view imports field). So: the second form the dispatch allowed, a rule schema both members share. Pinned by identity in `field-conditional-formatting.test.ts` (`FieldSchema...element` and `ListViewSchema...element` are both `toBe(ConditionalFormattingRuleSchema)`). 2. **The precedent `215e66204a`** was read. Its field-rule family binds `FIELD_RULE_BOUND_ROOTS` = `record`, `previous`, `parent` and refuses `value` as a bare reference, so this slot could not join that loop: it is a sibling pass beside it, with its own bound set (`FIELD_FORMATTING_BOUND_ROOTS` = `value`, `record`) and the family's membership-test shape. 3. **ADR-0087 entry: none owed, none added.** On `ee8751d41e` `FieldSchema` is a `strictObject` (`field.zod.ts:1053`) and `authorable-surface/data.json` has no `data/Field:conditionalFormatting` row, so the key was refused before and no stored source can carry a legacy spelling. `git grep conditionalFormatting ee8751d -- examples` hits only the showcase field-zoo LIST VIEW. `node scripts/check-adr-0087-registration.mjs --base origin/main`: "this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)", exit 0. 4. **PR #22509 landed** (`c76edeb8c6`) and was merged in (`5b81e00352`; one hand conflict in the `check` closure, both parameters stacked: `servedRowSite` then `extraRoots`). The new call site passes no `servedRowSite`, so it is fields-only by construction. The `SERVED_ROW_SITES` docblock names it beside a field's `visibleWhen`. ## Liveness ledger `packages/spec/liveness/field.json` row `conditionalFormatting`: **`planned`**, drilled (`condition`, `style`) because `check:liveness` refuses an undeclared container verdict. The in-repo reader is the authoring gate, which validates and does not make the key do anything, so the ledger's `live` ("has a runtime consumer") does not apply. Cross-repo, measured: objectui main `2a48bd4` and the `.objectui-sha` pin `f0268ad78` read `conditionalFormatting` only off list views, the grid and kanban blocks and reports (control: `plugin-grid/src/ObjectGrid.tsx` carries 8 hits at the pin); no field-level read. No `authorWarn` (the `settledWhen` precedent). The flip condition is written into the row. ## Files beyond the claim's declared surface (each one demanded by a gate or by the shared element) - `packages/spec/src/shared/conditional-formatting.zod.ts` and `packages/spec/src/ui/view.zod.ts`: the shared element and the list view mounting it. - Re-keyed declaring position `ui/view.zod.ts:ListViewShapeSchema.condition` to `shared/conditional-formatting.zod.ts:ConditionalFormattingRuleSchema.condition`: `packages/qa/dogfood/test/expression-conformance.ledger.ts` (still the `cel-ui` row, with a note that the position now has two mounts and the field mount has no evaluator yet) and `packages/spec/src/shared/evaluated-slot-population.test.ts` (count stays 34). - `packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`: a root `omit` row for `field.conditionalFormatting`, the `page.print` "planned, not yet rendered" precedent, instead of a form control nothing would paint. - `packages/metadata-core/src/object-schema-fls-references.ts` (+ test): `conditionalFormatting: ruleEntries`, so a rule whose condition reads a denied field is dropped whole (a bare `style` would read as always-on) and an emptied list is deleted. - `packages/drivers/driver-sql/src/builtin-column-collision.ts`: `presentation`. - `packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts`: `field` 77 to 78. - `packages/spec/llms.txt`: `check:llms-txt` counts (shared 15 to 16, total 202 to 203). - Generated: `authorable-surface/data.json`, `liveness/state-counts/field.md`, `content/docs/references/**`, the strictness-ledger counts, and three `skills/*/references/_index.md` lines (generator-owned: `check-governed-merges.mjs --test` reports "0 of 28 path(s) hit the register after 3 generated-artifact lift(s)", NOT governed). ## Docs `content/docs/data-modeling/fields.mdx`: a row in the Conditional Logic table and a "Cell formatting" subsection with the card's worked example (an amount below zero in red), the authoring checks, the presentation-only boundary (a deadline stays `dueLike` / `settledWhen`), and one closing status paragraph: the console reads the rules in a later objectui release. That paragraph is dropped on the ledger flip. ## Tests (all locked runs through `os-verify-lock`) Every suite below ran on the merged tree at `14b85354d3`. The one later commit, `ffce73a2ac`, changes only `packages/spec/llms.txt`: `check:llms-txt` passes at `ffce73a2ac`, and the one spec test that names that file (in a comment, `filter-array-declaration.test.ts`) re-ran green there with the new pin file (2 files, 32 passed). Filter direction: per package, `--filter PKG` itself (no consumer sweep); the consumer radius named by the dispatch (`packages/spec`, `packages/lint`, `packages/metadata*`, the examples) is run package by package. | Suite | Result | |:--|:--| | `@objectstack/spec` `--project local` | 632 files, 18,894 passed, 1 todo | | `@objectstack/spec` `--project repo` | 54 files, 915 passed | | `@objectstack/lint` (whole package) | 132 files, 6,047 passed | | `@objectstack/metadata-core` | 17 files, 388 passed | | `@objectstack/metadata-protocol` (whole package) | 223 files passed, 3 skipped; 28,347 passed, 19 skipped | | `@objectstack/metadata` | 58 files, 871 passed | | `@objectstack/driver-sql` | 218 files passed, 11 skipped; 3,635 passed, 206 skipped | | `@objectstack/dogfood` `test/expression-conformance.test.ts` | 7 passed | | example-showcase / example-crm / example-todo | 33 / 408, 5 / 45, 7 / 238 passed | | `objectstack validate` (showcase, crm, todo) | Validation passed, all three | | `typecheck`: spec (incl. `check:test-typecheck`), lint (incl. `check:test-typecheck`), metadata-core, driver-sql, metadata-protocol, dogfood | all exit 0 | New pins: `packages/spec/src/data/field-conditional-formatting.test.ts` (19: identity of the element on both members, accept, order, any field type, control without the key, refusals at the rule with issue path plus the message's first sentence) and `packages/lint/src/validate-expressions.field-conditional-formatting.test.ts` (41: `value` and `record` accepted, `today()`, a non-parsing condition refused naming the field and the rule, bare field, unknown field, every judged root other than `record` refused with this surface's verdict and only that one, control on the field-rule family's own slots, `os build` and the runtime object door under `expression-invalid`), plus one FLS case in `object-schema-fls-references.test.ts`. `@objectstack/cli` integration tier: not run locally, declared to CI (no spawn entry touched). ## Ablation (each committed first, mutated through `scripts/ablation-replace.mjs`, restored to blob == HEAD with an empty `git diff HEAD`; subjects resolve to `src` through relative imports, so no `dist` leg) Expected direction for each: red. Observed: red. | Mutation | Suite | Result | |:--|:--|:--| | the cell-rule `check` call passes no `value` root | lint pin file | 4 red, 37 green | | `fieldFormattingRootIssue` returns `null` always | lint pin file | 29 red, 12 green | | `FieldSchema.conditionalFormatting` mounts a loose `z.object` instead of the shared element | spec pin file | 9 red, 10 green | | FLS scrub drops only the condition (`arrayOf(block(...))`) instead of the whole rule | metadata-core FLS file | 1 red, 51 green | ## Gates At `ffce73a2ac`: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 116 families (the same 116 as before the last commit); with the 51 artifact-roster commands, 166 commands ran. `--ran`: "116 derived, 115 run, 0 NOT-MEASURED, 1 UNRUN" — the one is `pnpm check:dual-build-cjs-loads`, NOT MEASURED by dispatch (no whole-workspace build), left to CI. Of the 115: 114 exit 0, and `pnpm check:platform-checklist` exit 1, which is red identically on a pristine `origin/main` `faf6348508` worktree (see Acceptance notes). Roster: 48 exit 0; the three PR-context guards (`check-closing-target-claim`, `check-partof-closing-keyword`, `check-single-claim-paths`) answer exit 2 without PR context; they are re-run with this PR's context after it opens, and their verdicts are recorded in the `os-dev-report` comment on #22228. `check:generated` (spec, after a build): all 15 artifacts current. ## Acceptance notes - **The list view's unknown-rule-key refusal reads differently.** One element, one message: the `history` sentence is now true for both members, and the `color` guidance leads with "put it in `style`" before pointing a list view at `rowColor`. The accept set is unchanged; no test or doc pinned the old wording (`git grep` of both sentences: 0 hits outside the schema). - **One expression-ledger row speaks for two mounts.** The conformance ledger's unit is the declaring position, and the shared element is one position. The `cel-ui` row keeps `enforced` for the list-row mount and its note records that the field mount has no evaluator yet; the liveness row carries the field mount's `planned` state. - **The docs-corpus gate does not judge these conditions.** `check:doc-formula-expressions` admits only the field-level `*When` slots; a `condition` nested in `conditionalFormatting` is outside its arms, so the new docs example is judged by nothing at the corpus level (it is the same predicate the lint pins accept). Noted, not extended. - **No traversal refusal, null-guard or `parent` gate on this slot**, like `visibleWhen`: those are write-path gates and nothing on the write path reads it. Whether a cell renderer hydrates `record.REFERENCE.FIELD` is for objectui's card to declare (carrier: the objectui renderer card). - **No `authorWarn`.** Until objectui reads the key an authored rule styles nothing; the docs status paragraph and the changeset say so. A warned field row would reopen the lint's field walk for one display key (the `settledWhen` precedent). - **`check:platform-checklist` is red on `origin/main` itself** (`faf6348508`, a pristine worktree: `areas/attachments-storage.json` ABSENT SYMBOL `attachment-access-hooks.ts#canEdit`, anchors 27 of floor 28). This diff touches neither file. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0e00d36 commit 3d886ee

28 files changed

Lines changed: 699 additions & 36 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
'@objectstack/metadata-core': patch
5+
'@objectstack/driver-sql': patch
6+
---
7+
8+
A field can declare cell formatting rules: `conditionalFormatting: [{ condition, style }]`, the rule a list view already declares for its rows
9+
10+
Clause-②: yes (widening)
11+
12+
- **`FieldSchema.conditionalFormatting`** (optional) is an ordered list of `{ condition, style }` rules. The first rule whose CEL `condition` holds applies its CSS `style` map to that field's cell, wherever the field renders. A list view's row rules still style the row, and both may apply to one record. Example: `amount: Field.currency({ conditionalFormatting: [{ condition: P\`value < 0\`, style: { color: '#b91c1c' } }] })`.
13+
- **One rule element, no second style vocabulary.** The `{ condition, style }` element moved out of `ListViewSchema` into its own spec module, and both `ListViewSchema.conditionalFormatting` and `FieldSchema.conditionalFormatting` mount it. The list view accepts exactly what it accepted before. Its refusal of an unknown rule key now reads the same on both members: for a `color` written beside `style`, it says to put the colour in `style` first, then points a list view at `rowColor`.
14+
- **The scope is `value` and `record`.** `value` is this field's value on the record and `record` is the row. `@objectstack/lint` judges each condition at `objectstack build`, `objectstack validate` and the object save: it must parse, read fields as `record.<field>`, name declared fields, and read no other root (`previous`, `parent` and the user roots are refused, naming the field and the rule).
15+
- **For presentation only.** Use it for rules that carry no meaning of their own, such as an amount below zero in red or stock under its safety level in amber. A deadline's overdue state stays a declaration (`dueLike` with `settledWhen`).
16+
- **Display only.** Nothing on the write path reads the key, and every metadata that parsed before parses unchanged. Field-level security drops a rule whose condition reads a field the caller cannot read, keeping the other rules in order (`@objectstack/metadata-core`). The SQL driver classifies the key as presentation, so it never reaches column DDL (`@objectstack/driver-sql`).
17+
- **Who reads it.** objectui's cell renderers read the rules in a follow-up release, once this spec version is published. Until then a rule is validated when it is authored and styles no cell.

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ For values that must be encrypted at rest (API keys, tokens, DB passwords), use
391391
| `readonlyWhen` | `string \| Expression` | CEL predicate; field is read-only when `TRUE` |
392392
| `requiredWhen` | `string \| Expression` | CEL predicate; field is required when `TRUE` |
393393
| `settledWhen` | `string \| Expression` | CEL predicate on a `dueLike` date; the deadline is settled when `TRUE` — see [Deadlines](#deadlines-duelike-and-settledwhen) |
394+
| `conditionalFormatting` | `{ condition, style }[]` | Cell formatting rules; the first rule whose CEL `condition` is `TRUE` styles this field's cell — see [Cell formatting](#cell-formatting-conditionalformatting) |
394395
| `multiple` | `boolean` | Allow multiple values (for select, lookup, file) |
395396

396397
```typescript
@@ -490,6 +491,50 @@ Two rules make this predictable:
490491
lock the platform could not evaluate is never waived: that is what makes the
491492
declaration a guarantee instead of a hint.
492493

494+
#### Cell formatting: `conditionalFormatting`
495+
496+
`conditionalFormatting` styles a field's **cell** by a rule: an ordered list of
497+
`{ condition, style }`, the same rule a list view's `conditionalFormatting`
498+
declares for its rows. The first rule whose CEL `condition` is `TRUE` applies
499+
its CSS `style` map to this field's cell wherever the field renders — a grid, a
500+
kanban card, the record page, a related list or a report cell. A list view's
501+
row rules style the row, and both may apply to one record.
502+
503+
The condition reads two roots: `value`, this field's value on the record, and
504+
`record`, the row.
505+
506+
```typescript
507+
import { P } from '@objectstack/spec';
508+
import { Field } from '@objectstack/spec/data';
509+
510+
amount: Field.currency({
511+
label: 'Amount',
512+
conditionalFormatting: [
513+
{ condition: P`value < 0`, style: { color: '#b91c1c' } },
514+
],
515+
}),
516+
```
517+
518+
The rules are checked when the metadata is authored, like `visibleWhen`:
519+
520+
- A rule is exactly `{ condition, style }`, the condition is not blank, and
521+
`style` maps CSS property names to string values. Anything else is refused at
522+
parse.
523+
- The condition must parse, read other fields as `record.<field>`, name fields
524+
the object declares, and read no root but `value` and `record`.
525+
`objectstack validate`, `objectstack build` and saving the object all refuse
526+
anything else, naming the field and the rule.
527+
528+
The rules are for presentation: an amount below zero in red, stock under its
529+
safety level in amber, a priority cell coloured by its value. A state with a
530+
meaning of its own belongs in a declaration instead — a deadline's overdue
531+
state is [`dueLike` with `settledWhen`](#deadlines-duelike-and-settledwhen),
532+
from which the wording and the colour both follow. Nothing on the write path
533+
reads the rules.
534+
535+
The console's cell renderers read these rules in a later ObjectUI release.
536+
Until then a rule is validated when it is authored and styles no cell.
537+
493538
## Naming Conventions
494539

495540
- Field names use `snake_case`: `first_name`, `annual_revenue`, `is_active`

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const result = CurrencyConfigSchema.parse(data);
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. |
110110
| **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. |
111111
| **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. |
112+
| **conditionalFormatting** | `{ condition: string \| object; style: Record<string, string> }[]` | optional | Cell formatting rules for this field — `[{ condition, style }]`, the same rule a list view's `conditionalFormatting` declares. In order, the first rule whose CEL `condition` holds applies its CSS `style` map to THIS FIELD'S CELL wherever the field renders (grid, kanban card, record page, related list, report cell); a list view's row rules style the row, and both may apply. The condition reads `value` (this field's value on the record) and `record` (the row) and no other root, e.g. P`value < 0` with `style: { color: '#b91c1c' }`; `objectstack validate` refuses a condition that does not parse or reads any other root. For presentation only — a semantic state such as a deadline's overdue is declared with `dueLike` / `settledWhen` instead. Display only: nothing on the write path reads it. |
112113
| **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". |
113114
| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI |
114115
| **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. |
@@ -241,6 +242,13 @@ const result = CurrencyConfigSchema.parse(data);
241242
| **currencyMode** | `Enum<'dynamic' \| 'fixed'>` | optional (default: `"dynamic"`) | Currency mode. `fixed`: the field has one currency, `defaultCurrency`. `dynamic` (the default): the field has no currency of its own — amounts display in the tenant default currency (the `localization.currency` setting; a plain number when none is set) and `defaultCurrency` is not read. Neither mode is a per-record choice: the value is a bare number either way. |
242243
| **defaultCurrency** | `string` | optional (default: `"CNY"`) | The currency code (ISO 4217, e.g. USD, CNY, EUR) of a `fixed`-mode field: its one currency. Not read under `dynamic` (the default), where amounts display in the tenant default currency. |
243244

245+
### Nested Shape: `Field.conditionalFormatting[number]`
246+
247+
| Property | Type | Required | Description |
248+
| :--- | :--- | :--- | :--- |
249+
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) to evaluate. |
250+
| **style** | `Record<string, string>` | ✅ | CSS styles to apply when condition is true |
251+
244252
### Nested Shape: `Field.maskingRule`
245253

246254
| Property | Type | Required | Description |

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ const result = ApiMethod.parse(data);
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. |
275275
| **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. |
276276
| **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. |
277+
| **conditionalFormatting** | `{ condition: string \| object; style: Record<string, string> }[]` | optional | Cell formatting rules for this field — `[{ condition, style }]`, the same rule a list view's `conditionalFormatting` declares. In order, the first rule whose CEL `condition` holds applies its CSS `style` map to THIS FIELD'S CELL wherever the field renders (grid, kanban card, record page, related list, report cell); a list view's row rules style the row, and both may apply. The condition reads `value` (this field's value on the record) and `record` (the row) and no other root, e.g. P`value < 0` with `style: { color: '#b91c1c' }`; `objectstack validate` refuses a condition that does not parse or reads any other root. For presentation only — a semantic state such as a deadline's overdue is declared with `dueLike` / `settledWhen` instead. Display only: nothing on the write path reads it. |
277278
| **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". |
278279
| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI |
279280
| **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. |
@@ -611,6 +612,7 @@ const result = ApiMethod.parse(data);
611612
| **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. |
612613
| **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. |
613614
| **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. |
615+
| **conditionalFormatting** | `{ condition: string \| object; style: Record<string, string> }[]` | optional | Cell formatting rules for this field — `[{ condition, style }]`, the same rule a list view's `conditionalFormatting` declares. In order, the first rule whose CEL `condition` holds applies its CSS `style` map to THIS FIELD'S CELL wherever the field renders (grid, kanban card, record page, related list, report cell); a list view's row rules style the row, and both may apply. The condition reads `value` (this field's value on the record) and `record` (the row) and no other root, e.g. P`value < 0` with `style: { color: '#b91c1c' }`; `objectstack validate` refuses a condition that does not parse or reads any other root. For presentation only — a semantic state such as a deadline's overdue is declared with `dueLike` / `settledWhen` instead. Display only: nothing on the write path reads it. |
614616
| **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". |
615617
| **hidden** | `boolean` | optional (default: `false`) | Hidden from default UI |
616618
| **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)