Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .changeset/field-default-value-discriminator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
"@objectstack/spec": minor
---

fix(spec): `FieldSchema.defaultValue` is discriminated (literal / runtime token / CEL envelope) and each shape is validated on its own terms (#7127)

`FieldSchema.defaultValue` was `z.unknown().optional()` — the same acceptance
hole #6970 closed one layer up on action params, but NARROWER: a field default
is polymorphic by design (a literal, a runtime token `NOW()` / `current_user`,
or a CEL Expression envelope `{ dialect, source }`), so the vocabulary has to
be subtracted BEFORE the literal can be judged. Running the value contract
over the whole key judges a token's spelling as data — right only by accident
(`'current_user'` passes a `user` field as a would-be record id; `'NOW()'`
passes `text` as a plain string while the engine intercepts it and stores an
ISO instant instead).

Per the maintainer's sequenced ruling (2026-08-10), this lands in two steps
inside one release:

1. a shared **discriminator** (`@objectstack/spec/data`,
`default-value-shape.ts`): the engine's own envelope predicate verbatim,
the token predicates, and the shared literal-vs-stored-contract core —
one module, two consumers. The #6970 action-param gate is refactored onto
the shared core with zero behavior change.
2. `FieldSchema.defaultValue` narrowed on top of it, in the engine's own
discrimination order:
- **absent** (`null`/`undefined`) → skipped; `''` is a real default
(engine presence semantics, deliberately not the action-param rule);
- **CEL envelope** → structural acceptance only (the result type is
unknowable at parse time; a wrong one is an ADR-0032 runtime concern);
- **runtime token** → per-token × per-type: `NOW()` on
`datetime`/`date`/`time` (both resolvers and the docs already support
all three); `current_user` on `user` or `lookup` with
`reference: 'sys_user'` (#4560); no token on a multi-value field
(both resolve to one scalar);
- **literal** → the field's own stored value contract
(ADR-0104 D1 `valueSchemaFor(def, 'stored')`) — the #6970 mechanics one
layer down.

Rejections are prescriptive: they name the field, its type, the offending
value verbatim, why it cannot hold, and the legal alternatives — including a
suggested token for predictable near-miss spellings (`'now'`,
`'{current_user}'`), which are suggested but never silently widened into
tokens (a genuinely-intended literal must stay storable).

**Migration surface: zero.** All 371 shipped `defaultValue` declarations
across objectstack + cloud were re-censused against the implemented gate —
0 refusals (239 literals all pass their stored contracts; 131 × `NOW()` all
on `datetime`; 1 × `current_user` on `user`). The only declarations anywhere
that newly refuse are two hand-written docs samples that were already wrong
today (stored verbatim / dropped by the SQL DDL), fixed in this change.

**Stock compatibility.** As with #6970: stored metadata carrying a
nonconforming default keeps loading (the read path runs no Zod validation);
authoritative spec validation lives on the WRITE path and surfaces on reads
as the advisory `_diagnostics` envelope. Loud at authoring, non-fatal at
rest, no conversion owed — there is no mechanical rewrite for "the author
meant something else".

Also moved: `AddressSchema` is now declared in `field-value.zod.ts` (it IS
the enforced address value contract) and re-exported from `field.zod.ts`
unchanged — the move removes the one runtime ESM edge that would otherwise
have closed an evaluation cycle between the two modules.
23 changes: 20 additions & 3 deletions content/docs/references/data/field-value.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,30 @@ this contract has (ADR-0104 performance budget).
## TypeScript Usage

```typescript
import { AddressValueSchema, CalendarDateValueSchema, ClockTimeValueSchema, FileLikeValueSchema, FileReferenceIdValueSchema, FileValueSchema, InstantValueSchema, LocationValueSchema, ReferenceIdValueSchema } from '@objectstack/spec/data';
import type { AddressValue, CalendarDateValue, ClockTimeValue, FileLikeValue, FileReferenceIdValue, FileValue, InstantValue, LocationValue, ReferenceIdValue } from '@objectstack/spec/data';
import { AddressSchema, AddressValueSchema, CalendarDateValueSchema, ClockTimeValueSchema, FileLikeValueSchema, FileReferenceIdValueSchema, FileValueSchema, InstantValueSchema, LocationValueSchema, ReferenceIdValueSchema } from '@objectstack/spec/data';
import type { Address, AddressValue, CalendarDateValue, ClockTimeValue, FileLikeValue, FileReferenceIdValue, FileValue, InstantValue, LocationValue, ReferenceIdValue } from '@objectstack/spec/data';

// Validate data
const result = AddressValueSchema.parse(data);
const result = AddressSchema.parse(data);
```

---

## Address

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **street** | `string` | optional | Street address |
| **city** | `string` | optional | City name |
| **state** | `string` | optional | State/Province |
| **postalCode** | `string` | optional | Postal/ZIP code |
| **country** | `string` | optional | Country name or code |
| **countryCode** | `string` | optional | ISO country code (e.g., US, GB) |
| **formatted** | `string` | optional | Formatted address string |


---

## AddressValue
Expand Down
25 changes: 4 additions & 21 deletions content/docs/references/data/field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,13 @@ Field Type Enum
## TypeScript Usage

```typescript
import { AddressSchema, CurrencyConfigSchema, CurrencyValueSchema, FieldSchema, FieldType, LocationCoordinatesSchema, SelectOptionSchema, UniqueScopeSchema } from '@objectstack/spec/data';
import type { Address, CurrencyConfig, CurrencyValue, Field, FieldType, LocationCoordinates, SelectOption, UniqueScope } from '@objectstack/spec/data';
import { CurrencyConfigSchema, CurrencyValueSchema, FieldSchema, FieldType, LocationCoordinatesSchema, SelectOptionSchema, UniqueScopeSchema } from '@objectstack/spec/data';
import type { CurrencyConfig, CurrencyValue, Field, FieldType, LocationCoordinates, SelectOption, UniqueScope } from '@objectstack/spec/data';

// Validate data
const result = AddressSchema.parse(data);
const result = CurrencyConfigSchema.parse(data);
```

---

## Address

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **street** | `string` | optional | Street address |
| **city** | `string` | optional | City name |
| **state** | `string` | optional | State/Province |
| **postalCode** | `string` | optional | Postal/ZIP code |
| **country** | `string` | optional | Country name or code |
| **countryCode** | `string` | optional | ISO country code (e.g., US, GB) |
| **formatted** | `string` | optional | Formatted address string |


---

## CurrencyConfig
Expand Down Expand Up @@ -81,7 +64,7 @@ const result = AddressSchema.parse(data);
| **searchable** | `boolean` | optional | Is searchable |
| **multiple** | `boolean` | optional | Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. |
| **unique** | `boolean \| 'global' \| 'organization'` | optional | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization' |
| **defaultValue** | `any` | optional | Default value |
| **defaultValue** | `any` | optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes (#7127), discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. |
| **maxLength** | `number` | optional | Max character length |
| **minLength** | `number` | optional | Min character length |
| **precision** | `number` | optional | Total digits |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ Objects, fields, queries, filters, datasources and drivers — the ObjectQL laye
| [`external-catalog.zod.ts`](/docs/references/data/external-catalog) | `ExternalCatalog`, `ExternalColumn`, `ExternalTable` |
| [`external-lookup.zod.ts`](/docs/references/data/external-lookup) | `ExternalDataSource`, `ExternalFieldMapping`, `ExternalLookup` |
| [`feed.zod.ts`](/docs/references/data/feed) | `FeedFilterMode`, `FeedItemType` |
| [`field.zod.ts`](/docs/references/data/field) | `Address`, `CurrencyConfig`, `CurrencyValue`, `Field`, `FieldType`, `LocationCoordinates`, `SelectOption`, `UniqueScope` |
| [`field-value.zod.ts`](/docs/references/data/field-value) | `AddressValue`, `CalendarDateValue`, `ClockTimeValue`, `FileLikeValue`, `FileReferenceIdValue`, `FileValue`, `InstantValue`, `LocationValue`, `ReferenceIdValue` |
| [`field.zod.ts`](/docs/references/data/field) | `CurrencyConfig`, `CurrencyValue`, `Field`, `FieldType`, `LocationCoordinates`, `SelectOption`, `UniqueScope` |
| [`field-value.zod.ts`](/docs/references/data/field-value) | `Address`, `AddressValue`, `CalendarDateValue`, `ClockTimeValue`, `FileLikeValue`, `FileReferenceIdValue`, `FileValue`, `InstantValue`, `LocationValue`, `ReferenceIdValue` |
| [`filter.zod.ts`](/docs/references/data/filter) | `EqualityOperator`, `FieldReference`, `FilterArray`, `FilterCondition`, `QueryFilter`, `SetOperator`, `SpecialOperator`, `StringOperator` |
| [`hook.zod.ts`](/docs/references/data/hook) | `HookContext`, `HookEvent` |
| [`hook-body.zod.ts`](/docs/references/data/hook-body) | `ExpressionBody`, `HookBody`, `HookBodyCapability`, `ScriptBody` |
Expand Down
16 changes: 8 additions & 8 deletions docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ Remaining strip sites by class:

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 41 |
| unresolved — needs a per-schema verdict | 33 |
| authorable — the ruling's forced scope | 40 |
| unresolved — needs a per-schema verdict | 34 |
| wire / open — out of forced scope | 105 |
| no door — no carrier, ADR-0049 territory | 1 |
| no gate — carrier live, no parse | 0 |
Expand Down Expand Up @@ -98,8 +98,8 @@ classify and is not listed (it becomes reportable the day it grows its first sit
| `driver/turso.zod.ts` | 2 |
| `external-catalog.zod.ts` | 4 |
| `external-lookup.zod.ts` | 12 |
| `field-value.zod.ts` | 2 |
| `field.zod.ts` | 11 |
| `field-value.zod.ts` | 3 |
| `field.zod.ts` | 10 |
| `filter.zod.ts` | 11 |
| `hook-body.zod.ts` | 2 |
| `hook.zod.ts` | 7 |
Expand Down Expand Up @@ -192,8 +192,8 @@ over it is here.
| `driver/memory.zod.ts` | 5 | 6 |
| `external-catalog.zod.ts` | 4 | 4 |
| `external-lookup.zod.ts` | 12 | 12 |
| `field-value.zod.ts` | 1 | 2 |
| `field.zod.ts` | 3 | 11 |
| `field-value.zod.ts` | 2 | 3 |
| `field.zod.ts` | 2 | 10 |
| `filter.zod.ts` | 11 | 11 |
| `hook.zod.ts` | 5 | 7 |
| `object.zod.ts` | 1 | 20 |
Expand All @@ -203,8 +203,8 @@ over it is here.

| Bucket | Sites |
|---|---|
| authorable — the ruling's forced scope | 9 |
| unresolved — needs a per-schema verdict | 33 |
| authorable — the ruling's forced scope | 8 |
| unresolved — needs a per-schema verdict | 34 |
| wire / open — out of forced scope | 66 |
| no door — no carrier, ADR-0049 territory | 0 |
| no gate — carrier live, no parse | 0 |
Expand Down
9 changes: 9 additions & 0 deletions packages/spec/api-surface/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"DEFAULT_VALUE_TOKEN_CURRENT_USER (const)",
"DEFAULT_VALUE_TOKEN_DESCRIPTIONS (const)",
"DEFAULT_VALUE_TOKEN_NOW (const)",
"DEFAULT_VALUE_TOKEN_SUGGESTIONS (const)",
"DRIVER_CONFIG_SCHEMAS (const)",
"DRIVER_ID_ALIASES (const)",
"DataEngineAggregateOptions (type)",
Expand Down Expand Up @@ -155,6 +156,7 @@
"DateMacroToken (type)",
"DateMacroTokenSchema (const)",
"DateMacroUnit (type)",
"DefaultValueShape (type)",
"DefaultValueToken (type)",
"Dimension (type)",
"DimensionSchema (const)",
Expand Down Expand Up @@ -327,6 +329,7 @@
"LifecycleClass (type)",
"LifecycleClassSchema (const)",
"LifecycleSchema (const)",
"LiteralDefaultValueVerdict (interface)",
"LocalStoragePersistenceConfig (type)",
"LocalStoragePersistenceConfigSchema (const)",
"LocationCoordinates (type)",
Expand Down Expand Up @@ -357,6 +360,7 @@
"MysqlConfig (type)",
"MysqlConfigParsed (type)",
"MysqlConfigSchema (const)",
"NOW_DEFAULT_LEGAL_TYPES (const)",
"NUMERIC_VALUE_TYPES (const)",
"NoSQLDataTypeMapping (type)",
"NoSQLDataTypeMappingSchema (const)",
Expand Down Expand Up @@ -593,9 +597,11 @@
"asciiCaseInsensitiveRegexSource (function)",
"canonicalAstOperator (function)",
"canonicalizeSqlType (function)",
"checkLiteralDefaultValue (function)",
"classifyFilterToken (function)",
"countAuthorableFields (function)",
"defaultAggregateFor (function)",
"defaultValueTokenIssue (function)",
"defineCube (function)",
"defineDatasource (function)",
"defineHook (function)",
Expand All @@ -605,6 +611,7 @@
"deriveFieldGroupLayout (function)",
"deriveRecordFlowSurface (function)",
"deriveRecordSurface (function)",
"discriminateDefaultValueShape (function)",
"driverConfigJsonSchema (function)",
"driverHasLocalDefault (function)",
"effectiveOperationsArray (function)",
Expand All @@ -631,6 +638,7 @@
"isContextToken (function)",
"isCurrentUserDefaultToken (function)",
"isDateMacroToken (function)",
"isExpressionEnvelopeDefault (function)",
"isFileIdToken (function)",
"isFilterAST (function)",
"isGlobalUnique (function)",
Expand Down Expand Up @@ -675,6 +683,7 @@
"resolveSearchFields (function)",
"sequenceWidth (function)",
"stripLegacyApiMethods (function)",
"suggestDefaultValueToken (function)",
"suggestFieldTypeForSqlType (function)",
"utcInstantMs (function)",
"validateDriverConfig (function)",
Expand Down
11 changes: 10 additions & 1 deletion packages/spec/export-origins/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"API_PRIMITIVES": "src/data/api-derivation.ts#API_PRIMITIVES (const)",
"AUDIT_PROVENANCE_FIELDS": "src/data/field-group-layout.ts#AUDIT_PROVENANCE_FIELDS (const)",
"Address": "src/data/field.zod.ts#Address (type)",
"AddressSchema": "src/data/field.zod.ts#AddressSchema (const)",
"AddressSchema": "src/data/field-value.zod.ts#AddressSchema (const)",
"AddressValue": "src/data/field-value.zod.ts#AddressValue (type)",
"AddressValueSchema": "src/data/field-value.zod.ts#AddressValueSchema (const)",
"AggregationCase": "src/data/aggregation-conformance.ts#AggregationCase (interface)",
Expand Down Expand Up @@ -105,6 +105,7 @@
"DEFAULT_VALUE_TOKEN_CURRENT_USER": "src/data/default-value-tokens.ts#DEFAULT_VALUE_TOKEN_CURRENT_USER (const)",
"DEFAULT_VALUE_TOKEN_DESCRIPTIONS": "src/data/default-value-tokens.ts#DEFAULT_VALUE_TOKEN_DESCRIPTIONS (const)",
"DEFAULT_VALUE_TOKEN_NOW": "src/data/default-value-tokens.ts#DEFAULT_VALUE_TOKEN_NOW (const)",
"DEFAULT_VALUE_TOKEN_SUGGESTIONS": "src/data/default-value-shape.ts#DEFAULT_VALUE_TOKEN_SUGGESTIONS (const)",
"DRIVER_CONFIG_SCHEMAS": "src/data/driver/config-registry.zod.ts#DRIVER_CONFIG_SCHEMAS (const)",
"DRIVER_ID_ALIASES": "src/data/driver/config-registry.zod.ts#DRIVER_ID_ALIASES (const)",
"DataEngineAggregateOptions": "src/data/data-engine.zod.ts#DataEngineAggregateOptions (type)",
Expand Down Expand Up @@ -155,6 +156,7 @@
"DateMacroToken": "src/data/date-macros.zod.ts#DateMacroToken (type)",
"DateMacroTokenSchema": "src/data/date-macros.zod.ts#DateMacroTokenSchema (const)",
"DateMacroUnit": "src/data/date-macros.zod.ts#DateMacroUnit (type)",
"DefaultValueShape": "src/data/default-value-shape.ts#DefaultValueShape (type)",
"DefaultValueToken": "src/data/default-value-tokens.ts#DefaultValueToken (type)",
"Dimension": "src/data/analytics.zod.ts#Dimension (type)",
"DimensionSchema": "src/data/analytics.zod.ts#DimensionSchema (const)",
Expand Down Expand Up @@ -327,6 +329,7 @@
"LifecycleClass": "src/data/object.zod.ts#LifecycleClass (type)",
"LifecycleClassSchema": "src/data/object.zod.ts#LifecycleClassSchema (const)",
"LifecycleSchema": "src/data/object.zod.ts#LifecycleSchema (const)",
"LiteralDefaultValueVerdict": "src/data/default-value-shape.ts#LiteralDefaultValueVerdict (interface)",
"LocalStoragePersistenceConfig": "src/data/driver/memory.zod.ts#LocalStoragePersistenceConfig (type)",
"LocalStoragePersistenceConfigSchema": "src/data/driver/memory.zod.ts#LocalStoragePersistenceConfigSchema (const)",
"LocationCoordinates": "src/data/field.zod.ts#LocationCoordinates (type)",
Expand Down Expand Up @@ -357,6 +360,7 @@
"MysqlConfig": "src/data/driver/mysql.zod.ts#MysqlConfig (type)",
"MysqlConfigParsed": "src/data/driver/mysql.zod.ts#MysqlConfigParsed (type)",
"MysqlConfigSchema": "src/data/driver/mysql.zod.ts#MysqlConfigSchema (const)",
"NOW_DEFAULT_LEGAL_TYPES": "src/data/default-value-shape.ts#NOW_DEFAULT_LEGAL_TYPES (const)",
"NUMERIC_VALUE_TYPES": "src/data/field-value.zod.ts#NUMERIC_VALUE_TYPES (const)",
"NoSQLDataTypeMapping": "src/data/driver-nosql.zod.ts#NoSQLDataTypeMapping (type)",
"NoSQLDataTypeMappingSchema": "src/data/driver-nosql.zod.ts#NoSQLDataTypeMappingSchema (const)",
Expand Down Expand Up @@ -593,9 +597,11 @@
"asciiCaseInsensitiveRegexSource": "src/data/filter.zod.ts#asciiCaseInsensitiveRegexSource (function)",
"canonicalAstOperator": "src/data/filter.zod.ts#canonicalAstOperator (function)",
"canonicalizeSqlType": "src/data/type-compat.ts#canonicalizeSqlType (function)",
"checkLiteralDefaultValue": "src/data/default-value-shape.ts#checkLiteralDefaultValue (function)",
"classifyFilterToken": "src/data/context-tokens.zod.ts#classifyFilterToken (function)",
"countAuthorableFields": "src/data/record-surface.ts#countAuthorableFields (function)",
"defaultAggregateFor": "src/data/aggregation-policy.ts#defaultAggregateFor (function)",
"defaultValueTokenIssue": "src/data/default-value-shape.ts#defaultValueTokenIssue (function)",
"defineCube": "src/data/analytics.zod.ts#defineCube (function)",
"defineDatasource": "src/data/datasource.zod.ts#defineDatasource (function)",
"defineHook": "src/data/hook.zod.ts#defineHook (function)",
Expand All @@ -605,6 +611,7 @@
"deriveFieldGroupLayout": "src/data/field-group-layout.ts#deriveFieldGroupLayout (function)",
"deriveRecordFlowSurface": "src/data/record-surface.ts#deriveRecordFlowSurface (function)",
"deriveRecordSurface": "src/data/record-surface.ts#deriveRecordSurface (function)",
"discriminateDefaultValueShape": "src/data/default-value-shape.ts#discriminateDefaultValueShape (function)",
"driverConfigJsonSchema": "src/data/driver/common.zod.ts#driverConfigJsonSchema (function)",
"driverHasLocalDefault": "src/data/driver/config-registry.zod.ts#driverHasLocalDefault (function)",
"effectiveOperationsArray": "src/data/api-derivation.ts#effectiveOperationsArray (function)",
Expand All @@ -631,6 +638,7 @@
"isContextToken": "src/data/context-tokens.zod.ts#isContextToken (function)",
"isCurrentUserDefaultToken": "src/data/default-value-tokens.ts#isCurrentUserDefaultToken (function)",
"isDateMacroToken": "src/data/date-macros.zod.ts#isDateMacroToken (function)",
"isExpressionEnvelopeDefault": "src/data/default-value-shape.ts#isExpressionEnvelopeDefault (function)",
"isFileIdToken": "src/data/field-value.zod.ts#isFileIdToken (function)",
"isFilterAST": "src/data/filter.zod.ts#isFilterAST (function)",
"isGlobalUnique": "src/data/field.zod.ts#isGlobalUnique (function)",
Expand Down Expand Up @@ -675,6 +683,7 @@
"resolveSearchFields": "src/data/search-fields.ts#resolveSearchFields (function)",
"sequenceWidth": "src/data/autonumber-format.ts#sequenceWidth (function)",
"stripLegacyApiMethods": "src/data/object.zod.ts#stripLegacyApiMethods (function)",
"suggestDefaultValueToken": "src/data/default-value-shape.ts#suggestDefaultValueToken (function)",
"suggestFieldTypeForSqlType": "src/data/type-compat.ts#suggestFieldTypeForSqlType (function)",
"utcInstantMs": "src/data/calendar-day.ts#utcInstantMs (function)",
"validateDriverConfig": "src/data/driver/config-registry.zod.ts#validateDriverConfig (function)",
Expand Down
Loading
Loading