From a7f801f6bf40aa4ce214e0b2753ef8f723d9a3cc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 00:26:16 +0000 Subject: [PATCH 1/6] fix(spec): describe `format` by the readers that exist, not by email/phone `FieldSchema.format` was described as `Format string (e.g. email, phone)`, published verbatim to `content/docs/references/data/field.mdx`. Nothing honours `email` or `phone` for this key. `email`, `url` and `phone` are field TYPES; the `email | url | phone | json` vocabulary belongs to a `format` VALIDATION RULE, one schema over. What actually reads the key, measured: - `resolveAutonumberFormat` (`data/autonumber-format.ts`), called by the ObjectQL engine's `applyAutonumbers` and twice by driver-sql: on an `autonumber` field this is the record-number pattern, the shorthand that predates `autonumberFormat`. So an author following the old sentence and writing `format: 'email'` gets a pattern with no `{...}` token, which renders as literal text plus the bare counter: `email1`. - objectui's `DateCellRenderer` / `DateTimeCellRenderer` at the pinned `.objectui-sha`: a display style, with a different vocabulary and a different default per type. The new description names both readings, says the spec declares no vocabulary and validates nothing, and redirects value constraints to the field `type` and to the `format` validation rule. Scope is the sentence: no key is split, narrowed or retired, and no consumer moves. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/spec/src/data/field.zod.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 062cfa568f5..7471a6aea3c 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -1087,7 +1087,10 @@ export const FieldSchema = lazySchema(() => { label: z.string().optional().describe('Human readable label'), type: FieldType.describe('Field Data Type'), description: z.string().optional().describe('Tooltip/Help text'), - format: z.string().optional().describe('Format string (e.g. email, phone)'), + format: z.string().optional().describe('Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an ' + + '`autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: \'INV-{0000}\'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: \'email\'` mints `email1`). Prefer `autonumberFormat` on a new field. ' + + 'On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. ' + + 'Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` | `url` | `phone` | `json`.'), // `columnName` removed in the 16.x line (#2377, ADR-0049): the SQL driver // hardcodes the physical column = field key (createColumn never reads it), so From 09c1fbfdb7b310b8fae6c7ee2edc37cef8e40751 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 00:30:35 +0000 Subject: [PATCH 2/6] docs(spec): regenerate the references tree for the `format` description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pnpm --filter @objectstack/spec gen:docs`, from a `dist` this branch built. Three projections of one describe string move: `data/field.mdx`, `data/object.mdx` (twice) and `system/migration.mdx` (twice). `check:authorable-surface` stayed green across the edit, so `authorable-surface/data.json` and its `.base.json` anchor do not move — a description is not an authorable key. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- content/docs/references/data/field.mdx | 2 +- content/docs/references/data/object.mdx | 4 ++-- content/docs/references/system/migration.mdx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index dde28fdd5bd..5e4ae5aa9d4 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -56,7 +56,7 @@ const result = CurrencyConfigSchema.parse(data); | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Format string (e.g. email, phone) | +| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 870413c9837..9d49bd2ee20 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -218,7 +218,7 @@ const result = ApiMethod.parse(data); | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Format string (e.g. email, phone) | +| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | @@ -551,7 +551,7 @@ const result = ApiMethod.parse(data); | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Format string (e.g. email, phone) | +| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index 871727a6ab6..f2dc5690520 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -56,7 +56,7 @@ Add a new field to an existing object | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Format string (e.g. email, phone) | +| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | @@ -476,7 +476,7 @@ Add a new field to an existing object | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Format string (e.g. email, phone) | +| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | From 46aa4b4e87b6ed2451871055e5eab26ae5171f55 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 00:42:22 +0000 Subject: [PATCH 3/6] chore(changeset): patch `@objectstack/spec` for the `format` description repair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured rather than assumed: the changed bytes reach three entries of this package's own `files[]`. `packages/spec/src/data/field.zod.ts` is itself shipped (`src/**/*.zod.ts`), and the new text greps out of 22 `dist/` files and 13 `json-schema/` files — the same counts a positive control from the same schema (`required`'s published describe) returns over the same two trees. `skip-changeset` would have been wrong. `Clause-②: no`: the key stays `z.string().optional()` and no accept set moves in either direction. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../19679-format-describe-vocabulary.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .changeset/19679-format-describe-vocabulary.md diff --git a/.changeset/19679-format-describe-vocabulary.md b/.changeset/19679-format-describe-vocabulary.md new file mode 100644 index 00000000000..601a199b3b4 --- /dev/null +++ b/.changeset/19679-format-describe-vocabulary.md @@ -0,0 +1,19 @@ +--- +'@objectstack/spec': patch +--- + +**Fix:** `FieldSchema.format`'s description named a vocabulary nothing honours. `Format string (e.g. email, phone)` shipped in the JSON Schema, in `dist`, in the published `src/**/*.zod.ts` and verbatim to a customer in `content/docs/references/data/field.mdx` — and neither `email` nor `phone` is read by anything, on any field type. + +`Clause-②: no` — the key is still `z.string().optional()`. Nothing is split, narrowed, retired or gated by type; no accept set moves in either direction and no consumer is touched. Only the sentence changes. + +**What actually reads the key, measured on this tree rather than recalled.** + +- On an `autonumber` field it is the record-number **pattern** — the shorthand that predates `autonumberFormat`. `resolveAutonumberFormat` takes the canonical key first, then this one, then the declared default `{0000}`, and the ObjectQL engine's `applyAutonumbers` and `driver-sql` both mint through it. Measured against this build: `format: 'INV-{0000}'` → `INV-0001`; `format: 'email'` → `email1` (a value carrying no `{...}` token is literal text with the bare counter appended); `{ autonumberFormat: 'A-{000}', format: 'email' }` → `A-{000}`. +- On a `date` or `datetime` field the Studio UI grid cell reads it as a display **style**, with its own words and its own per-type default, and drops an unrecognised word silently onto a default face. +- Nothing else. It never reaches storage, coercion or write-time validation. + +So the author who followed the old sentence onto an autonumber field got `email1` as a business identifier: it parsed, it stored, and nothing reported it. + +**Where `email` / `phone` really live**, now stated in the description because that redirect is the whole value of the repair: `email`, `url` and `phone` are field **types** (`type: 'email'`), and the closed `email | url | phone | json` vocabulary belongs to a **`format` validation rule** (`{ type: 'format', field, format: 'email' }`), one schema over. The description also now says outright that the spec declares no vocabulary for this key and validates nothing, so a reader stops expecting a check that does not exist. + +The wider question — one `z.string()` key carrying two unrelated vocabularies with two different defaults — is a contract-shape decision and is deliberately untouched here. From 66cb68d947e101673ae8273acc44b4ff6ecf1dae Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 01:32:08 +0000 Subject: [PATCH 4/6] fix(spec): state only what each reader of `format` does, not that nothing else reads it The round-1 description said two consumers read the key and nothing else does, and that email/url/phone are field types rather than formats. At the pinned objectui the shared cell-renderer resolver promotes a plain-text field by a small word set, so both absolutes were false. The description now makes positive statements only: the autonumber pattern arm and its precedence, the server acting on the key for that arm alone, the Studio UI reading it as a display hint whose words its renderers own (the date/datetime style and the plain-text renderer hint as examples, no word list copied), and value checks keyed on `type`. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/spec/src/data/field.zod.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 7471a6aea3c..cc962542842 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -1087,10 +1087,11 @@ export const FieldSchema = lazySchema(() => { label: z.string().optional().describe('Human readable label'), type: FieldType.describe('Field Data Type'), description: z.string().optional().describe('Tooltip/Help text'), - format: z.string().optional().describe('Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an ' - + '`autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: \'INV-{0000}\'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: \'email\'` mints `email1`). Prefer `autonumberFormat` on a new field. ' - + 'On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. ' - + 'Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` | `url` | `phone` | `json`.'), + format: z.string().optional().describe('Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string, so any string parses on any field type. ' + + 'On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: \'INV-{0000}\'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: \'email\'` mints `email1`). Prefer `autonumberFormat` on a new field. ' + + 'On any other field type the server does not act on it: it picks no column type, coerces no value and runs no check from it. ' + + 'The Studio UI reads it as a display hint, in words and with defaults that its renderers own and declare. For example, the `date` and `datetime` cells read it as a display STYLE, and on a plain-text field the shared cell-renderer resolver reads a small set of words that promote the cell to a richer renderer, such as a link; each of those falls back silently to a default rendering when it does not recognise the word. ' + + 'To constrain a VALUE, use the field `type` (the write-time record validator\'s built-in email, url and phone checks key on `type`, never on this key) or a `format` validation rule, whose own `format` key is the closed set `email` | `url` | `phone` | `json`.'), // `columnName` removed in the 16.x line (#2377, ADR-0049): the SQL driver // hardcodes the physical column = field key (createColumn never reads it), so From 875c198d2c8a90e641a670c850b43346c56b83b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 01:37:43 +0000 Subject: [PATCH 5/6] docs(spec): regenerate the references tree for the round-2 `format` description Produced by `gen:schema` then `gen:docs` from the committed `describe`; nothing under content/docs/references/ is hand-edited. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- content/docs/references/data/field.mdx | 2 +- content/docs/references/data/object.mdx | 4 ++-- content/docs/references/system/migration.mdx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 5e4ae5aa9d4..31bcd42c6a5 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -56,7 +56,7 @@ const result = CurrencyConfigSchema.parse(data); | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | +| **format** | `string` | optional | Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string, so any string parses on any field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On any other field type the server does not act on it: it picks no column type, coerces no value and runs no check from it. The Studio UI reads it as a display hint, in words and with defaults that its renderers own and declare. For example, the `date` and `datetime` cells read it as a display STYLE, and on a plain-text field the shared cell-renderer resolver reads a small set of words that promote the cell to a richer renderer, such as a link; each of those falls back silently to a default rendering when it does not recognise the word. To constrain a VALUE, use the field `type` (the write-time record validator's built-in email, url and phone checks key on `type`, never on this key) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 9d49bd2ee20..13ae54574bb 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -218,7 +218,7 @@ const result = ApiMethod.parse(data); | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | +| **format** | `string` | optional | Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string, so any string parses on any field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On any other field type the server does not act on it: it picks no column type, coerces no value and runs no check from it. The Studio UI reads it as a display hint, in words and with defaults that its renderers own and declare. For example, the `date` and `datetime` cells read it as a display STYLE, and on a plain-text field the shared cell-renderer resolver reads a small set of words that promote the cell to a richer renderer, such as a link; each of those falls back silently to a default rendering when it does not recognise the word. To constrain a VALUE, use the field `type` (the write-time record validator's built-in email, url and phone checks key on `type`, never on this key) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | @@ -551,7 +551,7 @@ const result = ApiMethod.parse(data); | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | +| **format** | `string` | optional | Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string, so any string parses on any field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On any other field type the server does not act on it: it picks no column type, coerces no value and runs no check from it. The Studio UI reads it as a display hint, in words and with defaults that its renderers own and declare. For example, the `date` and `datetime` cells read it as a display STYLE, and on a plain-text field the shared cell-renderer resolver reads a small set of words that promote the cell to a richer renderer, such as a link; each of those falls back silently to a default rendering when it does not recognise the word. To constrain a VALUE, use the field `type` (the write-time record validator's built-in email, url and phone checks key on `type`, never on this key) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index f2dc5690520..6e63be89b54 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -56,7 +56,7 @@ Add a new field to an existing object | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | +| **format** | `string` | optional | Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string, so any string parses on any field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On any other field type the server does not act on it: it picks no column type, coerces no value and runs no check from it. The Studio UI reads it as a display hint, in words and with defaults that its renderers own and declare. For example, the `date` and `datetime` cells read it as a display STYLE, and on a plain-text field the shared cell-renderer resolver reads a small set of words that promote the cell to a richer renderer, such as a link; each of those falls back silently to a default rendering when it does not recognise the word. To constrain a VALUE, use the field `type` (the write-time record validator's built-in email, url and phone checks key on `type`, never on this key) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | @@ -476,7 +476,7 @@ Add a new field to an existing object | **label** | `string` | optional | Human readable label | | **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | -| **format** | `string` | optional | Free-form presentation hint: the spec declares NO vocabulary for it, so any string parses on any field type and nothing validates the value. Two unrelated consumers read it, and which one applies depends on the field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On a `date` or `datetime` field the Studio UI grid cell reads it as a display STYLE instead; the style words and the per-type default belong to that renderer and are declared there, and a word it does not recognise is dropped silently onto a default face. Nothing else reads this key — it never affects storage, coercion or write-time validation. To constrain a VALUE, use the field `type` (`email`, `url` and `phone` are field TYPES, not formats) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | +| **format** | `string` | optional | Free-form string whose meaning depends on the field type and on the reader. The spec declares NO vocabulary for it and checks nothing but that it is a string, so any string parses on any field type. On an `autonumber` field it is the record-number PATTERN — the shorthand that predates `autonumberFormat`, which wins when both are present: `format: 'INV-{0000}'` mints `INV-0001` on both the query engine and the SQL driver, while a value carrying no `{...}` token is emitted as literal text with the bare counter appended (`format: 'email'` mints `email1`). Prefer `autonumberFormat` on a new field. On any other field type the server does not act on it: it picks no column type, coerces no value and runs no check from it. The Studio UI reads it as a display hint, in words and with defaults that its renderers own and declare. For example, the `date` and `datetime` cells read it as a display STYLE, and on a plain-text field the shared cell-renderer resolver reads a small set of words that promote the cell to a richer renderer, such as a link; each of those falls back silently to a default rendering when it does not recognise the word. To constrain a VALUE, use the field `type` (the write-time record validator's built-in email, url and phone checks key on `type`, never on this key) or a `format` validation rule, whose own `format` key is the closed set `email` \| `url` \| `phone` \| `json`. | | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it (maintainer ruling 2026-08-18). NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Is searchable | From a8c7f177510a6ab895aec57ecac218d7226c65ee Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 01:39:39 +0000 Subject: [PATCH 6/6] chore(changeset): restate the `format` repair reader by reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round-1 body said neither `email` nor `phone` is read by anything on any field type, and that nothing but two consumers reads the key. At the pinned objectui a plain-text field's cell renderer is promoted by those words, so the body now states what each reader does and drops the absolutes. `patch` and `Clause-②: no` are unchanged. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../19679-format-describe-vocabulary.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.changeset/19679-format-describe-vocabulary.md b/.changeset/19679-format-describe-vocabulary.md index 601a199b3b4..f86b919fc53 100644 --- a/.changeset/19679-format-describe-vocabulary.md +++ b/.changeset/19679-format-describe-vocabulary.md @@ -2,18 +2,17 @@ '@objectstack/spec': patch --- -**Fix:** `FieldSchema.format`'s description named a vocabulary nothing honours. `Format string (e.g. email, phone)` shipped in the JSON Schema, in `dist`, in the published `src/**/*.zod.ts` and verbatim to a customer in `content/docs/references/data/field.mdx` — and neither `email` nor `phone` is read by anything, on any field type. +**Fix:** `FieldSchema.format`'s description said only `Format string (e.g. email, phone)`. It offered two example words without saying which field type they apply to or what reads them, and it shipped in the JSON Schema, in `dist`, in the published `src/**/*.zod.ts` and in `content/docs/references/data/field.mdx`. Followed onto an `autonumber` field, it produced `email1` as a business identifier. The value parsed, it was stored, and nothing reported it. -`Clause-②: no` — the key is still `z.string().optional()`. Nothing is split, narrowed, retired or gated by type; no accept set moves in either direction and no consumer is touched. Only the sentence changes. +`Clause-②: no`: the key is still `z.string().optional()`. Nothing is split, narrowed, retired or gated by type. No accept set moves in either direction, and no consumer is touched. Only the sentence changes. -**What actually reads the key, measured on this tree rather than recalled.** +**What the description now says, reader by reader.** Each point was measured, not recalled, and each is stated as what a reader does rather than as a claim that nothing else reads the key. -- On an `autonumber` field it is the record-number **pattern** — the shorthand that predates `autonumberFormat`. `resolveAutonumberFormat` takes the canonical key first, then this one, then the declared default `{0000}`, and the ObjectQL engine's `applyAutonumbers` and `driver-sql` both mint through it. Measured against this build: `format: 'INV-{0000}'` → `INV-0001`; `format: 'email'` → `email1` (a value carrying no `{...}` token is literal text with the bare counter appended); `{ autonumberFormat: 'A-{000}', format: 'email' }` → `A-{000}`. -- On a `date` or `datetime` field the Studio UI grid cell reads it as a display **style**, with its own words and its own per-type default, and drops an unrecognised word silently onto a default face. -- Nothing else. It never reaches storage, coercion or write-time validation. +- On an `autonumber` field the key is the record-number **pattern**, the shorthand that predates `autonumberFormat`. `resolveAutonumberFormat` takes the canonical key first, then this one, then the declared default `{0000}`. The ObjectQL engine's `applyAutonumbers` and `driver-sql` both mint through it, and the build-time autonumber lint in `@objectstack/lint` reads the same pattern. Measured against this build: `format: 'INV-{0000}'` gives `INV-0001`; `format: 'email'` gives `email1`, because a value with no `{...}` token is literal text with the bare counter appended; `{ autonumberFormat: 'A-{000}', format: 'email' }` gives `A-001`. +- On any other field type the server does not act on the key. It picks no column type from it, coerces no value by it and runs no check from it. The write-time record validator's built-in email, url and phone checks key on the field `type`. +- The Studio UI reads the key as a display hint, using words and defaults that its renderers own. The description names two examples. The `date` and `datetime` cells read a display style. On a plain-text field, the shared cell-renderer resolver reads a small word set that promotes the cell to a richer renderer; at the pinned objectui, `{ type: 'text', format: 'phone' }` renders a `tel:` link. The words themselves are deliberately not copied into the spec. They belong to those renderers, and a copy here would go stale without anything going red. +- The spec declares no vocabulary for the key and checks nothing except that it is a string, so any string parses on any field type. -So the author who followed the old sentence onto an autonumber field got `email1` as a business identifier: it parsed, it stored, and nothing reported it. +To constrain a **value**, the description points to the field `type` or to a `format` validation rule (`{ type: 'format', field, format: 'email' }`), whose own `format` key is the closed set `email | url | phone | json`. -**Where `email` / `phone` really live**, now stated in the description because that redirect is the whole value of the repair: `email`, `url` and `phone` are field **types** (`type: 'email'`), and the closed `email | url | phone | json` vocabulary belongs to a **`format` validation rule** (`{ type: 'format', field, format: 'email' }`), one schema over. The description also now says outright that the spec declares no vocabulary for this key and validates nothing, so a reader stops expecting a check that does not exist. - -The wider question — one `z.string()` key carrying two unrelated vocabularies with two different defaults — is a contract-shape decision and is deliberately untouched here. +The wider question is deliberately left alone here. One `z.string()` key is read differently by different readers, and nothing checks that they agree. Whether any of those readings should become a declared vocabulary is a contract-shape decision.