Skip to content

Commit e5687cb

Browse files
feat(spec): an email field declares itself a correspondent address for inbound mail (FieldSchema.mailAssociation) (#22943)
Part of #22870 Clause-②: yes (widening) PR 1 of 2 on #22870, as ruled in `6116489638` (Q1 → A) and built from the stage-0 report `6115941082` §3.3, §3.5 and §4. PR 2 (`packages/services/service-mailbox`: the three objects, both read gates, `manage_mailboxes` and the `mailbox` token) stays on the card, so #22870 remains open after this lands. The key's runtime reader is the sibling ingest card #22871, which is `Blocked-by: #22870`. ## The ruling executed (verbatim, not re-ruled) > **Q1 → A. The seam is a declared field-level key.** `FieldSchema.mailAssociation: true`, valid only on `type: 'email'` and refused at parse on any other type (the `referenceVia` refine's shape). Input: the participant set (normalised addr-specs from From, Sender, Reply-To, To and Cc, plus Q3's forwarded-part headers, minus the mailbox's own addresses), scoped to the mailbox's organisation. Output: `{ object, recordId, matchedField, matchedAddress }` rows, which become `sys_mail_association` links; an empty set means the pool. The platform learns which fields carry a correspondent address and never what a contact is. No published resolver contract in phase 1. The card's constraint, verbatim: "Declared = enforced. Every key this card adds must get its runtime reader in the sibling ingest card, or it is not added. Liveness rows start `planned` and leave it when the reader lands." ## What | Surface | Change | |:--|:--| | `FieldSchema` (`packages/spec/src/data/field.zod.ts`) | `mailAssociation: z.boolean().optional()` beside `referenceVia`. The TSDoc and the describe state the input, the output, the organization scope and the one legal type. No default, so `undefined` always means "not authored". | | The parse door | `FieldSchema`'s `superRefine`, right after the two `referenceVia` checks. On any type but `email` the key (TRUE or FALSE) gets one `custom` issue at `[mailAssociation]`. The message names the field's type and the remedy: change the type to `email`, or drop the key. | | Studio | `field.form.ts`: one row, "Match Inbound Mail", `visibleWhen: "data.type == 'email'"`. This is a parse gate, so the row shows exactly where the key parses. The help text says where the key takes effect ("only where a mailbox service is set up"), not when. The zh-CN, ja-JP and es-ES leaves are hand-authored. | | Liveness | `liveness/field.json`: `planned`, `authorWarn: true`, `authorHint` "Keep it: no mailbox service reads this yet; it takes effect when the mailbox capability ships." The note names the flip condition: the row goes `live` when the #22871 ingest lands, citing its matcher, and drops `authorWarn` / `authorHint` in the same edit. | | Changeset | `.changeset/22870-field-mail-association.md`: `@objectstack/spec` `minor`, `Clause-②: yes (widening)`; `@objectstack/driver-sql`, `@objectstack/metadata-core` and `@objectstack/platform-objects` `patch` (added in the patch round, seat order 6117707461). | Nothing outside `packages/spec` reads the key yet. No object, gate, capability name or capability token is added; those belong to PR 2. ## Declared = enforced | Half | Where | State after this PR | |:--|:--|:--| | Parse (authoring) | `FieldSchema` superRefine | Enforced: refused off `email` | | Author warning | `@objectstack/lint` `liveness-planned-property`, at `objectstack validate` / `objectstack lint` and the metadata save door | Live: an authored `mailAssociation: true` warns with the `authorHint` | | Runtime reader | Mailbox service ingest and re-match (#22871) | Not yet. The row is `planned` until that reader lands | ## Mechanism assumptions, measured at `origin/main` `018b077fb` 1. **Holds.** `git grep -c mailAssociation -- packages` answers 0 hits (exit 1). Control: `git grep -c referenceVia -- packages/spec/src/data/field.zod.ts` answers 12. 2. **Holds.** The `referenceVia` type check sits at `field.zod.ts:2173`–`:2200`, inside `FieldSchema`'s `superRefine`. The pin `expect([...authorWarnedProperties('field')]).toEqual(['conditionalRequired'])` sits at `lint-liveness-properties.test.ts:1624`. 3. **Holds.** `dropped-refinements.baseline.json` gains no site. `data/Field` already carries the root site `""`, the file is unchanged, and `check:authorable-surface` is green. 4. **Partly holds.** These generated shards moved: `authorable-surface/data.json` (+1 line, written by the spec build), `content/docs/references/data/field.mdx`, `.../data/object.mdx` and `.../system/migration.mdx` (from `gen:docs`), and `liveness/state-counts/field.md` (from `gen:liveness-counts`). The predicted `json-schema.manifest/` shards did **not** move. After regeneration `check:generated` answers "All 14 generated artifacts are up to date". 5. **Holds.** No gate demanded a governed surface. Five non-governed classification ratchets did, listed in the next section. ## Files beyond the claim's declared surface (each demanded by a pin; none governed) The claim fenced `field.zod.ts`, `field.form.ts`, one new spec test, `liveness/field.json`, the `authorWarnedProperties('field')` pin, the generated artifacts and one changeset. A new `FieldSchema` key and a new form row are also counted by the ratchets below, and each one is red until the key is classified. The `dueLike` precedent (215e662) paid the same set. - `packages/drivers/driver-sql/src/builtin-column-collision.ts`: `FIELD_KEY_STORAGE_CLASS` is pinned against `FieldSchema.shape`. The new entry is `mailAssociation: 'presentation'`. - `packages/metadata-core/src/object-schema-fls-references.ts`: `FIELD_REFERENCE_POSITIONS` is pinned against `FieldSchema.shape`. The new entry is `keep`, because the key is a boolean that names no field. - `packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts`: the served `field` property count moves 78 → 79. - `packages/lint/src/validate-predicate-path-refs.test.ts`: the shipped-form predicate census moves 87 → 88, and the literal-comparison census moves 64 → 65. - `packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts`: the row's label and help text, in en plus three hand-authored locales. In `object-lifecycle-panel-echo-decisions.test.ts`, the translated-label control moves 669 → 670. Neither classification table changes runtime behaviour for a boolean. An unclassified key was already silent in driver-sql, and the FLS scrub already passed a boolean through. The changeset first named `@objectstack/spec` alone, as the precedent's did; the patch round (seat order 6117707461) added a `patch` entry for each of the three touched published packages, because every touched published package names itself in a changeset. ## Tests (HEAD `52bcc8429`) - **New spec test** `packages/spec/src/data/field-mail-association.test.ts` (16 cases): - Accepted on `email` (TRUE and FALSE, full `safeParse` green), and no default materializes. - A non-boolean is `invalid_type`. - Refused on `text`, `url`, `phone`, `textarea`, `lookup`, `formula`, `number` and `boolean`. Each case gets exactly one `custom` issue at path `['mailAssociation']`, naming `type: 'email'` and the offending type. - An off-type `false` is refused. - Positive control: the refusal is the only issue on the text fixture, and the same fixture without the key parses. - The JSON-schema projection carries an optional `boolean` `mailAssociation`. - The form offers exactly one row, gated `data.type == 'email'`. - **Ablation** (committed state, `scripts/ablation-replace.mjs` with a `trap` restore): - The refusal condition was mutated to `if (false && field.mailAssociation …`, and the tool reported the anchor 1 → 0, blob `2a3049530` → `a290fcd02`. - Result: **10 failed | 6 passed (16)**. Every refusal case went red; the accept, projection and form cases stayed green. - Restore: blob back to the HEAD blob `2a30495308c6` and `git diff HEAD` empty, both from the tool and from the trap's own `git hash-object` comparison. - The test imports `./field.zod` from source, so no dist sits on the path. - **`@objectstack/spec`**, rebuilt at `52bcc8429`: the full suite `pnpm --filter @objectstack/spec test` passed 649 files / 19410 tests (1 todo), and `pnpm --filter @objectstack/spec typecheck` exits 0. - **Downstream suites**, each on a freshly built dependency closure (`pnpm --filter 'PKG^...' build` for the five packages): - `@objectstack/lint`: full suite, 137 files / 6529 tests passed. - `@objectstack/metadata-core`: full suite, 17 / 386 passed. - `@objectstack/platform-objects`: full suite, 69 / 1083 passed. - `@objectstack/driver-sql`: `builtin-column-collision.test.ts`, 11 passed. - `@objectstack/metadata-protocol`: `protocol.meta-types-degenerate-derivation.test.ts`, 18 passed. - `typecheck` exits 0 on all five. - **ESLint, narrowed to the 13 changed TS files** (`eslint --no-inline-config --format json`): 13 files linted, 0 errors, 0 warnings. - The population is `eslint.config.mjs`'s `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`; no changed file is ignored. - The config never enables type-aware linting (`eslint.config.mjs`: "no `parserOptions.project`, no typed `@typescript-eslint` rules"), so this diff cannot move a verdict on an untouched file. - The repo-wide `pnpm lint` is CI's. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) derived 114 commands at merge base `018b077fb`. Each one was run with its exit code recorded before any pipe, then reconciled with `--ran`: **114 derived, 112 run, 2 NOT-MEASURED, 0 UNRUN.** - `pnpm check:i18n` first answered PREREQUISITE NOT MET. It was re-run after building its stated prerequisite closure, and answered "all bundles in sync, no undeclared authoring keys" for 9 packages. That makes the hand-authored locale leaves byte-identical to the extractor's output, with the `*.source-hashes.generated.ts` files untouched. `check:lean-entry-closure` likewise went exit 3 → exit 0. - NOT MEASURED: `check:skill-examples`. Reason: the client SDK packages are not built with declarations, and this diff touches no skill or example. - NOT MEASURED: `check:dual-build-cjs-loads`. Reason: it needs every package built with declarations, while the local i18n prerequisite build was JS-only. - `check:platform-checklist` exits 1 with 8 ABSENT SYMBOL / ANCHORS LOST problems in `docs/qa/platform-checklist/areas/*.json`. The control run on a detached `018b077fb` checkout answers the identical 8, so this is not this diff. The gate is kept out of per-PR CI by `checklist-status.yml`'s own note. ## Acceptance notes - `packages/spec/liveness/README.md`'s `field` cell still says the type "carries NO `authorWarn` row at any depth". That was already untrue before this PR (`conditionalRequired`'s `dead` row has warned on its own since the verdict-warns ruling), and it is further from true now. Prose only; no gate reads it. Carrier: none. - `lint-liveness-properties.test.ts`'s "field walk: a malformed `fields` array …" block could take `mailAssociation` as a real subject. It is not re-subjected here because a `planned` row is scheduled to flip `live`, which is exactly the subject loss that block's neighbour exists to survive. - No hand-written docs page (`content/docs/data-modeling/fields.mdx`) is added. The reference page carries the describe, and a narrative page belongs with the reader, so no page advertises a capability that nothing delivers yet. ## Review `Clause-②: yes (widening)` on `packages/spec`, so a review at `CONTRACT_REVIEW_TIER` is owed before landing. No governed surface is in the file list. _Generated by [Claude Code](https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent da9c2bf commit e5687cb

20 files changed

Lines changed: 297 additions & 13 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/driver-sql': patch
4+
'@objectstack/metadata-core': patch
5+
'@objectstack/platform-objects': patch
6+
---
7+
8+
An `email` field can declare that it identifies a correspondent for inbound mail: `mailAssociation`
9+
10+
Clause-②: yes (widening)
11+
12+
- **`mailAssociation: boolean`** (optional, no default) on a field definition. TRUE marks this email field as identifying a message correspondent: when a mailbox service ingests a message, it associates the message with every record of the 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. Absent or FALSE: the field takes no part in matching.
13+
- **The platform learns which fields carry a correspondent's address, never what a contact is.** Every object that flags a field is treated alike; no app code and no resolver service is involved.
14+
- **The door that judges it.** `FieldSchema` refuses the key on any type other than `email`, naming the field's type and the fix (change the type to `email`, or drop the key). Every metadata that parsed before parses unchanged.
15+
- **Who reads it.** Nothing yet: the mailbox service's ingest is the reader, and it ships separately. Until then `objectstack validate` and `objectstack lint` warn an author who sets it (`liveness-planned-property`) that no mailbox service reads it yet.
16+
- **Where to author it.** The field designer offers it on email fields as "Match Inbound Mail".
17+
- **`@objectstack/driver-sql`.** `FIELD_KEY_STORAGE_CLASS` classifies `mailAssociation` as `presentation`: the mailbox service reads it, never `createColumn`, so a declaration on a builtin column name is not reported as discarded storage. No behaviour changes.
18+
- **`@objectstack/metadata-core`.** `FIELD_REFERENCE_POSITIONS` classifies `mailAssociation` as `keep`: the FLS scrub of a served object definition passes the flag through unchanged, because it names no field.
19+
- **`@objectstack/platform-objects`.** The field designer's new row carries its label and help text in the metadata-form translation bundles for en, zh-CN, ja-JP and es-ES.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const result = CurrencyConfigSchema.parse(data);
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. |
7979
| **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. |
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. |
81+
| **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. |
8182
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
8283
| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |
8384
| **inlineTitle** | `string` | optional | Title for the inline master-detail grid |

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ const result = ApiMethod.parse(data);
243243
| **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. |
244244
| **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. |
245245
| **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. |
246+
| **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. |
246247
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
247248
| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |
248249
| **inlineTitle** | `string` | optional | Title for the inline master-detail grid |
@@ -584,6 +585,7 @@ const result = ApiMethod.parse(data);
584585
| **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. |
585586
| **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. |
586587
| **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. |
588+
| **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. |
587589
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
588590
| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |
589591
| **inlineTitle** | `string` | optional | Title for the inline master-detail grid |

‎content/docs/references/system/migration.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Add a new field to an existing object
7979
| **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. |
8080
| **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. |
8181
| **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. |
82+
| **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. |
8283
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
8384
| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |
8485
| **inlineTitle** | `string` | optional | Title for the inline master-detail grid |
@@ -505,6 +506,7 @@ Add a new field to an existing object
505506
| **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. |
506507
| **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. |
507508
| **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. |
509+
| **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. |
508510
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
509511
| **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |
510512
| **inlineTitle** | `string` | optional | Title for the inline master-detail grid |

‎packages/drivers/driver-sql/src/builtin-column-collision.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export const FIELD_KEY_STORAGE_CLASS: Readonly<Record<string, FieldKeyClass>> =
123123
dueLike: 'presentation', // declares a date a deadline (#22227) — read by the renderer's date cells, never by `createColumn`: the column is the date the type already sizes
124124
settledWhen: 'presentation', // the deadline's settle predicate (#22227), display only like `visibleWhen` — no DDL reads it
125125
conditionalFormatting: 'presentation', // a field's cell formatting rules (#22228), display only — no DDL reads it
126+
mailAssociation: 'presentation', // marks an email field as a correspondent address for inbound-mail matching (#22870) — read by the mailbox service, never by `createColumn`: the column is the email string the type already sizes
126127
requiredPermissions: 'presentation',
127128
maskingRule: 'presentation',
128129
ackPlaintextMasking: 'presentation',

‎packages/lint/src/lint-liveness-properties.test.ts‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,17 +1611,22 @@ const fieldLedgerDir = () => ledgerDirWith((dir) => writeLedger(dir, 'field', SY
16111611

16121612
describe('the object/field walk, against a synthetic ledger directory (#19268)', () => {
16131613
// The state that makes this block necessary, asserted rather than recalled.
1614-
// If `field.json` ever warns again these two flip, and the block above
1615-
// ("field walk: a malformed `fields` array …") can take its real subject back
1616-
// — but this block keeps working either way, which is the point.
1614+
// If `field.json` ever warns on a STABLE subject again, the first assertion
1615+
// below names it, and the block above ("field walk: a malformed `fields`
1616+
// array …") can take its real subject back — but this block keeps working
1617+
// either way, which is the point.
16171618
it('the SHIPPED field ledger warns on no stable subject — which is why the walk needs a subject of its own', () => {
16181619
// `picklist` was the last opted-in row (`planned` + `authorWarn`) and left
16191620
// when the server began resolving picklist references (`live`). What is
16201621
// left is `conditionalRequired`, a `retiredKey` tombstone whose `dead` row
16211622
// warns on its own since #16094 — but a parse refuses it first, so it is no
1622-
// subject for #11385 either. The synthetic slot below is warned by nothing
1623-
// shipped.
1624-
expect([...authorWarnedProperties('field')]).toEqual(['conditionalRequired']);
1623+
// subject for #11385 either. `mailAssociation` (#22870) opted in as
1624+
// `planned` + `authorWarn` — the inbound-mail correspondent flag on an email
1625+
// field, declared ahead of the mailbox ingest that reads it — and it leaves
1626+
// this set when that reader lands and the row flips `live`: a verdict
1627+
// scheduled to move, so no stable subject either. The synthetic slot below
1628+
// is warned by nothing shipped.
1629+
expect([...authorWarnedProperties('field')]).toEqual(['conditionalRequired', 'mailAssociation']);
16251630
expect(
16261631
lintLivenessProperties({
16271632
objects: [{ name: 'widget', fields: [{ name: 'a', synthWarnedSlot: true }] }],

‎packages/lint/src/validate-predicate-path-refs.test.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,14 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
667667
// == true`, and removes none. Neither reaches the literal-comparison census
668668
// below: one is an `in`-list gate, and the other compares against the bare
669669
// `true`, which is not a quoted literal.
670-
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(87);
670+
// It is 88 today, an ADDITION of ONE: #22870 offered the field form's
671+
// `mailAssociation` row, a PARSE gate like the deadline pair (`FieldSchema`
672+
// refuses the key off `email`). Measured, not inferred: the shipped
673+
// `*.form.ts` corpus differenced against the merge base `018b077fb` by
674+
// `<form>::<field>::<source>`, 87 → 88, adds exactly
675+
// `field :: mailAssociation :: data.type == 'email'` and removes none. It
676+
// does reach the literal-comparison census below.
677+
expect(predicates, 'the shipped metadata forms carry no predicates at all').toBe(88);
671678

672679
const findings = validatePredicatePathRefs(corrupted);
673680
expect(findings).toHaveLength(predicates);
@@ -774,7 +781,9 @@ describe('#7010 corpus — shipped METADATA_FORM_REGISTRY', () => {
774781
// It is 64 today: #22727's `field :: currencyConfig` gate also opens for a
775782
// currency formula, `data.type == 'formula' && data.returnType == 'currency'`,
776783
// two more single-quoted literal comparisons.
777-
expect(comparisons, 'no shipped predicate carries an `==`/`!=` literal comparison').toBe(64);
784+
// It is 65 today: #22870's `field :: mailAssociation` row is gated on
785+
// `data.type == 'email'`, a single-quoted literal comparison.
786+
expect(comparisons, 'no shipped predicate carries an `==`/`!=` literal comparison').toBe(65);
778787

779788
const rhsFindings = validatePredicatePathRefs(corrupted)
780789
.filter((f) => f.rule === PREDICATE_RHS_PATH_SHAPED);

0 commit comments

Comments
 (0)