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
13 changes: 13 additions & 0 deletions .changeset/lookup-picker-reader-prose-remeasured.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@objectstack/spec": patch
---

The lookup-picker "who reads this" claims in `packages/spec` are re-measured against objectui and dated to the commit they were measured on. No schema, accept set, default or refusal moves — this is evidence prose, and every verdict it sits under is unchanged.

Three claims had gone false, all in the same direction: they credited objectui's picker with reading a `snake_case` alias that objectui no longer reads. A stale *tolerance* claim fails in the dangerous direction — it tells an author a spelling is accepted downstream when it is not, so a value that will silently arrive as nothing looks supported by the spec's own prose.

- **`liveness/field.json`, both `displayField` notes.** `/props/displayField` claimed the record picker "reads displayField || display_field"; `/props/inlineColumns/children/displayField` named the `snake_case` spelling flatly as *the* key the grid's lookup cells pass. objectui deleted that twin from `LookupFieldMetadata` with no deprecation window and no dual read. Both notes now name the read chain they actually have — `LookupField.tsx`'s `fieldMeta?.displayField || fieldMeta?.reference_field || 'name'`, and `GridField.tsx` handing the column's camelCase `displayField` straight through at all three lookup-cell call sites. Both entries stay `status: "live"`: `displayField` is live, and more exclusively so than the notes claimed.
- **`src/data/field.zod.ts`, the LOOKUP PICKER (forward) docblock.** It told authors that objectui's `LookupField` / `RecordPickerDialog` / `deriveLookupColumns` read "both these camelCase keys and their snake_case aliases" — a blanket claim over all seven keys declared beneath it. Measured, it holds for three: `lookupColumns`, `lookupPageSize` and `allowCreate` are each read as `<snake> ?? <camel>`. The other four — `displayField`, `descriptionField`, `lookupFilters` and `dependsOn` — are read camelCase-only. The docblock now states that per key, keeps saying the truth for the three aliases that survive, and records that those three are objectui's own back-compat rather than a spelling this schema declares.
- **`liveness/field.json`, the `valueDomain` `evidence` string.** It described the shared membership predicate as one "the write path **will** call" while its own first clause already quotes the landed call site that calls it. Tense corrected; the pointer is unchanged.

Each rewritten claim now names the objectui commit it is dated to, so a later reader can tell how old the evidence is instead of assuming it is current. That dating is prose by design: a gate over a pinned foreign tree would go stale at every pin bump and need its own anti-vacuity self-test, which is a worse trade than a dated sentence.
6 changes: 3 additions & 3 deletions packages/spec/liveness/field.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@
"valueDomain": {
"status": "live",
"verifiedAt": "2026-09-04",
"evidence": "packages/objectql/src/validation/record-validator.ts#validateOne (the write-path seam, beside `maxLength`'s: `if (def.valueDomain !== undefined && VALUE_DOMAIN_FIELD_TYPES.has(t) && !isValueDomainMember(def.valueDomain, s)) return fail('value_domain', { valueDomain: def.valueDomain }, ...)` \u2014 a non-member WRITTEN to a `text` field declaring a domain is refused with the ADR-0114 code `value_domain` and `constraint.valueDomain`); packages/spec/src/data/field.zod.ts#VALUE_DOMAIN_FIELD_TYPES (the parse-time applicability door: the key is accepted on `text` only and refused with a located `custom` issue at [valueDomain] on every other type — the same superRefine mechanism `maxLength` / `minLength` use); packages/spec/src/shared/value-domain.zod.ts#isValueDomainMember (the ONE membership predicate the write path will call — shared with the settings door)",
"evidence": "packages/objectql/src/validation/record-validator.ts#validateOne (the write-path seam, beside `maxLength`'s: `if (def.valueDomain !== undefined && VALUE_DOMAIN_FIELD_TYPES.has(t) && !isValueDomainMember(def.valueDomain, s)) return fail('value_domain', { valueDomain: def.valueDomain }, ...)` \u2014 a non-member WRITTEN to a `text` field declaring a domain is refused with the ADR-0114 code `value_domain` and `constraint.valueDomain`); packages/spec/src/data/field.zod.ts#VALUE_DOMAIN_FIELD_TYPES (the parse-time applicability door: the key is accepted on `text` only and refused with a located `custom` issue at [valueDomain] on every other type — the same superRefine mechanism `maxLength` / `minLength` use); packages/spec/src/shared/value-domain.zod.ts#isValueDomainMember (the ONE membership predicate the write path calls — shared with the settings door)",
"note": "The write path enforces it since 2026-09-04 (#15161, the engine half of the maintainer ruling 2026-09-02 option A on #14168; the spec half declared the slot, the closed vocabulary, the shared predicate, the ADR-0114 catalog member and its four-locale templates). WRITTEN VALUE ONLY, the `min`/`max`/`maxLength` transition-gate class: a stored value outside a domain declared later is never re-read and survives unrelated edits, and an absent/empty value is the field's `required` handling, not this check \u2014 both pinned in packages/objectql/src/validation/record-validator.value-domain.test.ts, together with the per-domain matrix (iso_3166_alpha2 admits CH and refuses ZZ; iana_time_zone admits UTC and refuses Mars/Olympus; iso_4217_currency admits CHF and refuses chf). The applicability door is one constant read by both seams \u2014 the schema refuses the key outside VALUE_DOMAIN_FIELD_TYPES at parse and the validator judges exactly that set, so the two cannot drift into two opinions (the #11875 discipline; the subset relation to BOUNDED_STRING_FIELD_TYPES, which the enforcement branch rides on, is pinned in the same file). The settings door (`service-settings/value-domains.ts`) has re-pointed onto that same predicate (#15434, the services half of the same ruling): its second copy of all three definitions is deleted and `firstRejectedDomainMember` asks `isValueDomainMember`, so a value Settings admits is the value a field admits and vice versa; what is left on that side is the door's own business (which declarations it agrees to enforce, how a multi-value carrier is walked, the fragments the env-override log line needs), and a re-added local table reddens packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts."
},
"rows": {
Expand All @@ -236,7 +236,7 @@
},
"displayField": {
"status": "live",
"note": "objectui LookupField/RecordPickerDialog — candidate label field in the record picker (reads displayField || display_field)."
"note": "objectui LookupField/RecordPickerDialog — candidate label field in the record picker. Read camelCase-only: packages/fields/src/widgets/LookupField.tsx does `fieldMeta?.displayField || fieldMeta?.reference_field || 'name'` and hands the result to RecordPickerDialog as its `displayField` prop. The snake_case twin this note used to name was deleted from LookupFieldMetadata by objectui#7155 (maintainer ruling A′ of 2026-09-03 — no deprecation window, no dual read), so the camelCase spelling this schema declares is the only authored one the picker reaches for; `reference_field` is objectui's own third leg of that chain, not a spec key (objectui#6875 grades it `no-producer`). Claim dated to objectui ed971e8, re-measured 2026-09-09 — a reading of a pinned foreign tree that nothing in this repo re-checks."
},
"descriptionField": {
"status": "live",
Expand Down Expand Up @@ -333,7 +333,7 @@
},
"displayField": {
"status": "live",
"note": "objectui GridField.tsx lookup cells — LookupField display_field."
"note": "objectui GridField.tsx lookup cells — the column's camelCase `displayField` is handed straight to LookupField as `displayField` at all three lookup-cell call sites (packages/fields/src/widgets/GridField.tsx: the read-only table cell, the list form-factor cell and the editable cell; the column type declares `displayField?: string`). The snake_case twin this note used to name was deleted from LookupFieldMetadata by objectui#7155. Claim dated to objectui ed971e8, re-measured 2026-09-09 — a reading of a pinned foreign tree that nothing in this repo re-checks."
},
"idField": {
"status": "live",
Expand Down
21 changes: 19 additions & 2 deletions packages/spec/src/data/field.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1347,8 +1347,25 @@ export const FieldSchema = lazySchema(() => {
* list of children); these configure the CHILD-side picker that chooses the
* parent. All optional: the renderer auto-derives a sensible multi-column
* result from the referenced object's schema when omitted (objectui
* packages/fields: LookupField / RecordPickerDialog / deriveLookupColumns,
* which read both these camelCase keys and their snake_case aliases).
* packages/fields: LookupField / RecordPickerDialog / deriveLookupColumns).
*
* SNAKE_CASE TOLERANCE — measured on the consumer, not assumed. Those
* readers accept a snake_case alias for three of the seven keys declared
* below, and for three only: `lookupColumns`, `lookupPageSize` and
* `allowCreate` are each read as `<snake> ?? <camel>` in LookupField.tsx.
* The other four — `displayField`, `descriptionField`, `lookupFilters`
* and `dependsOn` — are read camelCase-only, so a snake_case spelling of
* any of those four arrives at the picker as nothing at all. Three of them
* lost their twin in objectui#7155's LookupFieldMetadata alias sweep (no
* deprecation window, no dual read); `dependsOn` lost its own later,
* retired under ADR-0049 enforce-or-remove in objectui#7357. The three
* aliases that survive are objectui's own back-compat, never a spelling
* this schema declares — the camelCase key is the authored one in every
* case.
*
* Claim dated to objectui ed971e8, re-measured 2026-09-09. It is a reading
* of a pinned foreign tree and no gate here re-checks it: when the console
* pin moves, this paragraph is what goes stale.
*/
displayField: z.string().optional().describe("Field shown as each candidate's label in the picker/popover (defaults to the referenced object's name/title)."),
descriptionField: z.string().optional().describe('Secondary field shown under the label in the quick-select popover.'),
Expand Down
Loading