Skip to content

Commit 3983965

Browse files
fix(lint)!: a lookup or master_detail into a stored-metadata-body table is refused at author time (#24800)
Fixes #24677 Clause-②: no (narrowing) The authoring doors refuse a `lookup` or `master_detail` field whose `reference` is a stored-metadata-body table (`STORED_METADATA_BODY_OBJECTS`: `sys_metadata`, `sys_metadata_history`). This is the rule the maintainer ruled B on #24358 (「24358 同意」, then 「ok」 to its shape), pulled into 18.0 on 「都派」 (record 6282780489). Claim 6282819504. ## What changes - **`@objectstack/lint`.** The expression rule's per-object pass (`runStackExpressionPasses` in `validate-expressions.ts`) refuses the field. It sits beside the #24316 hop refusal, keyed on that refusal's own collector, `storedMetadataBodyReferencesOf`, and its per-object index, so the two refusals judge one set. - The finding is located at the key the author edits: `object 'NAME' · field 'FIELD' reference`. - It is reported under the existing `expression-invalid` rule, so there is no new error code and no new registry entry. It rides the object crossing the rule already has (`runtimeTypes` is unchanged), so `os validate`, `os build` and the object save door give the same finding. - The message names the field, its type, the target and the rule, and ends on `STORED_METADATA_BODY_PRESCRIPTION`. - The pass runs after `masterDetailCount`, so an unreadable `reference` carrier is still refused by the pass that owned that verdict first. It reads the field's type from the walk's field-type index rather than off the field (the #5017 receiver meta-test pins the keys read off `f`). - **`@objectstack/spec`.** The `FieldSchema.reference` describe and its TSDoc state the convention. This is the field's own `reference` at `field.zod.ts:1476`; `:989` is a grid cell column's. The reference pages were regenerated by `gen:docs` only (`content/docs/references/{data/field,data/object,system/migration}.mdx`, the describe line and nothing else). - **Docs.** `content/docs/data-modeling/field-types.mdx`: the `reference` rows of `lookup` / `master_detail`, plus a short section saying why the reference is refused. - **Riders,** declared on #6367 (comment 6282830389). Test files only, ⛔ no `protocol.ts` change: - a door block in `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, through the real `saveMetaItem` / `publishMetaItem`; - one row in the family enumeration pin `stored-metadata-body-family.pin.test.ts` (28 to 29). - ⛔ **Not touched:** the engine guards (they stay, for every path that runs no authoring rule), object keys, `apiEnabled`, and error codes. ## Premises, re-measured **P1: the doors accepted it.** Measured before the change, at base `4b114eda57`. Probes: one object carrying `meta`, a `lookup` or a `master_detail` into each family table; the controls point to `sys_user`. | Door | Before (`4b114eda57`) | After (head `5e54169edc`) | |:---|:---|:---| | `os validate` (CLI built from the tree) | 4/4 family probes exit 0; controls 2/2 exit 0 | 4/4 exit 1, refused at `object 'probe_carrier' · field 'meta' reference` under `expression-invalid`; controls 2/2 exit 0 | | `os build` | 4/4 exit 0, artifact written; controls 2/2 exit 0 | 4/4 exit 1; controls 2/2 exit 0 | | `PUT /api/v1/meta/object/NAME` on a fresh `pnpm dev:crm -- --fresh` backend, as the dev admin | 4/4 `200`; controls 2/2 `200`; gate-live control (a `sqrt(record.amount)` formula) `422` | 4/4 `422 INVALID_METADATA`, with the issue at `object 'probe_…' · field 'meta' reference` and the message ending on the prescription; controls 2/2 `200` | | lint's object-door gate in process (`runRuntimeAuthoringRules`) | 0 errors for all 4 and both controls | 1 error for each of the 4; 0 for the controls | **P2: where the hop refusal runs.** `refuseStoredMetadataBodyHop` and its collector index live in `runStackExpressionPasses` (`validate-expressions.ts`, about `:1934` and `:1953` on base). It is registered as `validateStackExpressions` in `authoring-rules.ts`, with `commands: ALL`, `surfaces: CLI_AND_RUNTIME` and `runtimeTypes: ['flow', 'action', 'hook', 'object']`. On an object write, the gate's `runtimeWriteType` runs the passes over the object's own body. The new refusal is in that same per-object loop, so it reaches the same doors. P1's "after" column is that reading. **P3: exposure.** A `git grep` for a family table as a `reference` (string literal, JSON, the `Field.lookup` / `Field.masterDetail` builders, `SystemObjectName.METADATA`): - over `examples/**`, `packages/apps/**`, `packages/platform-objects/**`, `packages/create-objectstack/src/templates/**` and `packages/qa/**` at `4b114eda57`: **0**. A positive control matched 3 of 3 synthetic lines. - over the whole repository at `4b114eda57`: 0. On `origin/main` `a43405d596`: 0. - control, references to an ordinary platform table over the same paths: **45 files** (36 in `platform-objects`, 4 in `app-showcase`, 1 in `app-todo`, 4 in `qa`). The family objects themselves (`packages/metadata-core`) reference other tables (`sys_user`, `sys_package_version`, `sys_environment`), and that is unaffected. Neither premise failed, and the family set is unchanged. ## Tests Run on head `5e54169edc`, each under `os-verify-lock.sh`, `--maxWorkers=2`: | Package | Result | |:---|:---| | `@objectstack/lint` test | 149 files / 6965 passed | | `@objectstack/lint` typecheck | exit 0 | | `@objectstack/metadata-protocol` test | 251 passed + 3 skipped files / 20818 passed + 19 skipped | | `@objectstack/metadata-protocol` typecheck | exit 0 | | `@objectstack/spec` test (`--project local`) | 702 files / 21352 passed + 1 todo | | `@objectstack/spec` typecheck | exit 0 | | `@objectstack/objectql` (`--project local`) | 450 / 8490 | | `@objectstack/platform-objects` | 79 / 1195 | | `@objectstack/runtime` (`--project local`) | 430 / 6779 + 19 skipped | | `@objectstack/cli` (`--project unit`; integration is declared to CI) | 314 / 4641 | | `os validate` and `os build` on every example app (`app-crm`, `app-multi-package`, `app-showcase`, `app-todo`) | 8/8 exit 0 | **Consumer sweep (G2).** No fixture outside `packages/lint` / `packages/spec` / `content/docs` went red, so there is no fixture for a rider. The family-iterating engine tests in `@objectstack/objectql` register objects directly and run no authoring rule (they are the installed-artifact path), and they are green. **The new pins:** - `packages/lint/src/validate-expressions.stored-metadata-body-reference.test.ts`, 21 cases: - each family table × `lookup` / `master_detail` is refused at the build, at `os validate` / `os build` (`runAuthoringRules`), and at the object door with the build's own finding; - a `user` naming the family is refused too (the collector's type set); - a stored object re-saved is still refused; - a stored sibling is not charged (the differential); - the controls (`sys_user` and an app object, as `lookup` and as `master_detail`) draw nothing; - the hop refusal is unchanged beside it. - The protocol door block (8 cases): 4 refusals with the `422` / `INVALID_METADATA` envelope and nothing landing; a draft's promotion refused; 2 controls saved; door finding equal to build finding. ## Ablation (G4) Run through `scripts/ablation-replace.mjs` on `6c2c70f3eb`. The later commits change only the message text and add a `user` pin; the mutated line is unchanged. - **Mutation.** `if (!target || !type) continue;` became `if ('ablation-24677') continue;`: anchor 1 to 0, blob `e85a4327e28f` to `6564a909a8fd`. - The lint pins (they import `src`) went **15 red / 11 green** of the 26 cases then on the branch (the `user` pin came later). The 11 green are the 4 controls, the differential and the 6 hop pins. - The lint build's JS pass emitted the mutation, and `ablation-dist-preflight` found the marker in 4 dist files. The DTS pass failed on the mutation itself (TS2872 / TS2345), which does not touch the JS the suite reads. - The protocol door block (it reads lint's `dist/`) went **6 red / 2 green**. The 2 green are the controls. - **Restore.** By the tool's trap: blob back to `e85a4327e28f` (equal to HEAD), `git diff HEAD` empty, whole-tree `git status --porcelain` clean. - Lint was rebuilt (exit 0), and preflight `--absent` found the marker in 0 of 20 dist files. - The lint pins went 26/26 green and the door block 8/8. On the final head the lint pins are 27/27 (with the hop file's 6). ## Gates - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, no paths, on `5e54169edc`: 111 commands. Each exit code was captured before any pipe. - `--ran`: `Run reconciliation — 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN`. - `pnpm check:type-check-debt` exited 3 in the battery: a queue-timeout on the verify lock, so nothing was measured. It was re-run under the lock: exit 0, `1 ledger entr(ies) re-measured … none above its recorded number`. - Every other command exited 0. The clone was deepened (`--shallow-since=2026-07-13`) first. - Local-only families: `check:dev-prereqs` exit 0 and `check:skill-frame-freshness` exit 0. - `@objectstack/spec` `check:generated`: `All 14 generated artifacts are up to date` after `gen:docs`. - `check:adr-0087-registration`: green, `not-required (no-migration-prescription)`. `check-changeset-no-major`: green, no `major`. - CI check-runs on this PR are not yet reported at open. ## Changeset `.changeset/24677-reference-into-stored-metadata-body-refused.md`: `@objectstack/lint` `minor`, `@objectstack/spec` `patch` (the describe text only). - It is BREAKING, an authoring narrowing, shipped `minor` under the launch-window convention. It carries the FROM / TO / fix mapping and the stored-row sentence. - The stored-row sentence: a stored object row that already declares such a field keeps loading (rehydration runs no authoring rule, and the engine guards hold its reads). Its next active save or draft promotion is refused until the field is removed or retargeted. The lint pin "a stored object that already declares one is still refused on its next save" holds this. - The ADR-0087 disposition is in the changeset. - `.changeset/pre.json` on `main` is `{"mode": "exit", "tag": "next"}`. So this changeset is consumed by the release that leaves the `next` pre-release line, and nothing here changes that mode. ## Acceptance notes - **`expression-invalid` for a field declaration.** The rule's id is the expression rule's, and that is where the ruling put the refusal ("where the hop refusal already runs"). The location (`… field 'FIELD' reference`) and the message carry the specifics. - **A `user` field.** The collector's type set is `REFERENCE_VALUE_TYPES`, and the parse accepts an authored `reference` on a `user` field (measured: `ObjectSchema.safeParse` accepts `{ type: 'user', reference: 'sys_metadata' }`). So a `user` naming the family is refused the same way, and is pinned. The collector's docblock claimed otherwise and now says what was measured. A `tree` cannot name the family: the object schema refuses a foreign `tree` target. - **Out of scope, reported to the seat rather than fixed here.** `os validate` accepts a `user` field whose `reference` names any other object (measured: `{ type: 'user', reference: 'probe_account' }`, exit 0). `Field.user`'s contract says "target fixed to the `sys_user` system object", and `referenceTargetOf` returns the authored carrier first. - **Branch base.** The branch merged `origin/main` once (`37aa1d68d6`). `origin/main` has since moved 10 commits (`a43405d596`), and none of them touches this diff's files. - **Review.** A `CONTRACT_REVIEW_TIER` review of record on the narrowing face is owed before the queue. --- _Generated by [Claude Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c56115b commit 3983965

11 files changed

Lines changed: 420 additions & 17 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/lint': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(lint)!: a `lookup` or `master_detail` field whose `reference` is a stored-metadata-body table is refused where it is declared
7+
8+
Clause-②: no (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No spec key, stored shape or authorable spelling is added, removed or renamed. The expression rule's object pass now refuses one field declaration it accepted before: a `lookup` or `master_detail` whose `reference` is `sys_metadata` or `sys_metadata_history`, which the engine never hydrates. Nothing stored is rewritten, so there is nothing for `objectstack migrate meta` to convert; the `@objectstack/spec` change is the text of the `reference` describe only. -->
11+
12+
**BREAKING** (an authoring narrowing), shipped as `minor` under the launch-window convention for breaking changes.
13+
14+
A `lookup` or `master_detail` whose `reference` names a table of the stored-metadata-body family (`STORED_METADATA_BODY_OBJECTS`: `sys_metadata`, `sys_metadata_history`) is an error. The engine never hydrates such a field: `$expand` keeps the stored id and never nests the row, a formula or a validation rule never reads through it, and a master-detail child's `parent` root stays unbound, so every write a `parent` rule judges is refused. `os validate`, `os build` and the object save door (`PUT /api/v1/meta/object/:name`, and a draft's promotion) run the same pass, under the existing `EXPRESSION_INVALID` rule, located at the field's `reference`.
15+
16+
- FROM: `meta: { type: 'lookup', reference: 'sys_metadata' }` (or a `master_detail`, or `sys_metadata_history`) saved with a 200 and built with exit 0, and then only ever showed an id. TO: refused, with `422 INVALID_METADATA` at the save door and an error at `os validate` / `os build`. The message names the field, its type and the target, and ends on the family's one prescription.
17+
- A reference to any other table, platform tables such as `sys_user` included, is unchanged.
18+
- A `user` field whose authored `reference` names a family table is refused the same way: the parse accepts an authored `reference` on a `user` field, and it is the target every reader resolves.
19+
- The refusal of a formula or a validation rule that reads through such a field is unchanged, and is still reported at that expression.
20+
- A stored object row that already declares such a field keeps loading: no authoring rule runs when a stored row is rehydrated, and the engine guards keep refusing every relationship read into the family. Its next active save, or a draft's promotion, through the metadata API is refused until the field is removed or retargeted. The engine guards stay, for every path that runs no authoring rule.
21+
- `@objectstack/spec` (`patch`): the `reference` describe states the convention, so the generated field reference says it too.
22+
23+
What to do. Remove the field, or retarget it. To point a record at a metadata item, store the item's type and name in `text` fields and read the item through the metadata API (`GET /api/v1/meta/:type/:name`), where its body is projected.

‎content/docs/data-modeling/field-types.mdx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ Reference to a record in another object (foreign key).
307307

308308
| Property | Type | Default | Description |
309309
|:---|:---|:---|:---|
310-
| `reference` | `string` | **required** | Target object name (snake_case) |
310+
| `reference` | `string` | **required** | Target object name (snake_case). A stored-metadata table (`sys_metadata`, `sys_metadata_history`) is refused — see [below](#a-relationship-never-targets-a-stored-metadata-table) |
311311
| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property. `FieldSchema` is a strict object, so an unknown key is **rejected with guidance**, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) |
312312
| `deleteBehavior` | `'restrict' \| 'cascade' \| 'set_null'` | `'set_null'` | Behavior when referenced record is deleted. On a *required* lookup `set_null` is escalated to `restrict`, since a NOT NULL foreign key cannot be cleared — **whether the `set_null` was defaulted or written out explicitly**. On a `multiple: true` required lookup the escalation is judged per referencing row: only a row the member removal would leave EMPTY is refused. `cascade` and `restrict` are the values honored as written. Where `set_null` does run, a `multiple: true` lookup loses only the deleted **member** — the other members are kept, and a set emptied that way is stored as `[]`, never `null` |
313313

@@ -329,7 +329,7 @@ Parent-child relationship (cascading delete by default).
329329

330330
| Property | Type | Default | Description |
331331
|:---|:---|:---|:---|
332-
| `reference` | `string` | **required** | Target (master) object name |
332+
| `reference` | `string` | **required** | Target (master) object name. A stored-metadata table is refused — see [below](#a-relationship-never-targets-a-stored-metadata-table) |
333333
| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property. `FieldSchema` is a strict object, so an unknown key is **rejected with guidance**, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) |
334334
| `deleteBehavior` | `'restrict' \| 'cascade'` | `'cascade'` | Behavior when parent is deleted. `restrict` refuses the parent delete while children exist; `cascade` (and omitting the key) deletes the children with the parent. An explicit `set_null` is a **parse-time rejection** on this type (#9689) — a detail row cannot outlive its master; use a `lookup` if children must survive the parent |
335335
| `inlineEdit` | `boolean \| 'grid' \| 'form'` | — | Edit child records inline on the parent create/edit form (`true` = auto-pick, `'grid'`, or `'form'`) |
@@ -347,6 +347,10 @@ Parent-child relationship (cascading delete by default).
347347
}
348348
```
349349

350+
#### A relationship never targets a stored-metadata table
351+
352+
A `lookup` or `master_detail` whose `reference` is `sys_metadata` or `sys_metadata_history` is refused by `os validate`, `os build` and the object save door alike. Those rows carry serialized metadata bodies, which are served only through the metadata API (`GET /api/v1/meta/:type/:name`), where the body is projected; the engine never hydrates a reference into them, so `$expand` would keep the stored id and never nest the row, and a master-detail child's `parent` root would stay unbound, refusing every write a `parent` rule judges. To point a record at a metadata item, store its type and name in `text` fields and read the item through the metadata API.
353+
350354
### `tree`
351355
Self-referential hierarchy (e.g., categories, org chart).
352356

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const result = CurrencyConfigSchema.parse(data);
7676
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
7777
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
7878
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
79-
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
79+
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
8080
| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
8181
| **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. |
8282
| **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. |

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ const result = ApiMethod.parse(data);
242242
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
243243
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
244244
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
245-
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
245+
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
246246
| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
247247
| **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. |
248248
| **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. |
@@ -598,7 +598,7 @@ const result = ApiMethod.parse(data);
598598
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
599599
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
600600
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
601-
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
601+
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. |
602602
| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
603603
| **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. |
604604
| **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. |

0 commit comments

Comments
 (0)