|
| 1 | +--- |
| 2 | +'@objectstack/objectql': minor |
| 3 | +'@objectstack/spec': minor |
| 4 | +--- |
| 5 | + |
| 6 | +feat(objectql,spec): `Field.valueDomain` binds at the write seam — a non-member is refused with `value_domain` (maintainer ruling 2026-09-02 on #14168, engine half) |
| 7 | + |
| 8 | +**BREAKING** accept-set narrowing on the ObjectQL record write path, shipped as |
| 9 | +`minor` under the repo's launch-window convention for breaking changes. |
| 10 | + |
| 11 | +The key is **already published, and published unenforced**. The version-packages |
| 12 | +cut `8a1bad8b8` (2026-09-04 10:20Z) consumed the spec half's changeset |
| 13 | +`field-value-domain-slot.md` and released `@objectstack/spec@17.3.0`, which |
| 14 | +declares `Field.valueDomain`, parses it, and refuses it on any type other than |
| 15 | +`text` — and never reads it when a record is written. The 17.3.0 liveness ledger |
| 16 | +states the gap in its own words: "a non-member WRITTEN to a `text` field |
| 17 | +declaring a domain is accepted today". That write is accepted on 17.3.0 and is |
| 18 | +refused from this release on. |
| 19 | + |
| 20 | +**Refused shape**, precisely: a record write that supplies a value for a `text` |
| 21 | +field whose definition declares `valueDomain`, where the WRITTEN value is not a |
| 22 | +member of the named standard. It fails with the field error code `value_domain`, |
| 23 | +carrying `constraint: { valueDomain }` and a message that names the standard in |
| 24 | +all four platform locales. Nothing else narrows — a field that declares no |
| 25 | +`valueDomain` is untouched, and so is every other field type, because the schema |
| 26 | +accepts the key on `text` alone and the validator judges exactly that set. |
| 27 | + |
| 28 | +**Remedy: write a member of the declared standard.** `iana_time_zone` admits |
| 29 | +`UTC` and refuses `Mars/Olympus`; `iso_4217_currency` admits `CHF` and refuses |
| 30 | +`chf`; `iso_3166_alpha2` admits `CH` and refuses `ZZ`. Dropping the |
| 31 | +`valueDomain` declaration from the field lifts the refusal entirely, for an |
| 32 | +author who declared a domain they did not mean. |
| 33 | + |
| 34 | +**No stored row is touched, and none becomes invalid.** This is the `min` / |
| 35 | +`max` / `maxLength` transition-gate class: a value stored before the domain was |
| 36 | +declared — or before this release — is never re-read, and it survives an edit of |
| 37 | +another field on the same record. An absent or empty value follows the field's |
| 38 | +`required` handling, not this check. |
| 39 | + |
| 40 | +<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is |
| 41 | +renamed, retired or tombstoned. `Field.valueDomain` keeps its name, its type and |
| 42 | +its position; this release only makes the declaration the key already carries |
| 43 | +bind at the write seam, so `objectstack migrate meta` has no metadata to |
| 44 | +rewrite — a document that declares a domain is already in its final spelling, |
| 45 | +and one that declares none is untouched. ⚠️ This disposition does NOT rest on |
| 46 | +the key being unpublished, and must not be read that way: 17.3.0 shipped |
| 47 | +`Field.valueDomain` declared, parsed and UNENFORCED, which is exactly why this |
| 48 | +changeset carries the BREAKING banner above. It rests on the stored side |
| 49 | +instead. A stored value outside a declared domain is never re-read, so no stored |
| 50 | +row is invalidated here and none is reachable by a ledger entry at all. And |
| 51 | +which member a stored non-member SHOULD have been is authoring intent no ledger |
| 52 | +entry can decide: the stored string carries no evidence of whether the author |
| 53 | +meant a different member of that standard, a different standard, or no |
| 54 | +declaration at all. The channel that reaches the author is the refusal itself, |
| 55 | +raised at the write, naming the standard — the same ground the sibling |
| 56 | +accept-set narrowing #15319 stands its own `no-migration-prescription` |
| 57 | +disposition on. --> |
| 58 | + |
| 59 | +- The membership test is the spec's shared `isValueDomainMember` — the same |
| 60 | + predicate, over the same closed vocabulary, that a settings specifier's |
| 61 | + `valueDomain` uses. A time zone accepted in Settings is the time zone |
| 62 | + accepted in a field. |
| 63 | +- The two authoring forms (`fieldForm`, `objectForm`) gain a `valueDomain` |
| 64 | + control, shown on exactly the types the schema accepts the key on. The |
| 65 | + object-form control's choices are derived from the vocabulary, not re-typed. |
0 commit comments