Repository navigation
Commit e5687cb
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
File tree
- .changeset
- content/docs/references
- data
- system
- packages
- drivers/driver-sql/src
- lint/src
- metadata-core/src
- metadata-protocol/src
- platform-objects/src/apps/translations
- spec
- authorable-surface
- liveness
- state-counts
- src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| 81 | + | |
81 | 82 | | |
82 | 83 | | |
83 | 84 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
| 246 | + | |
246 | 247 | | |
247 | 248 | | |
248 | 249 | | |
| |||
584 | 585 | | |
585 | 586 | | |
586 | 587 | | |
| 588 | + | |
587 | 589 | | |
588 | 590 | | |
589 | 591 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| 82 | + | |
82 | 83 | | |
83 | 84 | | |
84 | 85 | | |
| |||
505 | 506 | | |
506 | 507 | | |
507 | 508 | | |
| 509 | + | |
508 | 510 | | |
509 | 511 | | |
510 | 512 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
| 126 | + | |
126 | 127 | | |
127 | 128 | | |
128 | 129 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1611 | 1611 | | |
1612 | 1612 | | |
1613 | 1613 | | |
1614 | | - | |
1615 | | - | |
1616 | | - | |
| 1614 | + | |
| 1615 | + | |
| 1616 | + | |
| 1617 | + | |
1617 | 1618 | | |
1618 | 1619 | | |
1619 | 1620 | | |
1620 | 1621 | | |
1621 | 1622 | | |
1622 | | - | |
1623 | | - | |
1624 | | - | |
| 1623 | + | |
| 1624 | + | |
| 1625 | + | |
| 1626 | + | |
| 1627 | + | |
| 1628 | + | |
| 1629 | + | |
1625 | 1630 | | |
1626 | 1631 | | |
1627 | 1632 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
667 | 667 | | |
668 | 668 | | |
669 | 669 | | |
670 | | - | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
671 | 678 | | |
672 | 679 | | |
673 | 680 | | |
| |||
774 | 781 | | |
775 | 782 | | |
776 | 783 | | |
777 | | - | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
778 | 787 | | |
779 | 788 | | |
780 | 789 | | |
| |||
0 commit comments