|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec): refuse undeclared keys on `address` and `location` values — `AddressSchema` / `LocationValueSchema` are strict (#13802) |
| 6 | + |
| 7 | +<!-- adr-0087: registered address-location-value-unknown-keys-refused --> |
| 8 | + |
| 9 | +**BREAKING** accept-set narrowing on two ADR-0104 D1 value contracts, shipped |
| 10 | +as `minor` under the repo's launch-window convention for breaking changes; the |
| 11 | +migration prescription is registered under protocol major 18. Maintainer |
| 12 | +ruling 2026-09-01 on #13802 (director decision batch #26, verbatim 「同意」): |
| 13 | +option A. |
| 14 | + |
| 15 | +`LocationValueSchema` and `AddressSchema` (`AddressValueSchema` is the same |
| 16 | +schema) were all-optional **stripping** `z.object`s. Every member being |
| 17 | +optional meant a value with a completely wrong key set still parsed green, |
| 18 | +and the wrong keys vanished from the parse output — the showcase seed wrote |
| 19 | +`postal_code`, the platform accepted it, dropped it, and rendered an empty ZIP |
| 20 | +box (#13388), while a stored-value scan over either class could only ever |
| 21 | +report a clean count it had no way to earn. Both are now `strictObject`s. |
| 22 | +`FileValueSchema` stays `z.looseObject` — the one deliberate loose site, |
| 23 | +untouched. |
| 24 | + |
| 25 | +**What is refused:** any key the shape does not declare, with a prescriptive |
| 26 | +message naming the surface, the key, and a rename where one is known |
| 27 | +(`postal_code` / `zipCode` / `zip` / `postcode` → `postalCode`; |
| 28 | +`latitude` → `lat`, `longitude` → `lng`). The zod issue is |
| 29 | +`unrecognized_keys` and its `keys` name the offending spellings. |
| 30 | + |
| 31 | +**What stays accepted:** every declared key byte-identically — |
| 32 | +`street`, `city`, `state`, `postalCode`, `country`, `countryCode`, `formatted` |
| 33 | +on an address; `lat`, `lng`, `altitude`, `accuracy` on a location. |
| 34 | + |
| 35 | +**Where the refusal bites — and where it deliberately does not** (the |
| 36 | +ADR-0104 posture is unchanged; this changeset narrows the contract, not the |
| 37 | +write path's evidence gate): |
| 38 | + |
| 39 | +- **Authoring, hard reject, unconditional:** a `location` / `address` field's |
| 40 | + literal `defaultValue` (`FieldSchema`, #7127) and an action param of those |
| 41 | + types (`validateActionParams`, strict by default since 17.0). |
| 42 | +- **Record writes, per deployment:** objectql's `validateRecord` rejects the |
| 43 | + value (`400 VALIDATION_FAILED`, field code `invalid_type`, message naming |
| 44 | + the key) **only** on a deployment that has attested `adr-0104-value-shapes` |
| 45 | + or set `OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1` (`OS_ALLOW_LAX_VALUE_SHAPES=1` |
| 46 | + re-opens). Everywhere else the write is **admitted** warn-first, logged once |
| 47 | + per field, and reported to the admitted-violation sink — exactly as before. |
| 48 | +- **`os migrate value-shapes`** now counts an undeclared key as a violation, |
| 49 | + so a deployment holding such values cannot attest until they are cleaned at |
| 50 | + the producer. That scan is what keeps the strict flip from stranding stored |
| 51 | + data. |
| 52 | +- **Read paths: none.** No consumer parses these shapes on read; a stored |
| 53 | + `{ …, postal_code }` reads back as it was written. No read path was |
| 54 | + narrowed, and no consumer-side alias is introduced — `postal_code` is |
| 55 | + refused, never read. |
| 56 | + |
| 57 | +## FROM → TO |
| 58 | + |
| 59 | +```ts |
| 60 | +// before — parsed green; `postal_code` silently gone from the parsed output |
| 61 | +valueSchemaFor({ type: 'address' }, 'stored').safeParse( |
| 62 | + { street: '1 Main St', city: 'Seattle', state: 'WA', postal_code: '98101', country: 'US' }) |
| 63 | +// => { success: true, data: { street, city, state, country } } |
| 64 | + |
| 65 | +// after — refused, naming the key and the declared spelling |
| 66 | +// => { success: false, error: { issues: [{ code: 'unrecognized_keys', keys: ['postal_code'], |
| 67 | +// message: 'Unrecognized key(s) on this address value: `postal_code`. Did you mean `postal_code` → `postalCode`? …' }] } } |
| 68 | +``` |
| 69 | + |
| 70 | +Fix: spell the key as the contract declares it — `postal_code` → `postalCode` |
| 71 | +in the producer (seed, importer, geocoder adapter, widget). For a location, |
| 72 | +`latitude` / `longitude` → `lat` / `lng`; drop device extras such as |
| 73 | +`heading` / `speed` or model them as fields of their own. Run |
| 74 | +`os migrate value-shapes` to find stored values that carry undeclared keys. |
0 commit comments